09 May 2007

Object Collaboration Stereotypes

Synaesthesia

Another diagnosis for a Bloated Constructor might be that not all of the arguments are dependencies, services that must be passed to the object before it can do its job. Other arguments might be more to do with structuring the implementation of the object or varying its behaviour to suit its neighbours. To help me think about this distinction, I've found it useful to categorise the peers of an object into four stereotypes:

  • Dependencies are services that the object needs from its environment so that it can fulfil its responsibilities.
  • Notifications are other parts of the system that need to know when the object changes state or performs an action.
  • Policies are objects that tweak or adapt the object's behaviour to the needs of the system.
  • Parts are components in the implementation that are not controlled from outside the object after being set.

Dependencies must be passed in through the constructor. An object needs the system to provide implementations of its dependencies to function at all, so there's no point in creating an instance until they're available. Partially creating an object and then finishing it off by setting properties is risky and brittle because the programmer has to know whether all the dependencies have been set. As Yoda said, "New, or new not. There is no try."

Other types of peer can be passed to the constructor as a convenience but, if the list is becoming too long, they can also be initialised to safe defaults and overridden later (one way to distinguish the two is that there are no safe defaults for a Dependency). Policies and Parts can be initialised to commonly used cases, and collections of Parts can be initialised as empty. I can then add setters and add/remove methods to the class to allow the object's clients to configure it. Similarly, Notifications can be implemented as events. Unicast events can be initialised with a null implementation of the listener interface, and multicast events can be initialised with an empty collection of listeners. I always create a valid object, and I also have the hooks I need for testing.

A high speed example

Here's an example, it's probably not quite bad enough to need fixing, but it'll do to make the point. The application is a Formula 1 racing game, players can try out different configurations of car and driving style to see which one wins. A RacingCar represents a competitor within a race.

public class RacingCar {
  private final Track track;
  private Tyres tyres;
  private Suspension suspension;
  private Wing frontWing;
  private Wing backWing;
  private double fuelLoad;
  private CarListener listener;
  private DrivingStrategy driver;

  public RacingCar(Track track, DrivingStrategy driver, 
                  Tyres tyres, Suspension suspension, 
                  Wing frontWing, Wing backWing, double fuelLoad,
                  CarListener listener)
  {
    this.track = track;
    this.driver = driver;
    this.tyres = tyres;
    this.suspension = suspension;
    this.frontWing = frontWing;
    this.backWing = backWing;
    this.fuelLoad = fuelLoad;
    this.listener = listener;
  }
}

It turns out that the track is the only Dependency of a RacingCar, the hint is that it's the only field that's final. The driver is a Policy, the listener is a Notification, and the rest are Parts; all of these can be modified by the user before or during the race. Here's a reworked constructor:

public class RacingCar {
  private final Track track;

  private DrivingStrategy driver = DriverTypes.borderlineAggressiveDriving();
  private Tyres tyres = TyreTypes.mediumSlicks();
  private Suspension suspension = SuspensionTypes.mediumStiffness();;
  private Wing frontWing = WingTypes.mediumDownforce();
  private Wing backWing = WingTypes.mediumDownforce();
  private double fuelLoad = 0.5;

  private CarListener listener = CarListener.NONE;

  public RacingCar(Track track) {
    this.track = track;
  }
    
  public void setSuspension(Suspension suspension) { …
  public void setTyres(Tyres tyres) {  …
  public void setEngine(Engine engine) {  …
   
  public void setListener(CarListener listener) {  …
  …
}

Now I've initialised the peers to most common defaults, the user can then configure them through the user interface. I've initialised the listener to a null implementation, again this can be changed later by the object's environment.

A matter of taste

These stereotypes are only heuristics to help me think about the design, not hard rules, so I don't get obsessed with finding just the right classification of an object's peers. What matters most is the context in which I'm developing the object, my mental model of the domain. For example, an auditing log might be a Dependency, since it's a legal requirement for the business, or a Notification, since the object will otherwise function without it. Policies belong to the environment and can usually be constants, since they're often immutable and so don't need to be instantiated more than once. Parts belong to the object and usually cannot be stateless, so they need a new instance. For example, in my Racing Car, tyres might be a Policy if TyreTypes.mediumSlicks() describes only how the choice of tyre affects the car's handling, but a Part if it stores mutable values such as air pressure and wear.

4 comments:

Anonymous said...

Great post!
I'm wondering since a while how I could classify my dependencies and this post is the answer. Furthermore regarding unit tests: if a class is an integral part of my object under test maybe I can test it together with my object under test without mocking/stubing it out. This helps keeping the unit tests more stable if you do refactoring. In this case I would consider my object under test together with the contained objects as a "Unit" that is tested at once. Does this make sense?

Steve Freeman said...

Yes, sometimes we test a small cluster of objects. Whether we test the "contained" class separately depends on how complex it is. We might write a smaller number of higher-level tests to work out how the pieces fit together, then thoroughly exercise the pieces in individual unit tests — or not.

The issue for us is not so much stability in the face of refactoring, as what makes sense in terms of inputs and outputs for an object.

Unknown said...

Very interesting. For the past couple of years I've advocated almost the opposite. Here's my thinking.

1) Unnecessary use of setters makes it very difficult later on to track down just how any variable got set. This an issue from time to when you have otherwise well tested code that gets strange value.

2) People tend to use what ever setters are made available to them - whether their use is a wise choice.

Net result: I give people multiple constructors or if I'm using .NET, I provide defaults in the constructor.

Maybe you work with people you trust more.

Steve Freeman said...

@Mark We should have made it clearer that the only things that are settable are those that make sense being changed over the lifetime of the object. Everything else is locked down during construction.

Personally, I tend towards smaller, more immutable objects, but what's right for the domain take precedence. The point of the article is to help to distinguish between the two.

And of course I prefer to work with people I trust...