Wednesday, December 30, 2015

SOLID Principles: SRP and DIP (1 of 4)

In this series I'll explain five of the core O.O. design principles. Each entry will have an Agile Link section describing how the explained principle(s) relates to Agile thinking.

Single responsibility principle (SRP)

The SRP states that elements (classes and methods), should have only one reason to change. They must be focused on a single task, anything related to that task must be extracted as services / helpers and need to be placed inside other components, called dependencies.

This post will cover another core principle:

Dependency inversion principle (DIP)

Simply put, this principle says that both high and low level components should depend on abstractions (contracts), not on details.

Code sample

Code samples are on github repo lsolano/blog.solid.demo
This code sample (java SE 1.8), is a fake "ClientsReportBuilder" component, the ideal responsibility for this guy is to build a "Clients Report" pulling the data from underlying persistence mechanism and finally formatting it using one of the following (output formats): XML, JSON, or CSV. Code comments are omitted to keep it as short as possible.

This is the initial (dirty) version, the behavior is defined by the contract ClientsReportBuilder, and implemented by DirtyClientsReportBuilder. This version of the method buildNewClientsReports has around 40 lines of code (LOC). It has a very straitforward logic:
  • First, it does some validations for method's parameters.
  • Second, it instantiates a known version of the repository ClientsRepository (InMemoryClientsRepository), and uses it to get all clients with sign-on date within the given date range.
  • Third, it examines the format parameter and for each possible value builds the resulting string to return. Also if the format is not supported it throws and exception.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import java.util.Date;
import java.util.stream.Stream;

public class DirtyClientsReportBuilder implements ClientsReportBuilder {

  @Override
  public String buildNewClientsReport(final Date from, final Date to, final OutputFormat format) {
    if (from == null || to == null || format == null) {
      throw new IllegalArgumentException("Arguments can not be null.");
    }

    if (from.compareTo(to) > 0) {
      throw new IllegalArgumentException(String.format("Invalid date range [%s, %s].", from, to));
    }

    final ClientsRepository clientsRepo = new InMemoryClientsRepository();

    final Stream<Client> clients = clientsRepo.getBySignOnDate(from, to);
    final StringBuffer sb = new StringBuffer();
    switch (format) {
    case XML:
      sb.append("<report>");
      clients.parallel()
          .forEach(client -> sb.append("<client><name>").append(client.getName()).append("</name></client>"));
      sb.append("</report>");
      break;

    case JSON:
      sb.append('[');
      clients.parallel().forEach(client -> sb.append("{ \"name\": \"").append(client.getName()).append("\" },"));
      sb.deleteCharAt(sb.length() - 1);
      sb.append(']');
      break;

    case CSV:
      sb.append(String.format("email,name%n"));
      clients.parallel().forEach(client -> sb.append(client.getEmail()).append(",").append(client.getName()));
      break;

    default:
      throw new IllegalArgumentException(String.format("Format not supported '%s'.", format));
    }

    return sb.toString();
  }
}

What is wrong with this code: SRP?

  1. At the class level: this class is supposed to "build report(s)" but is doing a little more: it knows to much about the repository used, and also knows about how to handle all output formats. This means that:
    1. If a new format is added, this class must change
    2. If a different implementation of the repository (flat file, RDB, No-SQL DB, ...), is used this class must change
  2. At the method level: this method is supposed to build the report using helper entities within its class or as outside services. We can count up to 3 responsibilities here:
    1. The parameters validation logic,
    2. Pulling out clients from repository, and
    3. Building the report using the proper format
In order to fix these violations we must split as much as possible all things done by the dirty class.

What is wrong with this code: DIP?

  1. This class depends on the ClientsRepository but actually has to much information about it. It knows about the implementation details.
  2. Conceptually, it must depend on a "formatter" component, actually spread inside the switch statement.
To fix the violations to the DI principle we must extract these two dependencies and define contracts (abstractions) so both high and low level components can interact knowing as little as possible about each other. Just to be clear, in this example the high level component is the ClientsReportBuilder and the low level ones are the repository and the (not yet created) formatter. 

Cleaning the code

Honoring SRP

At the class level the helper responsibilities where extracted:
  • A dependency to the repository contract was introduced to the, new and only, constructor
  • A dependency to builder / factory function for the formatter component, which will take the Client's collection (stream) and return the final string with the proper format.
At the method level, the following changes were made:
  • The parameters validation logic was extracted into a private method
  • The formatter was extracted to a helper factory function using the Optional pattern. If the format is invalid (not managed yet), the Option will be empty
  • The method was reduced from around 40 lines to just 12 lines of code.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import java.util.Date;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Stream;

public class CleanClientsReportBuilder implements ClientsReportBuilder {

  private final Function<OutputFormat, Optional<Function<Stream<Client>, String>>> formattersFactory;
  private final ClientsRepository clientsRepo;

  public CleanClientsReportBuilder(
      final Function<OutputFormat, Optional<Function<Stream<Client>, String>>> formattersFactory,
      final ClientsRepository clientsRepo) {
    this.formattersFactory = formattersFactory;
    this.clientsRepo = clientsRepo;
  }

  @Override
  public String buildNewClientsReport(final Date from, final Date to, final OutputFormat format) {
    validateReportParameters(from, to, format);

    final Stream<Client> clients = clientsRepo.getBySignOnDate(from, to);

    final Optional<Function<Stream<Client>, String>> formatter = this.formattersFactory.apply(format);

    if (formatter.isPresent()) {
      return formatter.get().apply(clients);
    }

    throw new IllegalArgumentException(String.format("Format not supported '%s'.", format));
  }

  private void validateReportParameters(final Date from, final Date to, final OutputFormat format) {
    if (from == null || to == null || format == null) {
      throw new IllegalArgumentException("Arguments can not be null.");
    }

    if (from.compareTo(to) > 0) {
      throw new IllegalArgumentException(String.format("Invalid date range [%s, %s].", from, to));
    }
  }
}

Honoring the DIP

To use this version of the component we need to do the following (see tests on GitHub repository).

1
2
3
final ClientsReportBuilder clientReportBuilder = 
 new CleanClientsReportBuilder(ClientsReportFormatter.buildFormattersFactory(), 
          new InMemoryClientsRepository());

Basically, we set both dependencies using the constructor's parameters. With a good IOC tool you just inject those into the component. Here both dependencies were extracted and supplied, achieving the following goals:

  • The report builder component knows nothing about the implementation of the repository. It only knows about the contract (using a java interface).
  • The report builder component knows nothing about the formatter components family. If only requires a factory function that will give Nothing or an actual formatter function.


Agile Link

I'll elaborate the code link to agile thinking using the Agile Principles from the manifesto.
Welcome changing requirements, even late in development. Agile processes harness change for the customer's competitive advantage.
With the final implementation is easy to add a new output format. Also we can change the repository implementation without impacting the report builder. Adding / fixing the validation logic is easy because is encapsulated within a single, short method.

Agile processes promote sustainable development. ... [We] should be able to maintain a constant pace indefinitely.
With a clean code base we avoid the "maintenance hell". We avoid working extra hours and burning the team in the process. Remember this is job, we must not die like heroes in the process, if that what you want join the army :)

Continuous attention to technical excellence and good design enhances agility.
"Excellence" is a relative term, but code's final version is more elegant than the first. Surely, someone in the future can improve the code even further but is our job to let the code as clean and simple as our current knowledge level allows.

Next on this series

SOLID Principles: Open/closed principle (OCP) 

2 comments:

  1. Clear and concise explanation of those principles.

    ReplyDelete
  2. Code samples are on github repo lsolano/blog.solid.demo ( https://github.com/lsolano/blog.solid.demo )

    ReplyDelete

enter your comments here...

Subscribe to our mailing list

* indicates required
Email Format