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.