SOLID Design Principles

LiveRunGrow
10 min readJun 16, 2021

--

Written with reference to Clean Architecture: A Craftsman’s Guide to Software Structure and Design and of course, The Internet.

Photo taken in Daxing Airport, Beijing China (2019)

Clean code is the foundation of good software. We can imagine code as bricks and software architecture as the architecture of the building we are trying to construct. The architecture of the building isn’t important if the bricks aren’t well made. On the other hand, even if you are given well made bricks, you can still end up with a messy structure. This is then where the SOLID Principles come in.

The SOLID Principles teaches us to organise our functions and data structures into classes (coupled groupings of functions and data) as well as how to connect these classes.

In short, SOLID is ….

Single Responsibility Principle. Each software module has one, and only one, reason to change.

Open Closed Principle. Software systems must be designed such that they allow for the behaviour of those systems to be changed by adding new code rather than changing existing code.

Liskov Substitution Principle. In order to build software systems from interchangeable parts, these parts must adhere to a contract that allows those parts to be substituted one for another.

Interface Segregation Principle. Splits unnecessarily large interfaces into smaller and more specific ones so that clients will only have to know about the methods that are of interest to them and avoid depending on things they don’t use. Avoid God Classes.

Dependency Inversion Principle. The code that implements high level policy should not depend on the code that implements low level details. Details should depend on Policy. Depend on abstractions, not on concretions.

Single Responsibility Principle …

A module should have one, and only one, reason to change.

A common reason for software to change is due to a change in requirements either from users or stakeholders. Hence, in another way of phrasing, a module should be responsible to one, and only one actor.

To understand it better, lets consider the following situation where the SRP has been violated.

Imagine we have an Employee class from a payroll application with 3 methods: calculatePay(), reportHours(), and save().

Following our definition, we can see that the above class violates the SRP. This is because the three methods placed within it are responsible for 3 different actors, which are the CFO, COO and CTO respectively.

  • calculatePay(): Specified by the accounting department which reports to the CFO.
  • reportHours(): Specified and used by the human resources department, which reports to the COO.
  • save(): Specified by the database administrators who report to the CTO.

You might think, why might this be an issue?

There may be a scenario whereby the different methods, A and B, share a common underlying algorithm. The developer might then create another private method C to house this common algorithm and C will be used by A and B. Suppose that one of the client, perhaps the CFO, decides to modify the algorithm used in method A. To accommodate the change in the algorithm, the developer will then have to modify C. While everything works well for the CFO after the intended change, the CTO which uses method B might be unaware of the changes made to C. Hence, a problem arises.

Furthermore, if we have a class being used by different actors, then at any point where the two different actors have to make changes to the class, there will be a need for extra coordination between the two actors to ensure that any of the changes made by one of them do not affect the other.

Solution:

There are more than 1 solution to this problem.

One of the solution is to separate the data from the functions. We will construct a class for each of the three actors to contain the source code for their respective functions. These three classes will then share access to the data structure class, EmployeeData, which does not contain any methods.

The downside is that there will now be 3 classes for developers to instantiate. But, fret not! We can make use of the Facade pattern to create a single Facade class for all the actors to interact. According to the method called, this Facade class will then instantiate and delegate work to the respective actual classes containing the required functions.

Open-Closed Principle …

A software artifact should be open for extension but closed for modification.

Consider this….

Lets say we have a system to display financial records summary. Negative numbers are rendered in red and the page is scrollable.

An addition in requirement is requested whereby the same information is to be printable in black and white. The pages should have pagination with the appropriate page headers, footers and labels. Negative numbers should be surrounded by parentheses.

Due to this requirement addition, we will need to make changes to the code. Ideally we want to have 0 change.

To do so, we will need to separate the things that change for different reasons (SRP), then organise the dependencies between those things properly (Dependency Inversion Principle).

After we apply the SRP, we may come up with the following data flow. We have some procedure responsible for analysing the financial data and producing reportable data, which is then formatted by the two different reporter processes.

The key point to note here is that generating the report involves two separate responsibilities: calculation of the reported data, and the presentation of the data into web and printer friendly format.

Hence, we need to organise the code dependencies to ensure that changes to one of those responsibilities do not cause changes in the other. The new organisation should also make sure that the behaviour can be extended without modification of the existing code.

We do so by separating the processes into classes, and separating those classes into components.

Classes marked with <I> are interfaces; those marked with <DS> are data structure. Open arrow head indicate composition relationship while close arrowhead are implements or inheritance relationships.

Here we have the components: Controller, Interactor, Database, Presenter and Views.

An arrow pointing from class A to class B means that the source code of A mentions B but B does not know A.

The arrows points towards components that we want to protect from change. If A should be protected from changes in B, then B should depend on A. B should point to A. We want to protect the Controller from changes in the Presenters. We want to protect the Presenters from changes in the Views. We want to protect the Interactor from changes in everything.

Why is the Interactor protected from everything? It is because it contains the business rules and hence has the highest level policies of the application. On the other hand, Views are the lowest level concept and are the least protected.

This is how the OCP works at the architectural level. Architects separate functionality based on how, why, and when it changes, and then organise that separated functionality into a hierarchy of components. High level components are protected from changes in lower level components.

Another good article:

Liskov Substitution Principle …

Functions that are written to expect base classes must be able to use objects of derived classes without knowing it.

At the core, we can use LSP to help us in deciding between using inheritance or composition as our solution.

To achieve this, the following is what we need to ensure that we

  • Implement no more stringent restrictions on input parameters than the parent class.
  • Have a use case for every method present in the parent base class.
  • At the very least, apply the same rules to all output parameters as the parent class.

Consider …

the following example code:

public class Bird{
public void fly(){}
}
public class Duck extends Bird{}

The duck can fly because it is a bird. However, what if we have another type of bird, the Ostrich, which is not supposed to have the flying capability:

public class Ostrich extends Bird{}

This means we are breaking the LSP principle.

In this case, we are better off using object composition instead of object inheritance. To elaborate briefly how we could go about using composition, we could construct a separate class to hold the implementation detail for the flying behaviour. To instantiate a new Duck instance, we will then initialise it with the flying behaviour object where the flying behaviour object is held as an instance variable within the Duck class. In other words, we could include a behavior setter methods in the Duck class so that we can even modify and set the Duck class to contain an alternative flying behaviour at run time.

Interface Segregation Principle …

Depending on something that carries other methods that you do not use or need might cause more troubles.

Lets say we have 3 users who uses the OPS class. User1 uses op1 method, User2 uses op2 method and User3 uses op3 method. In this case, the source code of User1 will inadvertently depend on op2 and op3, even though it doesn’t call them. For eg, a change to the code of op2 will force User1 to be recompiled and redeployed even though nothing that it cared about has actually changed.

This problem could be resolved by segregating the operations into interfaces.

Open arrow -> Composition

U1Ops one = new OPS();

In general, it is harmful to depend on modules that contain more than you need.

Another explanation from Wikipedia:

The design problem was that a single Job class was used by almost all of the tasks. Whenever a print job or a stapling job needed to be performed, a call was made to the Job class. This resulted in a ‘fat’ class with multitudes of methods specific to a variety of different clients. Because of this design, a staple job would know about all the methods of the print job, even though there was no use for them.

The solution suggested is what is today called the Interface Segregation Principle. An interface layer between the Job class and its clients was added using the Dependency Inversion Principle. Instead of having one large Job class, a Staple Job interface or a Print Job interface was created that would be used by the Staple or Print classes, respectively, calling methods of the Job class. Therefore, one interface was created for each job type, which was all implemented by the Job class.

Avoid God Classes!

Differences between ISP and SRP

A “client”, as intended by Gang of Four definitions, is a class that implements an interface.

As quoted from the Stack Overflow answer above …

To be fair, they are both different views on the same idea — SRP is more focused on the designer-side point-of-view, while ISP is more focused on the client-side point-of-view.

ISP can be seen as similar to SRP for interfaces; but it is more than that. ISP generalizes into: “Don’t depend on more than you need.” SRP generalizes to “Gather together things that change for the same reasons and at the same times.”

Imagine a stack class with both push and pop. Imagine a client that only pushes. If that client depends upon the stack interface, it depends upon pop, which it does not need. SRP would not separate push from pop; ISP would.

Dependency Inversion Principle …

The most flexible systems are those in which source code dependencies refer only to abstractions, not to concretions. Although exceptions can be made to classes that we know to be stable. For example, the Java String class.

If we change an abstract interface, we will need to make a change to its concrete implementations. Conversely, changes to concrete implementations of a class do not necessarily require a change to its corresponding interface. For this reason, interfaces are often kept more stable than implementations and engineers often avoid changing them.

Rules

  • Refer to abstract interface.
  • Do not inherit from volatile concrete classes. Use inheritance with care.
  • Do not override concrete functions.

Inevitably, we have to initialise concrete classes. We cannot avoid this! We usually use factories to create objects since the creation of an object with the new keyword is a source code dependency on the concrete definition of the object.

The curved line separates the abstract from the concrete.

The abstract components -> high level business rules. The concrete components (bottom half) has all the implementation details.

The concrete component contains a single dependency, so it violates the DIP. This is ok. DIP cannot be entirely removed, but can be gathered into a small number of concrete components and kept separate from the rest of the system.

That’s all for the SOLID Principles. Bye bye, the end of this post! :)

An Image from Pinterest

--

--

LiveRunGrow

𓆉︎ 𝙳𝚛𝚎𝚊𝚖𝚎𝚛 🪴𝙲𝚛𝚎𝚊𝚝𝚘𝚛 👩‍💻𝚂𝚘𝚏𝚝𝚠𝚊𝚛𝚎 𝚎𝚗𝚐𝚒𝚗𝚎𝚎𝚛 ☻ I write & reflect weekly about software engineering, my life and books. Ŧ๏ɭɭ๏ฬ ๓є!