S.O.L.I.D Principles
Image by ijeab on Freepik

S.O.L.I.D Principles

In 2023, writing code that a machine can understand is doable, writing code that other developers can understand is also doable. The main challenge is to write a code that is scalable and extendable for additional features and system changes.

For that purpose, there are two important concepts we must focus on while designing a system, the cohesion and the coupling of system's entities.

Cohesion refers to the?degree to which the elements inside a?module?belong together, high cohesion means your system's entities are maintainable and not complex, for example, having a Todo list for each goal (e.g. shopping and personal tasks) is highly cohesion than having one Todo list holding all the tasks.

Coupling?refers to the?degree of interdependence between software modules. That means, high coupling refers to a strongly connected system's components, which makes breaking changes happen so often. Low coupling is the goal for a well-structured system.


S.O.L.I.D principles were created to ensure a solid system design, with high cohesion and low coupling possible. In this article, we will discuss the five principles for basic solid design.

  • S - Single Responsibility Principle (SRP)
  • O - Open-Closed Principle (OCP)
  • L - Liskov Substitution Principle (LSP)
  • I - Interface Segregation Principle (ISP)
  • D - Dependency Inversion Principle (DIP)


Single Responsibility Principle

Classes should be having one and only one responsibility        

SRP states that every class should have one job to do, imagine you have created a Rectangle class, that calculates the area of it and draw its design in the interface, your Rectangle class has two responsibilities now, whenever you would like to make a change in the user interface, the Rectangle class will be modified, so you have to test dependent classes that has nothing to do with your GUI changing job.



class Rectangle {
  double area() {}

  void draw() {}
}        

Do you see a high or a low degree of cohesion here? You may think that draw is part of a Rectangle module, but the drawing task is complex and expensive to be part of that module and it shouldn't be there, this is a low level of cohesion.

The solution here is to divide the above class into two classes, one for area calculation, and the other one takes a rectangle and draw it in the screen. Since no other divisions can be made, you achieved the higher cohesion possible using single responsibility classes.



class Rectangle {
  double area() {}
}

class DrawHandler {
  void draw(Rectangle rectangle) {}
}        


Open-Closed Principle



Classes should be open for extension, but closed for modification        

OCP is about extending an already built class, to have newer features, without the need of modifying the existing one, a developer can extend that class and start to write his modifications.

In most cases, interfaces are the key to extensions, have you ever worked on a framework where you wanted to extend an existing component with your custom features/design? that is Open-Closed principle! you extended an existing code instead of modifying and possibly breaking it.


Liskov Substitution Principle



Subtypes must be substitutable for their base types        

Basically, LSP is violated when you get NotImplementedException in a function that is inherited from a base interface.

LSP states that every subclass must be a substitution from their base one, that means, every method implemented in the base class must be implemented in subclasses. Let's take an example for that.

You implemented a car game and created an interface for car's basic actions like accelerate and shift. After implementing the interface, you created your favorite Chevrolet Spark car with manual shifts.



interface Car {
   void accelerate();
   void shift();
}

class SparkCar implements Car {
   void accelerate() {
      // accelerate logic
   }

   void shift() {
      // shift logic
   }
}        

Later on, your game introduced electric cars, with no shift action, if you don't want to refactor the code, but also you don't want to implement shifting for electric cars, you may end up doing something like this:



class ElectricCar implements Car {
   void accelerate() {
      // accelearte logic
   }

   void shift() {
      throw new NotImplementedException("Method not implemented")
   }
}        

Here you violated the LSP because your new class ElectricCar is not substitutable for the Car interface, the solution here is simple, in our case, we can create two interfaces, one for manual cars, and the other one for automatic cars. You can also create a base Car interface that has the shared actions between manual and automatic cars.

Interface Segregation Principle



Classes that implement an interface, shouldn't be forced to implement a method they don't use        

ISP supports Liskov Substitution Principle, actually it is the solution for not violating the LSP, rather than having less and inconvenient interfaces, create as many interfaces as you want as long as the logic fits.

If a class is doing jobs that it doesn't need to do because it is forced by the interface, then ISP is violated and possibly LSP, the solution is to split the logic between multiple classes rather than having fewer interfaces that violate ISP.

Dependency Inversion Principle



Class should depend on abstraction and not concrete instances.        

So what is the difference between abstraction and concrete instances? Let's build a data saving process in your NoSQL database using concrete instance:



class NoSqlDatabase {
   NoSqlDatabase() {
     // Initialize DB
   }
   
   void save(Content content) {
     // Save logic
   }    
}

class ContentHelper {  
   ContentHelper() {}
   
   void saveToDB(Content content) {
      NoSqlDatabase db = new NoSqlDatabase();
      db.save(content);
   }
}        

Do you see a high coupling in the above code? ContentHelper is highly coupled with NoSqlDatabase class, which means if you would like to modify your database, you may end up breaking the ContentHelper or have some changes to do in it, defining your actual NoSqlDatabase class in the constructor or in some cases passing it is the concrete instance.

The solution for this is simple, we can make ContentHelper class depend on an interface that has a save method (abstraction):



interface Database {
   void save();
}

class NoSql implements Database {
   void save() {
      // NoSql save logic
   }
}

class MongoDB implements Database {
   void save() {
      // MongoDB save logic
   }
}


class ContentHelper {
   Database db;

   ContentHelper(Database db) {
      this.db = db;
  }

   void saveToDB(Content content) {
      db.save(content);
   }
}        

By using Database interface, we now can create any instance of a database we want, implement the saving logic, and pass it to ContentHelper without the need to modify it anymore.


Conclusion

In conclusion, while designing a system or looking at a code, always think about the cohesion and the coupling of its components, look for places where two components are tightly coupled together and think of a solution make that coupling looser. By doing this, you may end up discovering S.O.L.I.D principles without knowing them!

Credits

Hatem Elsa'dany

Software Engineer | Instructor | 2 × ACPC Finalist

2 年

?????? great job

Peter Joseph

Software Engineer | Mobile React-Native Developer.

2 年

Perfect ????

Tasneem Hegazy

DevOps Engineer | AWS re/Start Graduate

2 年

Very insightful! ????

Amr Mahmoud

Software Engineer @ColadaApp || Flutter developer

2 年

Great job ????

Ahmed Hossam

SDE @noon | 3x ACPC Finalist

2 年

?? ????? ???? ???? ??????? ????? ???? ??

要查看或添加评论,请登录

社区洞察

其他会员也浏览了