Inside the Object-Oriented Toolbox — Avoiding bad design with SOLID Principles

Wimal Perera
9 min readJul 12, 2020

--

Although “Object-Oriented (OO) Programming” is used to achieve reusable, extendable, and maintainable software systems; some of the program logic created based on “OO Design” often end up as highly coupled, monolithic software implementations. While investigating the key reasons behind this dilemma, it can be often observed that “bad OO implementations” (i.e. OO program logic) emerge as a result of “misusing core OO concepts” (i.e. “Abstraction”, “Encapsulation”, “Inheritance” and “Polymorphism”) during software design.

Hence OO Theory is extended with “SOLID principles”, by providing a set of guidelines in order “to avoid a bad OO Design”.

  • Single Responsibility Principle (SRP)
  • Open Close Principle (OCP)
  • Liskov’s Substitution Principle (LSP)
  • Interface Segregation Principle (ISP)
  • Dependency Inversion Principle (DIP)

The main focus of this article is to explain the practical usage of each SOLID principle, by providing a “bad OO design example” followed by its “rectified OO design” (“Java” is used as the implementation language for all the provided code examples).

Please note that before reading the rest of this article, it is not mandatory, yet recommended to of have skimmed through the article; “Inside the Object-Oriented Toolbox — Mapping between Design & Implementation”, which provides an in-depth explanation of how you would map an “OO Design” presented in a “UML class diagram” to “Java” implementation code.

Single Responsibility Principle (SRP)

SRP states that, “a class should have ONLY one reason to change”.

Consider the design provided in Figure 1 and its corresponding implementation in Figure 2.

Figure 1 — Email: Design Example before applying SRP

By looking at the design and implementation in Figure 1 and Figure 2, we can observe that the “Email” class has at least 2 reasons to change (in other words the “Email” class handles 2 responsibilities).

  • Assembling an email by combining its meta data (sender, receiver etc.) and the body content
  • Parsing/Formatting email body content (html, richText etc.)
Figure 2 — Email: Java Code before applying SRP

Below are some of the side effects that could occur due to maintaining 2 “responsibilities” (i.e. 2 “reasons to change”) within the “Email” class.

  • We need to change the implementation inside the “Email” class, when introducing a new content type (like xml, excel etc.)
  • What if there were multiple implementations of “IEmail” interface (Outlook, Gmail etc.) and a new content type has to be introduced? Are we going to change all implementations of “IEmail”?​
Figure 3 — Email: Design Example after applying SRP

By adhering with SRP we split the 2 responsibilities of the “Email” class from previous design (Figure 1 and Figure 2), by creating 2 different classes in the new design (Figure 3 and Figure 4).

  • “Email” — Assembles the email with its meta data and body content
  • “HtmlContent” — Parses/Formats html based email content
Figure 4 — Email: Java Code after applying SRP

Note that since we have a separate interface “IContent” (see Figure 3) to decouple the content type from the “IEmail” and “Email”; the implementation code in the “Email” class can evolve independently irrespective of the content type.

Open Close Principle (OCP)

OCP states that, “software entities like classes, modules and functions should be open for extension but closed for modifications.​

Consider the below design and implementation (Figure 5 and Figure 6) for an “area calculator” which calculates the area of a circle and a rectangle.

Figure 5 — Area Calculator: Design Example before applying OCP

By looking at the design and implementation in Figure 5 and Figure 6; what would happen to code inside the “AreaCalculator” class (see Figure 6), if you introduced another new shape (such as ellipse)? Obviously the code inside the “AreaCalculator” class (see Figure 6) has to be modified with another “conditional if statement”.

Figure 6 — Area Calculator: Java Code before applying OCP

Thus the “AreaCalculator” violates OCP since its implementation is closed for extensions without being modified upon introducing new shapes.

One possible solution to fix the “AreaCalculator” class by applying OCP, is to introduce a new abstract class “Shape” and decouple the “AreaCalculator” with the “Circle” and the “Rectangle” (see Figure 7).

Figure 7 — Area Calculator: Design Example after applying OCP

As illustrated in Figure 8, the new implementation adhering to the OCP based design (in Figure 7) allows extensions since we can introduce more and more different shapes (like ellipse) without modifying the code inside the “AreaCalculator” class.

Figure 8 — Area Calculator: Java Code after applying OCP

Liskov’s Substitution Principle (LSP)

LSP states that, “derived types must be completely substitutable for their base types”.

Consider the initial design for a “FlightSimulator” (Figure 9) illustrated below.

Figure 9 — Flight Simulator: Design Example before applying LSP

What if an “Ostrich” is passed to the list of “Birds” in the code inside the “FlightSimulator” class (see Figure 10)? “flyAllBirds()” method is going to crash since an “Ostrich” can’t fly. The reason behind this code failure is that the “Bird” type is not completely substitutable for the code inside the “FlightSimulator”, since there are “Birds” like “Ostriches” that can’t fly.

Figure 10 — Flight Simulator: Java Code before applying LSP

We can fix the “flyAllBirds()” issue (in Figure 9 and Figure 10) by slightly modifying our “FlightSimulator” design (see Figure 11), while introducing the completely substitutable “FlightBird” class into the middle of the class hierarchy (i.e. by applying LSP).

Figure 11 — Flight Simulator: Design Example after applying LSP

Hence, introducing the new “FlightBird” type to the input list of “flyAllBirds()” method in the “FlightSimulator” class fixes the design issue (Figure 12) since “Ostriches” are “NonFlightBirds”.

Figure 12 — Flight Simulator: Java Code after applying LSP

Interface Segregation Principle (ISP)

ISP states that, “clients should NOT be forced to depend upon interfaces that they DON’T use”.

Figure 13 — Communication System: Design Example before applying ISP

Consider the initial design of a communication system illustrated above (Figure 13).

Figure 14 — Communication System: Java Code before applying ISP

As per the design in Figure 13, although we have selected “IContact” as the common interface type to represent all contacts in the system against the contact name; by looking at the code in Figure 14 (written based on the design in Figure 13) we can note the below issues:

  • The “PhoneContact” class does not require methods other than “getName()” and “getTelephone()”. But it has to leave other method stubs coming from the “IContact” interface as dummy empty implementations.
  • Although the “Dialler” class does not require methods other than “getName()” and “getTelephone()”, it has access to unwanted methods such as “getEmailAddress()”; since “IContact” is the parameter type representing contact details in the “makeCall()” method.
  • The “EmailContact” class does not require methods other than “getName()” and “getEmailAddress()”. But it has to leave other method stubs coming from the “IContact” interface as dummy empty implementations.
  • Although the “Emailer” class does not require methods other than “getName()” and “getEmailAddress()”, it has access to unwanted methods such as “getTelephone()”; since “IContact” is the parameter type representing contact details in the “sendMessage()” method.

The main reason behind all the above 4 problems is due to “IContact” being a “fatty” interface, containing a method set in which most of its implementation classes (such as “EmailContact” and “PhoneContact”) require only a subset of the total method set. Thus the classes, “EmailContact” and “PhoneContact” are forced to depend upon the “fatty” “IContact” interface containing certain methods that they don’t use. Therefore the “IContact”-based design violates ISP.

Figure 15 — Communication System: Design Example after applying ISP

The solution to avoid the “IContact” fatty interface, is to introduce the new “IDiallable”, “IEmailable” and “IPostable” interface types, each containing a subset of methods from “IContact” (see Figure 15).

Figure 16 — Communication System: Java Code after applying ISP

As illustrated in Figure 16 the implementation code based on the new ISP-based design has the characteristics described below.

  • Unlike in Figure 14, the “PhoneContact” class does not have any unwanted empty method stubs since it implements the non-fatty “IDiallable” interface type.
  • Unlike in Figure 14, the “Dialler” class now has access to its essential methods only, since “IDiallable” is the parameter type representing contact details in the “makeCall()” method.
  • Unlike in Figure 14, the “EmailContact” class does not have any unwanted empty method stubs since it implements the non-fatty “IEmailable” interface type.
  • Unlike in Figure 14, the “Emailer” class now has access to its essential methods only, since “IEmailable” is the parameter type representing contact details in the “sendMessage()” method.
Figure 17 — Communication System: Versatile Contacts after applying ISP

Considering a single class can implement multiple interfaces, the new ISP-based design (described in Figure 15 and Figure 16), allows us to implement versatile contacts having phone, address and email altogether (see “CompleteContact” class in Figure 17) that can be reused across “Dialler” and “Emailer” implementations.

Dependency Inversion Principle (DIP)

DIP states that,

  • “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”.​
Figure 18 — Bank Money Transfer: Design Example before applying DIP

Figure 18 and Figure 19, illustrates an initial design and corresponding implementation for transferring money between 2 bank accounts. “TransferManager” is the “high-level module” and “BankAccount” is the “low-level module”; since “TransferManager” accesses the methods of “BankAccount” within its code. An implementation change of the “BankAccount” class will directly affect the “TransferManager” class. In other words the “high-level module” “TransferManager”, significantly depends on the “low-level module” “BankAccount”, which is a violation of DIP.

Figure 19 — Bank Money Transfer: Java Code before applying DIP

The “DIP-based solution” to remove the tight coupling between “TransferManager” and “BankAccount” classes is to introduce 2 “abstractions” (i.e. java interfaces), “ITransferSource” and “ITransferDestination” (see Figure 20 and Figure 21).

Figure 20 — Bank Money Transfer: Design Example after applying DIP

Since both “TransferManager” and “BankAccount” modules now depend only on the 2 abstractions “ITransferSource” and “ITransferDestination”; the implementations of “TransferManager” and “BankAccount” classes can now freely evolve independently from each other.

Figure 21 — Bank Money Transfer: Java Code after applying DIP

Further as illustrated in Figure 20 and Figure 21, in the “DIP-based design” the “abstractions” (i.e. “ITransferSource” and “ITransferDestination” interface types) are independent from “details” (i.e. “TransferManager” and “BankAccount” classes) whereas the “detail” classes depend on “abstractions”; since “ITransferSource” and “ITransferDestination” are used within the implementation code of “TransferManager” and “BankAccount” classes.

It is also worth to note that the “Dependency Injection Pattern” (initially introduced by the “Spring Framework” and also abbreviated as “DIP”) is a practical application of the “Dependency Inversion Principle”.

Conclusion

Congratulations! you’re up to the final section of this “SOLID Principles” article. Having reached this point, it is worth to discuss about the high level relationship between “OOP Concepts” (“Abstraction”, “Encapsulation”, “Inheritance” and “Polymorphism”), “SOLID Principles” and “OO Design Patterns” (23 design patterns introduced by the “Gang-Of-Four”) within the Object-Oriented literature (see Figure 22).

Figure 22 — OOP Overview

As illustrated in Figure 22,

  • “OO Concepts” focus on the theoretical aspect within the spectrum of OO literature.
  • “SOLID Principles” (inspired from the basic 4 “OO Concepts”; “Abstraction”, “Encapsulation”, “Inheritance” and “Polymorphism”) are both into theory and practice, instructing how to use the “OO Concepts” properly and avoid a bad OO design.
  • “Gang-Of-Four” (GoF) “OO Design Patterns” (inspired by both “OO Concepts” and “SOLID Principles”) are at the practical end of the OO literature spectrum.

Having read the entirety of this blog, you’re now in a much better position to apply the basic “OO Concepts” and understanding “OO Design Patterns” in true depth.

Interested in taking that initial step in improving your business by designing and developing a value-driven IT system? Contact Us.

--

--

Wimal Perera

A Software Engineer with 12+ years of development experience; from frontend web to backend IT infrastructure. (https://www.linkedin.com/in/wimalperera/)