The Five Basic Object-Oriented (SOLID Principles) for Designing Software
Mohamed Kassem | July 30, 2020
Table of Contents
- Single Responsibility Principle (SRP)
- Open/Closed Principle (OCP)
- Liskov Substitution Principle (LSP)
- Interface Segregation Principle (ISP)
- Dependency Inversion Principle (DIP)
- Organizing and extending SOLID Principles into your project
Solid-Principles
SOLID principles are the design principles that enable us to manage most of the software design problems promoted by Robert C. Martin and introduced by Michael Feathers.
it is a mnemonic acronym. It helps to define the five basic object-oriented design principles to make software designs more understandable, flexible, and maintainable:
- Single Responsibility Principle
- Open-Closed Principle
- Liskov Substitution Principle
- Interface Segregation Principle
- Dependency Inversion Principle
Single Responsibility Principle (SRP)
- The Code/Class/Method “do one thing and do it well”
- Uncle Bob, “There should never be more than one reason for a class to change”
- A Class or method Should have one and only one reason to change, meaning that a class should have only one job or responsible for one part of the functionality
Examples of responsibilities of the application
every software has many responsiblities while you building it like
- Persistence (Storage)
- Logging
- Validation
- Business Logic
Example 1 for the code here:
Finding Responsibilities in one class that make the code difficult and longer to test one responsibility in isolation
Solution after making them in different classes that now are easily tested
Example 2:
Problem:
Solution:
Open/Closed Principle (OCP)
- Software entities(modules, classes, methods) should be open for extension but closed for modification.
- any new functionality should be done by adding new classes instead of changing an existing one
Why Should Code be closed to modification?
- Less Likely to introduce bugs in code we don’t touch
- Less Likely to break dependent code when we don’t have to deploy updates
- Fewer conditionals in code that is open to extension results in simpler code
- All the attributes and behaviors are encapsulated
- Proven to be stable within your system
- Bug Fixes are ok
closed
does not mean that changes cannot be made to a class during development. It happens when most of the design decisions have been finalized and once you have implemented most of your system.
How can you predict future changes
- start concrete and modify the code the first time or two
- by the third modification, consider making the code open to extension for that axis of change
How to Implement OCP
- adding new functionality to derived class (abstract class)
- or allow the client to access the original class with an abstract interface
1- Abstract Class
- first, make the base class that contains the original method abstract class and make the method abstract method don’t have an implementation,
- Create class/classes inherit from this abstract class to do these abstract method in different functionality … so now it’s close for modifications and open for an extension new method
2- Interface
- Create an interface that contains the common method that wants to add new functionality to it
- Create class/classes implement this interface to do the different functionalities for this method
Other Typical Approaches to OCP
- 1- Parameters
- 2- Inheritance
- 3- Composition/ Injection
Applying these approaches to this simple code
public class DoOneThing
{
public void Execute()
{
Console.WriteLine("Hello World.");
}
}
- Parameters
- Inheritance
- Composition/ Injection
Example OCP in Packages and Libraries in NuGet or NPM
using extension methods in C# like this example
Liskov Substitution Principle (LSP)
- if you have class B inherit from class A then class A should be replaceable by class B without any changes/ problems
- LSP Is a Subset of Polymorphism
How to Apply LSP
- Make the method in base class is virtual
- to override this method while used in derived class when create object
Example
namespace SOLID_PRINCIPLES.LSP
{
class Program
{
static void Main(string[] args)
{
Fruit fruit = new Orange();
Console.WriteLine(fruit.GetColor());
fruit = new Apple();
Console.WriteLine(fruit.GetColor());
}
}
public abstract class Fruit
{
public abstract string GetColor();
}
public class Apple : Fruit
{
public override string GetColor()
{
return "Red";
}
}
public class Orange : Fruit
{
public override string GetColor()
{
return "Orange";
}
}
}
Detecting LSP Violations in your code
- Type Checking with is or as in polymorphic code
foreach(var employee in employees)
{
if(employee is Manager)
{
Helpers.PrintManager(employee as Manager);
break;
}
Helpers.PrintEmployee(employee);
}
- null checks
foreach(var employee in employees)
{
if(employee == null)
{
Console.WriteLine("Employee not found.");
break;
}
Helpers.PrintEmployee(employee);
}
- NotImplementException in interface implementation
Detecting LSP Violations in your code
- Follow the “Tell, Don’t Ask” Principle
- Minimize null checks with - c# features - Guard clauses - Null Object Design Pattern
- Follow ISP and be sure to fully implement interfaces
Interface Segregation Principle (ISP)
- uncle bob said “Clients should not be forced to depend on methods they do not use”
-
Many client-specific interfaces are better than one general-purpose interface.
- avoid fat interface so prefer small
- Client must not implement unnecessary methods
The Interface Segregation Principle states that any classes that implement an interface, should not have “dummy” implementations. Instead you should split large interfaces into smaller generalizations.
Detecting ISP Violations in your code
- Large Interfaces
- NotImplementedException
- Code uses just a small subset of a larger interface
Fixing ISP Violations
- Split interface Up into smaller ones and if you need use multiple interface inheritance
Example 1
Example 2
Dependency Inversion Principle (DIP)
The principle states that high-level modules should depend on high-level generalizations, and not on low-level details. This means your class should depend on Interface or Abstract class, not a concrete class. Interfaces and Abstract classes are high-level resources. A concrete class is a low-level resource.
-
High-Level Modules should not depend upon Low-Level Modules. Both should depend upon abstractions.
- abstractions should not depend on details
- details should depend on abstractions
- Applying this principle would make your code loosely coupling and highly cohesive (don’t depend on low-level classes)
- High-level Module is the module that has business rules, more abstract, process-oriented, further from (I/O) while Low-Level Module is closer to (I/O) and interacts with specific external systems and hardware (keep plumbing code separate from high-level business logic)
- Low-Level Dependencies like Database, File system, Email, Web APIs, Configuration and Clock
- This principle name “Dependency Inversion” for studying in a book, but in actual coding called “dependency injection” like in asp.net core that is completely depend on it and follow it
-
dependency injection (DI) says don’t create your own dependencies instead you should depend on abstractions and request those dependencies from the client. there are three methods on how to apply that
- Constructor arguments (Prefer one because it follows explicit dependencies principle)
- Properties injection
- Method Constructor
- Like this design/problem that causes pain like tight coupling, low cohesive, difficult to isolate and unit test and duplicate the code
- but the solution would be :
So instead of the following diagram that implement low level concrete class/details which it consume a lot of work if there are any changes.
Applying DIP to depend on High-Level Generalization will become like this
This means that the Client is dependent on expected behaviors (high level), not on a specific implementation (low level).
Least Knowledge Principle (Law of Demeter)
This principle states that classes should know about and interact with a few other classes as possible. Not every other class of the system. It reduces coupling and provides stability to your systems.
According to this design principle a method M
of and object should only call other methods if they are:
- Encapsulated within the same object
- Encapsulated within an object that is in the parameters of
M
- Encapsulated within an object that is instantiated inside of
M
- Encapsulated within an object that is referenced in an instance variable of the class for
M
All this rules of this law mean ⇒ that a method should not invoke methods of any object that is not local. Don’t call objects that you shouldn’t know about or unknown type of object don’t exist in your class.
Returned objects from methods must be of the same type as:
- Those declared in the method parameter
- Those declared and instantiated locally in the method.
- Those declared in instance variables of the class that encapsulates the method.
Classes should know as little as possible about your system as a whole to reduce the amount of coupling and prevent unwanted effects from cascading through your entire system
Organizing and extending SOLID Principles into your project
cause following all these principles would need too many files that focus on more classes and interfaces so do the following technique for avoiding that problem by
-
Use Folders!
- several folders with each folder have few classes/interfaces
- each folder should be specified in its task by naming them appropriately, it should be pretty easy to find what you’re looking
- the default way is by kind of thing the file is, so might put controllers in one folder, models in another, a folder for interfaces and another for services … etc
- another way especially when the app is growing is to use feature folders by organizing your app with feature not kind of thing like an online store might have top-level folders for catalog, search and cart instead of controllers, models, views, and services… also follow this repo