Dependency Inversion - Applying the principle

11 Jul 2016 OO, Java, Design, Pattern

The Dependency Inversion Principle was named in 1994 but quite often it is overshadowed by Dependency Injection. They share a number of similarities and outcomes but while Dependency Injection is about providing dependencies based on context Dependency Inversion is about layering. Dependency Inversion is very useful refactoring interaces defined with getters (attribute accessor methods) into something more extensible. Here’s how:

Intent
  • High-level modules should not depend on low-level modules. Both should depend on abstractions.

  • Abstractions should not depend on details. Details should depend on abstractions.

An abstraction != interface but an interface is an abstraction.

Typical dependency. The architecturally higher level component depends on the lower level implementation.

Inverted dependency. The higher level component depends on abstractons. The lower level component depends on the higher level’s implementation.

typical dependency

into

inverted dependency
Job Scheduler example

Job scheduling is an example of a fairly common orchestraton problem. We have an application that wants to provide the execution ruls (when) but not what has to happen.

In this example we have a simple interface declaration Job that defines a couple of properties for the name and schedule that the Job requires. A concrete realization of that class SimpleJob and then the calling application.

There are a few problems with this design. A job can only be run on a single schedule. The calling application interprets property values and the two classes share knowledge through the Schedule enum, limiting the options available.

As the interface evolves additional properties will be needed. Interdependence between properties will increase the complexity of the calling code, increasing the risks of complex defects.

public interface Job {
    String getName();
    Schedule getSchedule();
    void run(PrintStream output);
}

public class SimpleJob implements Job {

    public String getName() {
        return "Simple";
    }

    public Schedule getSchedule() {
        return Schedule.DAILY;
    }

    public void run(PrintStream output) {
        output.println("Daily activity");
    }
}

public class Application {
    private final Map<String, Job> hourlyJobs = new HashMap<String, Job>();
    private final Map<String, Job> dailyJobs = new HashMap<String, Job>();

    public static void main(String[] args) {
        new Application().run();
    }

    private void run() {
        final Job job = new SimpleJob();

        switch (job.getSchedule()) {
            case HOURLY:
                hourlyJobs.put(job.getName(), job);
                break;
            case DAILY:
                dailyJobs.put(job.getName(), job);
                break;
        }
    }
}

An interface that defines getters is a smell. Interfaces should define behaviour not access to attributes. To every rule there are exceptions but it stands true in this case:

Testing is pretty difficult. All the logic for registration and execution is handled within the parent. SimpleJob has no registration logic and at most the property accesses can be tested.

Improving the dependencies. In this example the calling code and called code share an interface declaration Registrar. The Job interface is much simpler, defining just two methods. One to register the job within the context, and the second to execute the job.

The calling application code is also simpler, removing the need for an enum entirely by implementing the Registrar interface.

The previous limitations are removed. Jobs can register themselves to multiple schedules without changing the Registrar interface.

public interface Job {
    void register(Registrar registrar);
    void run(PrintStream output);
}

public interface Registrar {
    void daily(String name, Job job);

    void hourly(String name, Job job);
}

public class SimpleJob implements Job {
    public void register(Registrar registrar) {
        registrar.daily("Simple", this);
        registrar.hourly("Simple", this);
    }

    public void run(PrintStream output) {
        output.println("Daily activity");
    }
}

public class Application implements Registrar {
    private final Map<String, Job> hourlyJobs = new HashMap<String, Job>();
    private final Map<String, Job> dailyJobs = new HashMap<String, Job>();

    public static void main(String[] args) {
        new Application().run();
    }

    private void run() {
        final Job job = new SimpleJob();
        job.register(this);
    }

    public void daily(String name, Job job) {
        dailyJobs.put(name, job);
    }

    public void hourly(String name, Job job) {
        hourlyJobs.put(name, job);
    }
}

Testing is also improved. The registrar can be stubbed or mocked to test the registration process.

    @Test
    public void registersSelfHourlyAndDailyAsSimple() {
        Registrar registrar = mock(Registrar.class);

        SimpleJob job = new SimpleJob();

        job.register(registrar);

        verify(registrar).daily("Simple", job);
        verify(registrar).hourly("Simple", job);
    }

Summary

Getter interfaces should be avoided. Any interface that interrogates objects to decide what to do breaks the encapsulaton of the class.

A -depends on→ B into B -dependes on→ A. This typically shows up as list of getters in B that are used by A.

Linkedin

Graham Brooks Photo

This is a personal weblog. The opinions expressed here represent my own and not those of my employer (ThoughtWorks).

My thoughts and opinions change over time as I learn. This weblog is intended to provide a semi-permanent record of these thoughts and is for informational purposes only.

comments powered by Disqus