Chapter 2. SOLID Principles

Lambda-Enabled SOLID Principles

The SOLID principles are a set of basic principles for designing OO programs. The name itself is an acronym, with each of the five principles named after one of the letters: Single responsibility, Open/closed, Liskov substitution, Interface segregation, and Dependency inversion. The principles act as a set of guidelines to help you implement code that is easy to maintain and extend over time.

Each of the principles corresponds to a set of potential code smells that can exist in your code, and they offer a route out of the problems caused. Many books have been written on this topic, and I’m not going to cover the principles in comprehensive detail.

In the case of all these object-oriented principles, I’ve tried to find a conceptually related approach from the functional-programming realm. The goal here is to both show functional and object-oriented programming are related, and also what object-oriented programmers can learn from a functional style.

The Single-Responsibility Principle

Every class or method in your program should have only a single reason to change.

An inevitable fact of software development is that requirements change over time. Whether because a new feature needs to be added, your understanding of your problem domain or customer has changed, or you need your application to be faster, over time software must evolve.

When the requirements of your software change, the responsibilities of the classes and methods that implement these requirements also change. If you have a class that has more than one responsibility, when a responsibility changes, the resulting code changes can affect the other responsibilities that the class possesses. This possibly introduces bugs and also impedes the ability of the code base to evolve.

Let’s consider a simple example program that generates a BalanceSheet. The program needs to tabulate the BalanceSheet from a list of assets and render the BalanceSheet to a PDF report. If the implementer chose to put both the responsibilities of tabulation and rendering into one class, then that class would have two reasons for change. You might wish to change the rendering in order to generate an alternative output, such as HTML. You might also wish to change the level of detail in the BalanceSheet itself. This is a good motivation to decompose this problem at the high level into two classes: one to tabulate the BalanceSheet and one to render it.

The single-responsibility principle is stronger than that, though. A class should not just have a single responsibility: it should also encapsulate it. In other words, if I want to change the output format, then I should have to look at only the rendering class and not at the tabulation class.

This is part of the idea of a design exhibiting strong cohesion. A class is cohesive if its methods and fields should be treated together because they are closely related. If you tried to divide up a cohesive class, you would result in accidentally coupling the classes that you have just created.

Now that you’re familiar with the single-responsibility principle, the question arises, what does this have to do with lambda expressions? Well lambda expressions make it a lot easier to implement the single-responsibility principle at the method level. Let’s take a look at some code that counts the number of prime numbers up to a certain value (Example 2-1).

Example 2-1. Counting prime numbers with multiple responsibilities in a method
public long countPrimes(int upTo) {
    long tally = 0;
    for (int i = 1; i < upTo; i++) {
        boolean isPrime = true;
        for (int j = 2; j < i; j++) {
            if (i % j == 0) {
                isPrime = false;
            }
        }
        if (isPrime) {
            tally++;
        }
    }
    return tally;
}
    

It’s pretty obvious that we’re really doing two different responsibilities in Example 2-1: we’re counting numbers with a certain property, and we’re checking whether a number is a prime. As shown in Example 2-2, we can easily refactor this to split apart these two responsibilities.

Example 2-2. Counting prime numbers after refactoring out the isPrime check
public long countPrimes(int upTo) {
    long tally = 0;
    for (int i = 1; i < upTo; i++) {
        if (isPrime(i)) {
            tally++;
        }
    }
    return tally;
}

private boolean isPrime(int number) {
    for (int i = 2; i < number; i++) {
        if (number % i == 0) {
            return false;
        }
    }
    return true;
}
    

Unfortunately, we’re still left in a situation where our code has two responsibilities. For the most part, our code here is dealing with looping over numbers. If we follow the single-responsibility principle, then iteration should be encapsulated elsewhere. There’s also a good practical reason to improve this code. If we want to count the number of primes for a very large upTo value, then we want to be able to perform this operation in parallel. That’s right—the threading model is a responsibility of the code!

We can refactor our code to use the Java 8 streams library (see Example 2-3), which delegates the responsibility for controlling the loop to the library itself. Here we use the range method to count the numbers between 0 and upTo, filter them to check that they really are prime, and then count the result.

Example 2-3. Counting primes using the Java 8 streams API
public long countPrimes(int upTo) {
    return IntStream.range(1, upTo)
                    .filter(this::isPrime)
                    .count();
}

private boolean isPrime(int number) {
    return IntStream.range(2, number)
                    .allMatch(x -> (number % x) != 0);
}
    

So, we can use higher-order functions in order to help us easily implement the single-responsibility principle.

The Open/Closed Principle

Software entities should be open for extension, but closed for modification.

Bertrand Meyer

The overarching goal of the open/closed principle is similar to that of the single-responsibility principle: to make your software less brittle to change. Again, the problem is that a single feature request or change to your software can ripple through the code base in a way that is likely to introduce new bugs. The open/closed principle is an effort to avoid that problem by ensuring that existing classes can be extended without their internal implementation being modified.

When you first hear about the open/closed principle, it sounds like a bit of a pipe dream. How can you extend the functionality of a class without having to change its implementation? The actual answer is that you rely on an abstraction and can plug in new functionality that fits into this abstraction. We can also use higher-order functions and immutability to achieve similar aims in a functional style.

Abstraction

Robert Martin’s interpretation of the open/closed principle was that it was all about using polymorphism to easily depend upon an abstraction. Let’s think through a concrete example. We’re writing a software program that measures information about system performance and graphs the results of these measurements. For example, we might have a graph that plots how much time the computer spends in user space, kernel space, and performing I/O. I’ll call the class that has the responsibility for displaying these metrics MetricDataGraph.

One way of designing the MetricDataGraph class would be to have each of the new metric points pushed into it from the agent that gathers the data. So, its public API would look something like Example 2-4.

Example 2-4. The MetricDataGraph public API
    class MetricDataGraph {

        public void updateUserTime(int value);

        public void updateSystemTime(int value);

        public void updateIoTime(int value);

    }
    

But this would mean that every time we wanted to add in a new set of time points to the plot, we would have to modify the MetricDataGraph class. We can resolve this issue by introducing an abstraction, which I’ll call a TimeSeries, that represents a series of points in time. Now our MetricDataGraph API can be simplified to not depend upon the different types of metric that it needs to display, as shown in Example 2-5.

Example 2-5. Simplified MetricDataGraph API
    class MetricDataGraph {

        public void addTimeSeries(TimeSeries values);

    }
    

Each set of metric data can then implement the TimeSeries interface and be plugged in. For example, we might have concrete classes called UserTimeSeries, SystemTimeSeries, and IoTimeSeries. If we wanted to add, say, the amount of CPU time that gets stolen from a machine if it’s virtualized, then we would add a new implementation of TimeSeries called StealTimeSeries. MetricDataGraph has been extended but hasn’t been modified.

Higher-Order Functions

Higher-order functions also exhibit the same property of being open for extension, despite being closed for modification. A good example of this is the ThreadLocal class. The ThreadLocal class provides a variable that is special in the sense that each thread has a single copy for it to interact with. Its static withInitial method is a higher-order function that takes a lambda expression that represents a factory for producing an initial value.

This implements the open/closed principle because we can get new behavior out of ThreadLocal without modifying it. We pass in a different factory method to withInitial and get an instance of ThreadLocal with different behavior. For example, we can use ThreadLocal to produce a DateFormatter that is thread-safe with the code in Example 2-6.

Example 2-6. A ThreadLocal date formatter
// One implementation
ThreadLocal<DateFormat> localFormatter
    = ThreadLocal.withInitial(SimpleDateFormat::new);

// Usage
DateFormat formatter = localFormatter.get();
    

We can also generate completely different behavior by passing in a different lambda expression. For example, in Example 2-7 we’re creating a unique identifier for each Java thread that is sequential.

Example 2-7. A ThreadLocal identifier
// Or...
AtomicInteger threadId = new AtomicInteger();
ThreadLocal<Integer> localId
    = ThreadLocal.withInitial(() -> threadId.getAndIncrement());

// Usage
int idForThisThread = localId.get();

Immutability

Another interpretation of the open/closed principle that doesn’t follow in the object-oriented vein is the idea that immutable objects implement the open/closed principle. An immutable object is one that can’t be modified after it is created.

The term “immutability” can have two potential interpretations: observable immutability or implementation immutability. Observable immutability means that from the perspective of any other object, a class is immutable; implementation immutability means that the object never mutates. Implementation immutability implies observable immutability, but the inverse isn’t necessarily true.

A good example of a class that proclaims its immutability but actually is only observably immutable is java.lang.String, as it caches the hash code that it computes the first time its hashCode method is called. This is entirely safe from the perspective of other classes because there’s no way for them to observe the difference between it being computed in the constructor every time or cached.

I mention immutable objects in the context of this report because they are a fairly familiar concept within functional programming. They naturally fit into the style of programming that I’m talking about.

Immutable objects implement the open/closed principle in the sense that because their internal state can’t be modified, it’s safe to add new methods to them. The new methods can’t alter the internal state of the object, so they are closed for modification, but they are adding behavior, so they are open to extension. Of course, you still need to be careful in order to avoid modifying state elsewhere in your program.

Immutable objects are also of particular interest because they are inherently thread-safe. There is no internal state to mutate, so they can be shared between different threads.

If we reflect on these different approaches, it’s pretty clear that we’ve diverged quite a bit from the traditional open/closed principle. In fact, when Bertrand Meyer first introduced the principle, he defined it so that the class itself couldn’t ever be altered after being completed. Within a modern Agile developer environment, it’s pretty clear that the idea of a class being complete is fairly outmoded. Business requirements and usage of the application may dictate that a class be used for something that it wasn’t intended to be used for. That’s not a reason to ignore the open/closed principle though, just a good example of how these principles should be taken as guidelines and heuristics rather than followed religiously or to the extreme. We shouldn’t judge the original definition too harshly, however, since it used in a different era and for software with specific and defined requirements.

A final point that I think is worth reflecting on is that in the context of Java 8, interpreting the open/closed principle as advocating an abstraction that we can plug multiple classes into or advocating higher-order functions amounts to the same approach. Because our abstraction needs to be represented by an interface upon which methods are called, this approach to the open/closed principle is really just a usage of polymorphism.

In Java 8, any lambda expression that gets passed into a higher-order function is represented by a functional interface. The higher-order function calls its single method, which leads to different behavior depending upon which lambda expression gets passed in. Again, under the hood, we’re using polymorphism in order to implement the open/closed principle.

The Liskov Substitution Principle

Let q(x) be a property provable about objects x of type T. Then q(y) should be true for objects y of type S where S is a subtype of T.

The Liskov substitution principle is often stated in these very formal terms, but is actually a very simple concept. Informally we can think of this as meaning that child classes should maintain the behavior they inherit from their parents. We can split out that property into four distinct areas:

  • Preconditions cannot be strengthened in a subtype. Where the parent worked, the child should.
  • Postconditions cannot be weakened in a subtype. Where the parent caused an effect, then the child should.
  • Invariants of the supertype must be preserved in a subtype. Where parent always stuck left or maintained something, then the child should as well.
  • Rule from history: don’t allow state changes that your parent didn’t. For example, a mutable point can’t subclass an immutable point.

Functional programming tends to take a different perspective to LSP. In functional programming inheritance of behavior isn’t a key trait. If you avoid inheritance hierachies then you avoid the problems that are associated with them, which is the antipattern that the Liskov substitution principle is designed to solve. This is actually becoming increasing accepted within the object-oriented community as well through the composite reuse principle: compose, don’t inherit.

The Interface-Segregation Principle

The dependency of one class to another one should depend on the smallest possible interface

In order to properly understand the interface-segregation principle, let’s consider a worked example in which we have people who work in a factory during the day and go home in the evening. We might define our worker interface as follows:

Example 2-8. Parsing the headings out of a file
    interface Worker {
        public void goHome();
        public void work();
    }
    

Initially our AssemblyLine requires two types of Worker: an AssemblyWorker and a Manager. Both of these go home in the evening but have different implementations of their work method depending upon what they do.

As time passes, however, and the factory modernizes, they start to introduce robots. Our robots also do work in the factory, but they don’t go home at the end of the day. We can see now that our worker interface isn’t meeting the ISP, since the goHome() method isn’t really part of the minimal interface.

Now the interesting point about this example is that it all relates to subtyping. Most statically typed object-oriented languages, such as Java and C++, have what’s known as nominal subtyping. This means that for a class called Foo to extend a class called Bar, you need to see Foo extends Bar in your code. The relationship is explicit and based upon the name of the class. This applies equally to interfaces as well as classes. In our worked example, we have code like Example 2-9 in order to let our compiler know what the relationship is between classes.

Example 2-9. Parsing the headings out of a file
    class AssemblyWorker implements Worker
    class Manager implements Worker
    class Robot implements Worker
    

When the compiler comes to check whether a parameter argument is type checked, it can identify the parent type, Worker, and check based upon these explicit named relationships. This is shown in Example 2-10.

Example 2-10. Parsing the headings out of a file
    public void addWorker(Worker worker) {
        workers.add(worker);
    }

    public static AssemblyLine newLine() {
        AssemblyLine line = new AssemblyLine();
        line.addWorker(new Manager());
        line.addWorker(new AssemblyWorker());
        line.addWorker(new Robot());
        return line;
    }
    

The alternative approach is called structural subtyping, and here the relationship is implicit between types based on the shape/structure of the type. So if you call a method called getFoo() on a variable, then that variable just needs a getFoo method; it doesn’t need to implement an interface or extend another class. You see this a lot in functional programming languages and also in systems like the C++ template framework. The duck typing in languages like Ruby and Python is a dynamically typed variant of structural subtyping.

If we think about this hypothetical example in a language which uses structural subtyping, then our example might be re-written like Example 2-11. The key is that the parameter worker has no explicit type, and the StructuralWorker implementation doesn’t need to say explicitly that it implements or extends anything.

Example 2-11. Parsing the headings out of a file
    class StructuralWorker {
        def work(step:ProductionStep) {
            println("I'm working on: " + step.getName)
        }
    }

    def addWorker(worker) {
      workers += worker
    }

    def newLine() = {
        val line = new AssemblyLine
        line.addWorker(new Manager())
        line.addWorker(new StructuralWorker())
        line.addWorker(new Robot())
        line
    }
    

Structural subtyping removes the need for the interface-segregation principle, since it removes the explicit nature of these interfaces. A minimal interface is automatically inferred by the compiler from the use of these parameters.

The Dependency-Inversion Principle

Abstractions should not depend on details; details should depend on abstractions.

On of the ways in which we can make rigid and fragile programs that are resistant to change is by coupling high-level business logic and low-level code designed to glue modules together. This is because these are two different concerns that may change over time.

The goal of the dependency-inversion principle is to allow programmers to write high-level business logic that is independent of low-level glue code. This allows us to reuse the high-level code in a way that is abstract of the details upon which it depends. This modularity and reuse goes both ways: we can substitute in different details in order to reuse the high-level code, and we can reuse the implementation details by layering alternative business logic on top.

Let’s look at a concrete example of how the dependency-inversion principle is traditionally used by thinking through the high-level decomposition involved in implementing an application that builds up an address book automatically. Our application takes in a sequence of electronic business cards as input and accumulates our address book in some storage mechanism.

It’s fairly obvious that we can separate this code into three basic modules:

  • The business card reader that understands an electronic business card format
  • The address book storage that stores data into a text file
  • The accumulation module that takes useful information from the business cards and puts it into the address book

We can visualize the relationship between these modules as shown in Figure 2-1.

Dependencies
Figure 2-1. Dependencies

In this system, while reuse of the accumulation model is more complex, the business card reader and the address book storage do not depend on any other components. We can therefore easily reuse them in another system. We can also change them; for example, we might want to use a different reader, such as reading from people’s Twitter profiles; or we might want to store our address book in something other than a text file, such as a database.

In order to give ourselves the flexibility to change these components within our system, we need to ensure that the implementation of our accumulation module doesn’t depend upon the specific details of either the business card reader or the address book storage. So, we introduce an abstraction for reading information and an abstraction for writing information. The implementation of our accumulation module depends upon these abstractions. We can pass in the specific details of these implementations at runtime. This is the dependency-inversion principle at work.

In the context of lambda expressions, many of the higher-order functions that we’ve encountered enable a dependency inversion. A function such as map allows us to reuse code for the general concept of transforming a stream of values between different specific transformations. The map function doesn’t depend upon the details of any of these specific transformations, but upon an abstraction. In this case, the abstraction is the functional interface Function.

A more complex example of dependency inversion is resource management. Obviously, there are lots of resources that can be managed, such as database connections, thread pools, files, and network connections. I’ll use files as an example because they are a relatively simple resource, but the principle can easily be applied to more complex resources within your application.

Let’s look at some code that extracts headings from a hypothetical markup language where each heading is designated by being suffixed with a colon (:). Our method is going to extract the headings from a file by reading the file, looking at each of the lines in turn, filtering out the headings, and then closing the file. We shall also wrap any Exception related to the file I/O into a friendly domain exception called a HeadingLookupException. The code looks like Example 2-12.

Example 2-12. Parsing the headings out of a file
public List<String> findHeadings(Reader input) {
    try (BufferedReader reader = new BufferedReader(input)) {
        return reader.lines()
                     .filter(line -> line.endsWith(":"))
                     .map(line -> line.substring(0, line.length() - 1))
                     .collect(toList());
    } catch (IOException e) {
        throw new HeadingLookupException(e);
    }
}

Unfortunately, our heading-finding code is coupled with the resource-management and file-handling code. What we really want to do is write some code that finds the headings and delegates the details of a file to another method. We can use a Stream<String> as the abstraction we want to depend upon rather than a file. A Stream is much safer and less open to abuse. We also want to be able to a pass in a function that creates our domain exception if there’s a problem with the file. This approach, shown in Example 2-13, allows us to segregate the domain-level error handling from the resource-management-level error handling.

Example 2-13. The domain logic with file handling split out
public List<String> findHeadings(Reader input) {
  return withLinesOf(
    input, 
    lines -> lines.filter(line -> line.endsWith(":"))
                  .map(line -> line.substring(0, line.length()-1))
                  .collect(toList()),
    HeadingLookupException::new);
}
    

I expect that you’re now wondering what that withLinesOf method looks like! It’s shown in Example 2-14.

Example 2-14. The definition of withLinesOf
private <T> T withLinesOf(
   Reader input,
   Function<Stream<String>, T> handler,
   Function<IOException, RuntimeException> error) {
   
   try (BufferedReader reader = new BufferedReader(input)) {
       return handler.apply(reader.lines());
   } catch (IOException e) {
       throw error.apply(e);
    }
}
    

withLinesOf takes in a reader that handles the underlying file I/O. This is wrapped up in BufferedReader, which lets us read the file line by line. The handler function represents the body of whatever code we want to use with this function. It takes the Stream of the file’s lines as its argument. We also take another handler called error that gets called when there’s an exception in the I/O code. This constructs whatever domain exception we want. This exception then gets thrown in the event of a problem.

To summarize, higher-order functions provide an inversion of control, which is a form of dependency-inversion. We can easily use them with lambda expressions. The other observation to note with the dependency-inversion principle is that the abstraction that we depend upon doesn’t have to be an interface. Here we’ve relied upon the existing Stream as an abstraction over raw reader and file handling. This approach also fits into the way that resource management is performed in functional languages—usually a higher-order function manages the resource and takes a callback function that is applied to an open resource, which is closed afterward. In fact, if lambda expressions had been available at the time, it’s arguable that the try-with-resources feature of Java 7 could have been implemented with a single library function.

Summary

We’ve now reached the end of this section on SOLID, but I think it’s worth going over a brief recap of the relationships that we’ve exposed. We talked about how the single-responsibility principle means that classes should only have a single reason to change. We’ve talked about how we can use functional-programming ideas to achieve that end, for example, by decoupling the threading model from application logic. The open/closed principle is normally interpreted as a call to use polymorphism to allow classes to be written in a more flexible way. We’ve talked about how immutability and higher-order functions are both functional programming techniques which exhibit this same open/closed dynamic.

The Liskov substitution principle imposes a set of constraints around subclassing that defines what it means to implement a correct subclass. In functional programming, we de-emphasize inheritance in our programming style. No inheritance, no problem! The interface-segregation principle encourages us to minimize the dependency on large interfaces that have multiple responsibilities. By moving to functional languages that encourage structural subtyping, we remove the need to declare these interfaces.

Finally we talked about how higher-order functions were really a form of dependency inversion. In all cases, the SOLID principles offer us a way of writing effective object-oriented programs, but we can also think in functional terms and see what the equivalent approach would be in that style.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset