Object-oriented Programming (OOP) Notes

Russell Bateman
last update:


Common mistakes in class design

The law of Demeter

This is a discussion of theory.

This law of object-oriented implementation states that, (this is recouched using Java terminology)

for all classes C, and for all methods M, all objects to which M sends a message (i.e.: consumes) must be:

  1. this,
  2. M's arguments,
  3. instance variables of C,
  4. objects create by M, or by methods that M calls, or
  5. objects in static fields.

An illustration of #1:

public class NetworkConnection
{
  public void close()
  {
    sendShutdownMessage();
  }

  private void sendShutdownMessage()
  {
    ...
  }
}

An illustration of #3:

public class NetworkConnection
{
  private Socket socket;

  public void close()
  {
    socket.close();
  }
}

Working backwards, what does this law prohibit?

First off, this tends to preclude writing classes that contain static, utility methods because the object acted upon are often created elsewhere. A good example is StringUtilities because it typically contains methods that act upon Strings that don't meet any of the five criteria above.

Consider this method in class NetwareConnection:

public class NetworkConnection
{
  public void send( Person person )
  {
    sendBytes( person.getName().getbytes() );
    ...
  }
}

It is not an illustration of #2 above despite that person is an argument, it's not an object of type C. It is permissible to call call methods of type Person on person, for example, to induce person to behave in some way, but it's not permissible to call an accessor like getName() since it produces an object that doesn't belong to NetworkConnection in the way listed among the five rules.

So-called fluent APIs are granted under rule #4:

Report report = new ReportBuilder()
              .withBorder( 1 )
              .withBorderColor( Color.black )
              .withMargin( 3 )
              .withTitle( "Law of Demeter Report" )
              .build();

Most, if not all of the negative examples, that is, of violating the law of Demeter, involve getter methods. In conclusion, examples of its violation are really examples of what everyone does all the time.


Dependency injection

I thought I'd drop a word on a thorny issue in Java that applies equally well to other programming languages.

We little thought back in C of injecting state into objects. We winged it. When I think about how I used to program, I wince. Only academics thought about best practice and discussed it in dissertations. However, increasingly this passes from the realm of academia into practice.

When you instantiate an object, you're suddenly faced with all sorts of problems concerning its state. Creating the initial state of an object is called injection. The object is dependent upon various ways to get its state right:

The dependency part of dependency injection is that an object's state is more often than not dependent upon other state than its own, whether by inheritance or by composition, of the aspects of that object's state.

It's considered ugly to have massive numbers of constructors, but this dissolves into personal religion. It's considered equally ugly to support accessors. Utterly verboten in everyone's religious belief is the manipulation of an object's instance variables directly (because lacking the keyword private).

I tend to hate getters and setters on the basis that an object's data is its own business and no one else's. (Behavior of an object, including what it can tell you, is another matter, but its behavior is a function of its state and what it says should not merely be revelation of state.) A setter is nothing more than violation of the injunction against direct manipulation. It's the same thing as not using private and allowing code to reach into the object and tweak it. Saints and heavenly angels forfend!

Really, the prize sought is immutability: you create an object and launch it upon the world, fully formed and fully able to contribute to what's being done. This can only be done via a constructor or by using a factory to instantiate an object. You do this as close to the creation (instantiation) of the object as possible.

Thereafter, an object's state should only change as a function of its nature and life cycle. In a recent article, acceleration of a Vehicle object is an example.

I won't say much about the factory. It's a design pattern that's very important and covered by the Gang of Four (the original work on design patterns) as well as by the much easier read, Head First Design Patterns (and every other book about this topic). The factory accomplishes two things. First, it can be used to select between multiple types of objects to be created, for example, in creating a pizza crust, it determines, on the basis of other known state (injected by the pizza order), which style of crust to create for the final object of type Pizza. Second, it can and should fully instantiate the object or, at least, that part of the object's state for which it's responsible (because injection can be a very complicated thing especially in complex objects like pizzas).

In summary, we've learned over our lifetime that dependency injection (also confusingly called inversion of control) is a far more crucial problem than just setting the original value, willy-nilly, of a variable (however complex) as we used to do in C. There is a science, but also an art to it. Get it right, things sail smoothly. Get it wrong, you're plagued with instability, bugs, confusion of duties, etc. and you'll never finish sorting things out. In short, huge though avoidable technical debt. The right way is in as few places as possible (e.g.: only ever via constructors or only ever via setters), as predictably as possible (why design patterns for it exist), and mutability is to be avoided or, at least, controlled.

This note on dependency injection doesn't merely apply to Java. You will find it useful to think hard about DI when you program in C. One danger of working in C is that you already know it and, long ago, developed bad habits in it. Thinking about what we have to learn when getting into Java (and C# and C++) is helpful.

Originally, in C and in other languages, we had

...and thought of the data as our objects, if we thought in terms of objects at all. But, the behavior wasn't associated directly with the data (object). Object-oriented languages fix that (more than anything else they do) by confining data (state) and behavior (methods) to each object (as defined by the class, a template). This fix is more overt in Python, which is a procedural language that was retrofitted with objects after the fact. However, I won't delve into that right now.

In C, you can still care about how your "objects" (structures) get initialized to save yourself grief later—all I'm saying.