SOLID Design Principles

SOLID Design Principles

S.O.L.I.D is an acronym for the first five object-oriented design(OOD) principles . These principles, when combined together, make it easy for a programmer to develop software that is easy to maintain and extend. They also make it easy for developers to avoid code smells, easily refactor code and are also a part of agile or adaptive software development.

  1. SRP The Single Responsibility Principle: — a class should have one, and only one, the reason to change, meaning that a class should have only one job.
  2. OCP The Open Closed Principle: — you should be able to extend a class’s behavior, without modifying it.
  3. LSP The Liskov Substitution Principle: — If any module is using a Base class then the reference to that Base class can be replaced with a Derived class without affecting the functionality of the module.
  4. ISP The Interface Segregation Principle: — make fine grained interfaces that are client specific.
  5. DIP The Dependency Inversion Principle — depend on abstractions not on concrete implementations.

Single Responsibilty Principle:

One Class should be responsible for one task.

class DataAccess 
{
public static void InsertData()
{
Console.WriteLine("Data inserted into database successfully");
Console.WriteLine( "Logged Time:" + DateTime.Now.ToLongTimeString() + " Log Data insertion completed successfully");
}
}

So tomorrow if you want add a new logging like event viewer or File I/O then we need to go and change the “DataAccess”class, which is not right.
It’s like if “JOHN” has a problem why do I need to check “BOB”.

// Data access class is only responsible for data base related operations 
class DataAccess
{
public static void InsertData()
{
Console.WriteLine("Data inserted into database successfully");
}
}
// Logger class is only responsible for logging related operations
class Logger
{
public static void WriteLog()
{
Console.WriteLine( "Logged Time:" + DateTime.Now.ToLongTimeString() + " Log Data insertion completed successfully");
}
}

Open Closed Principle:

we should strive to write code that doesn’t have to be changed every time the requirements change. How we do that can differ a bit depending on the context, such as our programming language.
Create a Base class with Required functionality, and ensure we will not modify that class. (Closed for modification)
Create a Derived class by inheriting the Base class for extension (Open for modification)

public class Rectangle
{
public double Width { get; set; }
public double Height { get; set; }
}
public class AreaCalculator
{
public double Area(Rectangle[] shapes)
{
double area = 0;
foreach (var shape in shapes)
{
area += shape.Width*shape.Height;
}

return area;
}
}

If we want to calculate the area of not only rectangles but of circles as well.

public double Area(object[] shapes)
{
double area = 0;
foreach (var shape in shapes)
{
if (shape is Rectangle)
{
Rectangle rectangle = (Rectangle) shape;
area += rectangle.Width*rectangle.Height;
}
else
{
Circle circle = (Circle)shape;
area += circle.Radius * circle.Radius * Math.PI;
}
}

return area;
}

AreaCalculator isn’t closed for modification as we need to change it in order to extend it. Or in other words: it isn’t open for extension. In a real world scenario where the code base is ten, a hundred or a thousand times larger and modifying the class means redeploying it’s assembly/package.

One way of solving this puzzle would be to create a base class for both rectangles and circles as well as any other shapes which defines an abstract method for calculating it’s area.

public abstract class Shape
{
public abstract double Area();
}
public class Rectangle : Shape
{
public double Width { get; set; }
public double Height { get; set; }
public override double Area()
{
return Width*Height;
}
}
public class Circle : Shape
{
public double Radius { get; set; }
public override double Area()
{
return Radius*Radius*Math.PI;
}
}

As we’ve moved the responsibility of actually calculating the area away from AreaCalculator’s Area method it is now much simpler and robust as it can handle any type of Shape that we throw at it.

public double Area(Shape[] shapes)
{
double area = 0;
foreach (var shape in shapes)
{
area += shape.Area();
}
return area;
}

Liskov Substitution Principle:

If any module is using a Base class then the reference to that Base class can be replaced with a Derived class without affecting the functionality of the module.

we must make sure that new derived classes are extending the base classes without changing their behavior. If we are calling a method defined at a base class upon an abstracted class, the function must be implemented properly on the subtype class.

Let’s say our system wants to calculate discounts for Enquiries. Now Enquiries are not actual customer’s they are just leads. Because they are just leads we do not want to save them to database for now.

So we create a new class called as Enquiry which inherits from the “Customer” class. We provide some discounts to the enquiry so that they can be converted to actual customers and we override the “Add’ method with an exception so that no one can add an Enquiry to the database.

class Enquiry : Customer
{
public override double getDiscount(double TotalSales)
{
return base.getDiscount(TotalSales) - 5;
}

public override void Add()
{
throw new Exception("Not allowed");
}
}

So as per polymorphism rule parent “Customer” class object can point to any of it child class objects i.e. “Gold”, “Silver” or “Enquiry” during runtime without any issues.

List<Customer> Customers = new List<Customer>();
Customers.Add(new SilverCustomer());
Customers.Add(new goldCustomer());
Customers.Add(new Enquiry());

foreach (Customer o in Customers)
{
o.Add(); //throw exception for Enquiry
}

As per the inheritance hierarchy the “Customer” object can point to any one of its child objects and we do not expect any unusual behavior.

But when “Add” method of the “Enquiry” object is invoked it leads to exception. In other words the “Enquiry” has discount calculation , it looks like a “Customer” but IT IS NOT A CUSTOMER. So the parent cannot replace the child object seamlessly. In other words “Customer” is not the actual parent for the “Enquiry”class. “Enquiry” is a different entity altogether.

interface IDiscount
{
double getDiscount(double TotalSales);
}


interface IDatabase
{
void Add();
}
class Enquiry : IDiscount
{
public double getDiscount(double TotalSales)
{
return TotalSales - 5;
}
}
class Customer : IDiscount, IDatabase
{
public virtual void Add()
{
// Database code goes here
}

public virtual double getDiscount(double TotalSales)
{
return TotalSales;
}
}

In case we make a mistake of adding “Enquiry” class to the list compiler would complain.

List<Customer> Customers = new List<Customer>();
Customers.Add(new SilverCustomer());
Customers.Add(new goldCustomer());
Customers.Add(new Enquiry());//error

Interface Segregation Principle:

Clients should not be forced to depend upon interfaces that they do not use.

interface IToy {
void setPrice(int price);
void setColor(String color);
void move();
void fly();
}
class ToyHouse :IToy {
int price;
String color;
public void setPrice(double price) {
this.price = price;
}
public void setColor(String color) {
this.color=color;
}
public void move(){
throw new Exception(“Not allowed”);
}
public void fly(){
throw new Exception(“Not allowed”);
}
}

ToyHouse needs to provide implementation of the move() and fly()methods, even though it does not require them. This is a violation of the Interface Segregation Principle. Such violations affect code readability and confuse programmers. Violation of the Interface Segregation Principle also leads to violation of the complementary Open Closed Principle. As an example, consider that the Toy interface is modified to include a walk() method to accommodate toy robots. As a result, you now need to modify all existing Toy implementation classes to include a walk() method even if the toys don’t walk. In fact, the Toy implementation classes will never be closed for modifications, which will lead to a fragile application that is difficult and expensive to maintain.

By following the Interface Segregation Principle, you can address the main problem of the toy building application- The Toy interface forces clients (implementation classes) to depend on methods that they do not use.

The solution is- Segregate the Toy interface into multiple role interfaces each for a specific behavior. Let’s segregate the Toy interface, so that our application now have three interfaces: ToyMovable, and Flyable.

interface IToy {
void setPrice(int price);
void setColor(String color);
}
interface IMovable {
void move();
}
interface IFlyable {
void fly();
}

As all toys will have a price and color, all Toy implementation classes can implement this interface. Then, we wrote the Movable and Flyable interfaces to represent moving and flying behaviors in toys.

class ToyHouse :IToy {
int price;
String color;
public void setPrice(double price) {
this.price = price;
}
public void setColor(String color) {
this.color=color;
}
}
Class ToyPlane implements IToy, IMovable, IFlyable {
double price;
String color;
public void setPrice(int price) {
this.price = price;
}
public void setColor(String color) {
this.color=color;
}
public void move(){//code related to moving plane}public void fly(){// code related to flying plane}
}

There are times when you might need your interface to have multiple methods, and that’s ok.Include methods which are all very specific to the interface and the client will most likely want to interact with them, therefore packaging them together in the same interface is the right thing to do.

Dependency Inversion Principle :

Entities must depend on abstractions not on concretions. It states that the high level module must not depend on the low level module, but they should depend on abstractions.Abstractions should not depend on details. Details should depend on abstractions

Inversion of Control:

With traditional programming, the main function of an application might make function calls into a menu library to display a list of available commands and query the user to select one. The library thus would return the chosen option as the value of the function call, and the main function uses this value to execute the associated command. In this interaction, my code is in control: it decides when to ask questions, when to read responses, and when to process those results.

With inversion of control, on the other hand, the program would be written using a software framework that knows common behavioral and graphical elements, such as windowing systems, menus, controlling the mouse, and so on. The custom code “fills in the blanks” for the framework, such as supplying a table of menu items and registering a code subroutine for each item, but it is the framework that monitors the user’s actions and invokes the subroutine when a menu item is selected.

In the command line form I control when these methods are called, but in the window example I don’t. Instead I hand control over to the windowing system. It then decides when to call my methods, based on the bindings I made when creating the form. The control is inverted - it calls me rather me calling the framework. This phenomenon is Inversion of Control (also known as the Hollywood Principle - "Don't call us, we'll call you").

So rather than the internal program controlling the flow, events drive the program flow. Event flow approach is more flexible as their no direct invocation which leads to more flexibility. You can delegate the control flow by callback delegates, observer pattern, events, DI (Dependency injection) and lot of other ways.


Problem: You have classes that have dependencies on services or components whose concrete type is specified at design time. In this example, ClassA has dependencies on ServiceA and ServiceB. Figure 1 illustrates this.


This situation has the following problems:

  • To replace or update the dependencies, you need to change your classes’ source code.
  • The concrete implementations of the dependencies have to be available at compile time.
  • Your classes are difficult to test in isolation because they have direct references to dependencies. This means that these dependencies cannot be replaced with stubs or mocks.
  • Your classes contain repetitive code for creating, locating, and managing their dependencies.

Any of the following conditions justifies using the solution described in this pattern:

  • You want to decouple your classes from their dependencies so that the dependencies can be replaced or updated with minimal or no changes to your classes’ source code.
  • You want to write classes that depend on classes whose concrete implementations are not known at compile time.
  • You want to test your classes in isolation, without using the dependencies.
  • You want to decouple your classes from being responsible for locating and managing the lifetime of dependencies.

Solution:

Delegate the function of selecting a concrete implementation type for the classes’ dependencies to an external component or source.

Implementation Details

The Inversion of Control pattern can be implemented in several ways. The Dependency Injection pattern and the Service Locator pattern are specialized versions of this pattern that delineate different implementations. Figure 2 illustrates the conceptual view of both patterns.

Service Locator:

Create a service locator that contains references to the services and that encapsulates the logic to locate them. In your classes, use the service locator to obtain service instances. The service locator does not instantiate the services. It provides a way to register services and it holds references to the services. After the service is registered, the service locator can find the service.

public interface IServiceLocator { T GetService<T>(); }

Now let’s see a very simple implementation of this contract:

class ServiceLocator : IServiceLocator {  
// map that contains pairs of interfaces and
// references to concrete implementations
private IDictionary<object, object> services;
internal ServiceLocator() {
services = new Dictionary<object, object>(); // fill the map this.services.Add(typeof(IServiceA), new ServiceA()); this.services.Add(typeof(IServiceB), new ServiceB()); this.services.Add(typeof(IServiceC), new ServiceC());
}
public T GetService<T>() {
try {
return (T)services[typeof(T)];
}
catch (KeyNotFoundException) {
throw new ApplicationException("The requested service is not registered");
}
}
}

The generic GetService() method returns a reference the correct implementation fetching it from the dictionary .This is how a client would invoke the service:

IServiceLocator locator = new ServiceLocator();
IServiceA myServiceA = locator.GetService<IServiceA>();

The clients do not know the actual classes implementing the service. They only have to interact with the service locator to get to an implementation.

Dependency Injection:

Declaratively express dependencies in your class definition. Use a Builder object to obtain valid instances of your object’s dependencies and pass them to your object during the object’s creation and/or initialization.

public class ManagementController : Controller
{
private readonly ITenantStore tenantStore;

public ManagementController(ITenantStore tenantStore)
{
this.tenantStore = tenantStore;
}

public ActionResult Index()
{
var model = new TenantPageViewData<IEnumerable<string>>
(this.tenantStore.GetTenantNames())
{
Title = "Subscribers"
};
return this.View(model);
}
}

ManagementController constructor receives an ITenantStore instance as a parameter, injected by some other class. The only dependency in the ManagementContoller class is on the interface type. This is better because it doesn’t have any knowledge of the class or component that is responsible for instantiating the ITenantStore object.

The class that is responsible for instantiating the TenantStore object and inserting it into the ManagementController class is called the DependencyInjectionContainer class.


Thanks, for reading the blog, I hope it helps you. Please share this link on your social media accounts so that others can read our valuable content. Share your queries with our expert team and get Free Expert Advice for Your Business today.


About Writer

Ravinder Singh

Full Stack Developer
I have 12+ years of experience in commercial software development. I write this blog as a kind of knowledge base for myself. When I read about something interesting or learn anything I will write about it. I think when writing about a topic you concentrate more and therefore have better study results. The second reason why I write this blog is, that I love teaching and I hope that people find their way on here and can benefit from my content.

Hire me on Linkedin

My portfolio

Ravinder Singh Full Stack Developer