Software Design Principles are guidelines that programmers should follow while they create software, to generate clear and maintainable code. They are a collection of techniques and best practices recommended by many well-known industry experts and authors.
This blog post is the first out of three parts, in which I will go over key software engineering principles that will assist you in developing high-quality software.
The Importance of Software Design Principles
Did you know that you only spend 20% to 40% of development time actually writing code? The rest of the time, you’re reading code and maintaining the system. Therefore, it is important to create a good system design.
A good system needs a good code base that is easy to read, understand, maintain (add/change features, fix bugs), and extend in the future. This saves development time and resources while increasing work satisfaction.
Object-Oriented Design includes these five principles:
S – Single Responsibility Principle (SRP)
O – Open-Close Principle (OCP)
L – Liskov Substitution Principle (LSP)
I – Interface Segregation Principle (ISP)
D – Dependency Inversion Principle (DIP)
These five SOLID principles are frequently used as a guide when refactoring software into a better design.
Single Responsibility Principle (SRP)
Every module, class, or function in a computer program should have responsibility for a single part of that program’s functionality. Also, they should encapsulate that part, and their services should be narrowly aligned with that responsibility.
SRP is closely related to the concepts of Coupling (low) and Cohesion (high). SRP does not necessarily mean that your class should only have one method or property, but rather that the functionality should be related to a single responsibility (and have only one reason for changing).
With SRP, classes become smaller and cleaner, making them easier to maintain.
As an example, let’s take a look at a service that deals with invoicing. The source code can be found here.
As the above image shows conceptually, we are going to create an InvoiceService class with four functionalities:
- Adding invoices
- Deleting invoices
- Error Logging
- Sending emails
By putting all the four above-mentioned functionalities into a single class or module, we are violating the Single Responsibility Principle. Why? Because sending emails and logging errors are not part of the InvoiceService module.
To remove the violation of SRP, we’re going to implement three classes. Only invoice-related functionalities will be implemented in the InvoiceService class. The LoggerService class will be used exclusively for logging. Similarly, the EmailService class will take care of the functionality for sending e-mails.
In general, consider the following examples of responsibilities that may need to be separated:
- Error Handling
- Class Selection / Instantiation
Open-Close Principle (OCP)
OCP states that “software entities such as modules, classes, functions, etc. should be open for extension, but closed for modification.” In simple words, one module/class should be developed in such a way that it allows its behavior to be extended without needing to alter its source code.
How to apply OCP:
- Add the new functionalities by creating new derived classes which should be inherited from the original base class.
- Allow the client to access the original class with an abstract interface through compositional design patterns like Strategy.
So, instead of changing the existing functionality, create new derived classes and leave the original class implementation as it is.
Problems of not following OCP
If you allow a class or function to add new logic, you must test the entire functionality of the application, including both new and existing functionality. You must also inform the QA team about future changes so that they can prepare for regression testing as well as new feature testing.
For example, suppose that we have implemented a mechanism for applying a discount to the final amount value on an invoice. There are two types of discounts: one that is only applicable to final Invoices and one that applies to proposed Invoices. The OCP violation occurs when we need to add new discount type(s), and we need to change the original implementation of the Invoice class.
The source code can be found here.
Liskov Substitution Principle (LSP)
The LSP is a Substitutability principle in OOP (Object-Oriented Programming). The third SOLID principle states that if S is a subtype of T, then objects of type T should be replaced with objects of type S.
So, if we can successfully replace the object/instance of a parent class with an object/instance of the child class, without affecting the behavior of the base class instance, then we are following LSP.
When this principle is violated, it usually leads to a lot of extra conditional logic scattered throughout the application, checking to see if an object is of a specific type.
As the application grows, the duplicated and scattered code becomes a breeding ground for bugs. The partial implementation of interfaces or base class functionality, leaving unimplemented methods or properties to throw an exception, is a very common violation of this principle (for example: NotImplementedException).
In code that you know will only be used by one client that you can monitor, this is fine. But in a shared codebase, or worse, in framework code that is shipped to third parties, such implementations should be avoided.
If a given interface has more features than you need, use the Interface Segregation Principle (ISP) to create a new interface that only has the features that your client code needs and that you can fully implement.
As a parallel example, consider the case where a father is a teacher and his son is a doctor. The son cannot simply replace his father, even though both belong to the same family. Another ‘Apples and Oranges’ example can be found in the GitHub repository here.
Interface Segregation Principle (ISP)
The ISP states that “Clients should not be forced to implement any methods they do not use. Rather than one fat interface, numerous little interfaces are preferred, based on groups of methods, with each interface serving one submodule.“
That definition can be split into two parts:
- No class should be forced to implement any interface method(s) that it does not use.
- Rather than creating large interfaces, create multiple smaller interfaces to allow clients to focus on the methods that are relevant to them.
Suppose we are dealing with a model for a printer rental company. Inkjet printers, laser printers, and multifunction printers are all available. Every one of the classes inherits the IPrinterTasks interface, which contains the declaration for the following method:
You can see that some printers do not provide all the features mentioned above. Regular printers, for example, do not offer scan or fax functionality. All classes are forced to implement the above-mentioned methods (without any real functionality) by inheriting from a single interface, which would be a clear violation of ISP.
The mitigation implies splitting the interface into smaller, dedicated interfaces, and using multiple inheritances of the interfaces on a class (a multifunction printer) that provides additional functionality.
The source code can be found here.
Dependency Inversion Principle (DIP)
DIP, the fifth SOLID principle, states that high-level modules/classes should not depend on low-level modules/classes. Instead, both should depend upon abstractions.
Secondly, abstractions should not depend on details; details should depend upon abstractions.
Always try to keep the high-level module and the low-level module as loosely coupled as possible.
When a class knows about the design and implementation of another class, it raises the risk that changes to one class will break the other class. So, we must keep these high-level and low-level modules/classes loosely coupled as much as possible.
To do that, we need to make both of them dependent on abstractions instead of knowing each other. The source code can be found here.
Don’t forget to come back to our blog space next week. After presenting the SOLID principles, in the next article (Part 2), I explain additional common software engineering principles, such as the Boy Scout Rule, Don’t Repeat Yourself (DRY), Encapsulation, the Principle of Least Astonishment (PoLA), and Don’t Call Us, We’ll Call You (Hollywood).