SOLID Part Three. The Interface Segregation and Dependency Inversion Principles.

Interface Segregation Principle

This is defined as clients should not be forced to depend upon interfaces that they do not use in the book Clean Architecture. The reason for this is because when a class depends on an interface it doesn’t use but that another class does the two classes become inadvertently coupled to each other. This is because changes forced upon the interface’s implementing class by each of the client classes that use the interface affect the other client classes.

We can end up in this situation when two or more “client” classes depend on an interface which manipulates the same data but in different ways. The client classes could be inheriting from a class or simply using an object of the class as a property. Changes to the interface will force change upon all of the “client” classes. Thus it will force recompilation at the very minimum and perhaps a redeploy. The client classes are all tightly coupled to each other. The answer to this problem is for each class to have it’s own interface in order to make the classes more loosely coupled and thus easier to refactor.

When Robert C. Martin first introduced the ISP he used the example of an ATM system as an example of applying the ISP. The ATM example describes a class hierarchy where deposit, withdrawal and transfer classes depend upon a single UI interface. Thus each of the different transaction classes inadvertently become coupled to each other when in reality they have nothing to do with each other. This is mitigated by applying the ISP and creating WithdrawlUI, TransferUI and DepositUI interfaces and each of the respective classes hold a reference to their own UI interfaces.

In the end the ISP is about avoiding depending on what you don’t need in order to avoid coupling to other unrelated components in the system. When coupling is prevented the software system becomes more flexible and therefore more resistant to changes in requirements which are more or less inevitable.

Dependency Inversion Principle

The dependency inversion principle (DIP) is something that I’m sure most software developers make use of on a daily basis, perhaps unknowingly. The idea behind the DIP is that software becomes more flexible when it depends on abstractions rather than on concrete implementations. The idea of inversion comes from thinking about the direction of source code dependencies. Consider the following contrived example:

type BusinessRules struct {
    db Database
}

func (b BusinessRules) ProcessMessage(m Message) error {
    // Some fake business rules here which creates var record
    if (condition) {
        b.db.AddRecord(record)
    } else {
        d.db.DeleteRecord(record)
    }
}


//Here starts the database package
type Record struct { ... }

type Database struct {
    db sql.DB
}

func (d Database) AddRecord(record Record) error {
    // SQL stuff
}

func (d Database) AddRecord(record Record) error {
    // SQL stuff
}

In this example the higher-level type BusinessRules is depending on low-level details of the Database package, including both the Database and Record types. We can invert this dependency by creating an interface called Store in the BusinessRules package which the Database type implements. This includes defining the Record type which should ideally be a plain old data (POD) type in the BusinessRules package. This means we have inverted the direction of source code dependency from the lower level package to the higher-level package. The Store and Record types may also be located in their own package. We end up with something like the following:

// Business Rules package

type Record struct{ ... }

type store interface {
    AddRecord(record Record) error
    DeleteRecord(record Record) error
}

type BusinessRules struct {
    store store
}

func NewBusinessRules(store store) BusinessRules {
    return BusinessRules{
        store: store,
    }
}

func (b BusinessRules) ProcessMessage(m Message) error {
    // Some fake business rules here which creates var record
    if (condition) {
        b.store.AddRecord(record)
    } else {
        d.store.DeleteRecord(record)
    }
}


// Data package
type Database struct {
    db sql.DB
}

func (d Database) AddRecord(record Record) error {
    // SQL stuff
}

func (d Database) AddRecord(record Record) error {
    // SQL stuff
}

I also added a constructor for BusinessRules which takes a store as a parameter, this technique is known as dependency injection. We could also have made use of a factory here. This is required since we want our higher-level module to depend solely on an abstraction for the lower level detail and therefore the higher-level module should be importing nothing from the lower level “details” module.

As well as making software more flexible to change in details such as transport mechanism or storage mechanism, applying this principle also makes software much more easily testable. This is because we are relying on abstractions which pass around plain old data types which means we can easily create mocks for our abstractions. Mocks can be created manually or created by a generation tool such as “mockify” for Go. This allows thorough unit testing of the business rules without having to worry about external dependencies.

To summarise, the dependency inversion principle encourages developers to create source code that depends on abstractions rather than concrete implementations. There are places where it is hard to avoid such as using the standard library of your given programming language and it’s not likely there is much to gain from abstracting away the standard library though there may be some cases where there is something to gain. When dependencies are inverted we create a more pluggable architecture where lower-level details such as HTTPS vs GRPC or filesystem vs SQL become unimportant to the higher-level business rules. This in turn makes our business rules more flexible and testable as they are not entangled with lower-level details.

Written on September 30, 2019