SOLID Principles: Dependency Inversion

The Dependency Inversion Principle “D”

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.

Overview

The word inversion is a misfit in the 2020s but it has some history around it. Most software were developed using procedural methodologies in the 80s and 90s; in this approach high level business functionalities are broken into low level reusable functions. Although this approach creates good reusable low level functions but it makes high level business functions to require rework when any change is done in low level functions. The term dependency ‘inversion’ was used in the late 90s to say that low level modules should depend on high level modules (main business function) via abstraction in Object Oriented Analysis & Design.

Thus, inverting the way dependencies should be defined is called Dependency Inversion.

Layered Design

A good software architecture defines different layers with specific responsibilities. The bottom layers are more generic & reusable and functionalities tend to become more specific to business use cases as we move up the layers. E.g.

Figure-1 Layered Design
  • The lowest layer contains Infrastructure or Utilities like file handling, DB repo, security.
  • The middle layer contains reusable business services or models. The functionalities defined here are business functions which are independent of business use cases.
  • The top layer contains business use cases which are orchestrated using the middle layer. These are the functionalities understood by users of the application.

The different layers are defined in different classes, packages/namespaces but they are still tightly coupled. To reduce the coupling and enable higher reuse of Business Services, Different layers can invoke the concrete classes via abstraction, like in the Figure-2:

Figure-2 Layered Design

Conclusion

Defining module dependencies via Abstraction helps in maintaining low coupling while keeping the code reusable.

Proper implementation of Dependency Inversion principle requires a broad understanding of the Domain for which application is being developed. The key decision is to segregate functionality between middle layer (reusable business services) and top layer (business user cases).

SOLID Principles: Interface Segregation

Interface segregation is another technique that helps in keeping the system loosely coupled by dividing the functionality into small components.

The Interface Segregation Principle “I”

It states that no client should be forced to depend on methods it does not use. In other words, only relevant methods and functionalities should be exposed to clients and clients should have ability to pick and choose what’s relevant to them.

The problem with creating large Interfaces and hence heavy classes is that every client may not require all the functionality offered by the large Interface but they end up including things they don’t use. This leads to a very common issue in the Software Life-cycle where things break as a side-effect of some other change. Unexpected errors may occur for a client when there are changes in certain areas of the underlying application (which are not even being used by the client).

Interface Segregation can be considered an extension of Single Responsibility Principle which helps in segregating related but different functionalities.

Consider the example of a generalized E-Commerce platform which can be utilized for sale & delivery of physical goods and electronic subscription.

Classes which handle Delivery can be defined like this:

Figure 1

Some of OrderDelivery methods like assignPostalServiceProvider() may not be applicable for ElectronicDelivery. Clients that only require ElectronicDelivery will unnecessarily get the APIs specific to Physical Delivery.

class ElectronicDelivery implements OrderDelivery {

  @Override
  public void assignPostalServiceProvider(String provider) {
    //Do nothing
  }
}

Any changes related to Physical Delivery in the OrderDelivery interface, would require updates in the ElectronicDelivery concrete implementation too. It is useless but necessary change that can be avoided with proper segregation of Interfaces like:

Figure 2

The way classes are defined in Figure 2 is much cleaner and each class contains only relevant behavior.

Conclusion

When each class contains only relevant functionality, then client applications are never impacted by unused dependencies. Interface Segregation helps in reducing side-effect bugs during the maintenance of Application.

SOLID Principles: Behavioral Subtyping

Also known as Liskov substitution principle, it states that a derived entity should be behavioral extension of the base entity and not just syntactical extension.

The Liskov Substitution Principle “L”

This is an often overlooked principle while defining class hierarchies. Base & derived classes should have behavioral relation between them and Inheritance should not be applied just for the sake of reusing code.

As per LSP, objects of Base class should be replaceable with objects of Derived class without any change in the behavior or correctness of the application.

Inheritance signifies an Is-A relation between the derived class and the base class but at times, it can be misleading. Couple of classic examples:

  1. Square extends Rectangle: In basic geometric terms a square is a specific form of rectangle, so it might seem right for Square class to derive from Rectangle.
    • In object oriented design terms, a Rectangle has 2 sides but a Square has only 1.
    • When Square class extends Rectangle, it gets more variables than it needs and thus would need to do some work-around to achieve true behavior of a single side square.
    • Consider the following code snippet defying Behavioral Subtyping. setHeight() of Rectangle changes the value of variable height but setHeight() of Square changes both height & width. If clients replaces a Rectangle object with a Square object, then they will observe (unexpectedly) that setHeight() changes both height & width.
public class Rectangle {

  protected int height;
  protected int width;

  public int getHeight() {
    return height;
  }

  public void setHeight(int height) {
    this.height = height;
  }

  public int getWidth() {
    return width;
  }

  public void setWidth(int width) {
    this.width = width;
  }
}

public class Square extends Rectangle {

  @Override
  public int getHeight() {
    return super.getHeight();
  }

  @Override
  public void setHeight(int height) {
    super.setHeight(height);
    super.setWidth(height);
  }

  @Override
  public int getWidth() {
    return super.getWidth();
  }

  @Override
  public void setWidth(int width) {
    super.setHeight(width);
    super.setWidth(width);
  }

}
  1. Stack extends Queue: Both of these data structures have put() and get() methods. The put() method for them is same because it adds an element at the end of the data structure. The get() method is different; it implements FIFO in case of Queue and LIFO in case of Stack.
    • Defining Stack to extend Queue does provides code re usability but breaks the Liskov substitution principle because a Queue cannot be replaced with Stack without changing the underlying behavior.
    • A better class design would be something like the following. An abstract class AbstractCollection contains the common code of put() method and both Stack and Queue extend the abstract class to add concrete definition of get() method.
Figure 1

As per Wikipedia, Liskov substitution principle says “if S is a subtype of T, then objects of type T in a program may be replaced with objects of type S without altering any of the desirable properties of that program“.

In practice, applying this principle helps in deciding between Inheritance (Is-a) and Composition (Has-a) relation between classes.

Consider the following example of Rolling File Logger.

The objective is to add log messages to a file and roll the file based on the size of file. The library needs to support another functionality of rolling the log file based on time interval.

In this first approach (Figure 2) which defies Behavioral Subtyping, these two functionalities can be achieved by TimeRollingFileLogger extending SizeRollingFileLogger and overriding the method isRolloverRequired() which determines when to roll the log file.

Figure 2

A better design is to separate the Rolling Strategy from the FileLogger like:

Figure 3

In the second approach of Figure-3, FileLogger has-a RollingStrategy and Interface RollingStrategy is implemented by three different concrete classes for three specific strategies viz None, SizeBased and TimeBased.

The second approach better adheres to the behavioral relation between FileLogger and it’s rolling strategies in term of Object Oriented Design.

Conclusion

Behavioral Subtyping OR Liskov Substitution Principle helps in defining proper Object Oriented Design relation between classes by distinguishing Composition and Inheritance.

SOLID Principles: Open-Closed

The Open-Closed Principle “O”

“software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification”.

This principle advocates Inheritance and Polymorphism. The way to achieve this contradictory suggestion is to design loosely coupled classes which bind together using Interfaces or Abstraction.

Functionality can be extended by Inheriting a class OR by providing concrete implementation for an Interface or Abstract class. These can be done without changing anything in the existing classes; in fact existing classes can be in separate library.

Consider this class diagram for Logger:

Figure 1

The class LogManager is defying Open-Closed principle by containing functionalities for logToFile() and logToDB(). In future, there may be a requirement to log messages using webservice call. This new functionality would require changes in existing LogManager class; but it’s bad design if an existing class is modified to add new functionality.

A good design is to have separate classes for logging to File and DB:

Figure 2

The LogManager class doesn’t know how to write a String and it depends on derived classes for writing string. It has log(Object) method to serialize object into string and log(String) abstract method for concrete implementation:

    public void log(Object obj) {

      String message = serializer.serialize(obj);

     log(message);

    }

    public abstract void log(String msg); 

With this design, any new type of logger like web-service or message queue can be added by providing another concrete implementation of LogManager. It will not require any change in the existing classes.

Conclusion

The Open-Closed principle helps in Object Oriented class designing. It enforces Abstraction and increases code reusability.

SOLID Principles: Single Responsibility

Introduction

SOLID Design Principles are tips and tricks to organize the code in different classes which helps in keeping the code clean, maintainable and scalable (or extensible).

The way software modules & classes are designed & organized have a major impact on the people associated with it:

  1. Programmers: The application can be easy to manage and extends.
  2. Business owners: The application can be quickly enhanced to add business critical and compliance mandatory functionalities.
  3. Users: The application can provide improved experience and help achieve more.

The Single Responsibility Principle “S”

As the name suggests, a class should be responsible for one and only one thing. In words of Robert C. Martin “A class should have only one reason to change”.

Although it seems pretty obvious & it is basic Object Oriented Design, but still it gets defied many times.

Consider this example of LogManager class having a field Enum LogType with possible values of FileBased, DBBased:

Figure 1

The log() method converts Object to string and logs the string to File or DB conditionally:

    void log(Object obj) {

      String message = convertToJson(obj);

      switch(logType) {
        case FileBased:
          logToFile(message);
          break;

        case DBBased:
          logToDB(message);
          break;
      }

    }

The purpose of above code is to perform logging, it seems to be doing fine by logging into a File or DB and it includes some helpers like converting an Object to JSON string.

However, this class has two reasons to change:

  1. Changes in the way messages are logged
  2. Changes in the way String is created

In future, it may be decided to log messages as xml or csv or any other format. OR a different library may be used for converting Object to json string. But it should not impact the way strings are being persisted to storage (either File or DB).

Doing multiple things in a single class makes it tightly-coupled:

  • Some variable may be reused
  • Low level and Mid level methods may get mixed. e.g. log(String msg) should have been a low level method (only writing the string to storage) but LogManager class has log(Object obj) which is mid level method invoking convertToJson(Object).

In a class like this, it becomes very difficult to change something in isolation. A fix in one part of class impacts other parts too and might break something unintentionally.

A better design is to create a Serializer Interface for converting Object to string. Implement a JSONSerializer for json string conversion.

Use abstraction (Serializer) in the LogManager class:

Figure 2

The LogManager class doesn’t need to know how serialization is happening and the log() method should depend on Abstraction for Serialization:

    void log(Object obj) {

      String message = serializer.serialize(obj);

      switch(logType) {
        case FileBased:
          logToFile(message);
          break;

        case DBBased:
          logToDB(message);
          break;
      }

    } 

Conclusion

Single Responsibility principle helps in :

  1. Making the classes loosely-coupled
  2. Minimizing the impact of a change (bug-fix or enhancement)
  3. Making the code easy to understand