I am a programmer and architect (the kind that writes code) with a focus on testing and open source; I maintain the PHPUnit_Selenium project. I believe programming is one of the hardest and most beautiful jobs in the world. Giorgio is a DZone MVB and is not an employee of DZone and has posted 638 posts at DZone. You can read more from them at their website. View Full User Profile

What's in a constructor?

04.24.2013
| 2710 views |
  • submit to reddit

The constructor of an object contains a declaration of its dependencies, in hardwired or injected form. There are a few exceptions, such as creating new objects all over the place inside other methods (with no regard for Dependency Injection), accepting objects as method parameters or with setters; however, constructor injection remains one of the simplest and more powerful ways to build a web of collaborating objects.

Here are the stages I went trough regarding constructors. Read the phases not only as a possible path in design skills, but also as a road for refactoring towards that web of objects based on Dependency Inversion (sic).

Phase 1: centralize creation of collaborators

The simplest constructors just declare hardcoded dependencies:

class Car
{
   Formula1Driver driver;   
   String manufacturer;
   Car() {
     driver = new Formula1Driver();
     manufacturer = "Ferrari";
   }
}

(Car examples are my favorite. I'm writing in a pseudo-Java as I don't want to be language-specific).

I don't call this phase 0 just because object creation is starting to be segregated. I call the dependency hardcoded because there is no way to change the concrete class Formula1Driver, or even to configure it differently without having to break the encapsulation of Car (for example exposing it with Car.getDriver()).

Phase 1.5: interfaces exist

In this phase we start to recognize that the interface called by Car does not necessarily coincide with the public methods of Formula1Driver:

class Car
{
   Driver driver;   
   String manufacturer;
   Car() {
     driver = new Formula1Driver();
     manufacturer = "Ferrari";
   }
}

Formula1Driver implements Driver. Usually, we are not calling Formula1Driver.wear(helmet, suit) or Formula1Driver.uncorkChampagne() from inside Car: in fact, they're not methods present in the Driver interface.

Value Objects such as strings and similar immutable objects do not usually have an interface that can be extracted. Bear with me to see where they end up.

Phase 2: injection of existing collaborators

class Car
{
   Driver driver;
   String manufacturer;
   Car(Driver driver, String model) {
     this.driver = driver;
     this.manufacturer = manufacturer;
   }
}

A Factory object (or another creational pattern) is passing in dependencies to the Car class when a new object is instantiated. This may happen at the application level (if the Car has a long lifecycle) or in smaller scopes (if you create several Cars in a web application in order to satisfy each HTTP request, for example).

Phase 3: outside-in

We can't really improve the constructor in isolation now: we have reached a local minimum by applying mechanically Dependency Injection. However, if we stick to the rule of injecting dependencies we are prone to fall into this situation:

   Car(Driver driver, String manufacturer, String tires, String engineSize, ...) {
     this.driver = driver;
     this.manufacturer = manufacturer;
  this.tires = 'Goodyear';
     this.engineSize = engineSize;
     ...
   }

This is high extrinsic coupling, a word popularized by Greg Young to indicate an high number of outgoing dependencies. Read high as more than 3 (even 2 in some cases where we can do better).

To prevent this situation, let's go back to the original constructor from phase 2:

   Car(Driver driver, String manufacturer) {
     this.driver = driver;
     this.manufacturer = manufacturer;
   }

We are injecting manufacturer from outside, and we are injecting as a base type to reduce dependencies instead of giving the Car some Configuration object (a typical focal point where all dependencies converge).

If we take a look at how manufacturer is used in the class, we see:

   public String toString()
   {
     return manufacturer + " driven by " + driver.toString();
   }

I will stick with just this toString() logic because:

  • it shines some light on how we have to add code to exit the local minimum, even if it seems not useful at the time (the Open/Closed Principle tells us to close against modifications before developing new requirements; the 3rd rule of simple design tells us to express all concepts.)
  • working on more complex logic would require a book chapter more than this post.

If you see a likely expansion of properties which are more related to the manufacturer than to the Driver (or already have the new user story that requires them in your hands), you can introduce a new abstraction over the primitive String type:

   Car(Driver driver, Model model) {
     this.driver = driver;
     this.model = model;
   }
   public String toString()
   {
     return model.toString() + " driven by " + driver.toString();
   }

By introducing the Model class, you gain several possible points where you can fit new behavior (and the collaborators it requires). By using a single class that contains several procedures, you can only add new dependencies to it, increasing extrinsic coupling. With a web of objects, you can substitute existing objects with new ones or expand one of the existing classes with new collaborators instead of reworking on the same code and constructor every time.

This kind of development is usually driven by tests and is called outside-in by the authors of GOOS, because Car is the first object to be created, pulling into existence the Driver and Model interfaces. We've been reworking existing code to make these abstractions emerge, but with more experience we can get to the point where interfaces are introduced before their implementations, but still having faith that the objects implementing those protocols will be feasible and cohesive.

Published at DZone with permission of Giorgio Sironi, author and DZone MVB.

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)