Solid Principles of OOP

Coding standards define code convention or style guide, like how English follow grammar rules, writing codes that follow coding conventions improve the readability and understandability of the code.

Let's check Some Principles here:

What is SOLID?

As a software developer, we always need to deal with changes in requirements or a new feature in the middle of development or some extension on a feature in the maintenance phase. It is important to write code that’s easy to extend and maintain and make our life less painful.
SOLID is a set of rules that guide us on how to achieve that in object-oriented programming. Using SOLID guidelines you can avoid bad design in your code and give a well-structured architecture to your design. Bad design leads to the inflexible and brittle codebase, a simple and small change can result in bugs with bad design.

SOLID is an acronym of five sets of principles that was proposed by Robert C Martin aka “Uncle Bob” in the year 2000 to write high cohesive, maintainable and extensible software systems.

Following are the five concepts that make up SOLID principles:

1- Single Responsibility principle

2- Open/Closed principle

3- Liskov Substitution principle

4- Interface Segregation principle

5- Dependency Inversion principle

So let’s understand each principle in detail with example

1- Single Responsibility Principle:

As the name suggests, this principle’s purpose is to have a single responsibility for a class/module. In other words, we can say that the class or module should solve one and only one problem So it should have a single reason to change. It makes our code more cohesive hence make it easy to test and maintain.

“A class should only have a single responsibility, that is, only changes to one part of the software’s specification should be able to affect the specification of the class”

Let’s understand with a simple example:

we have the following code:

public class RegisterService { public void RegisterUser(string username) { if (username == "admin") throw new InvalidOperationException(); SqlConnection connection = new SqlConnection(); connection.Open(); SqlCommand command = new SqlCommand("INSERT INTO [...]");//Insert user into database. SmtpClient client = new SmtpClient("smtp.myhost.com"); client.Send(new MailMessage()); //Send a welcome email. } }

In the above example code, we have simple register class with a method which is taking care few responsibilities from checking authorization to db connection and sending email which breaks the single responsibility principle!

instead, we could have the following code:

public void RegisterUser(string username) { if (username == "admin") throw new InvalidOperationException(); _userRepository.Insert(...); _emailService.Send(...); }

By having above code, we split out code into separate classes and also abstracting code so that changes unrelated to our core responsibility of registering users affects minimal change to our code.

2-Open/Close Principle:

This principle purpose that existed and well-tested class should not be modified when a new feature needs to be built. It may introduce a new bug when we modify an existing class to make a new feature. So rather than changing an existing class/Interface, we should extend that class/Interface in a new class to add new features.

“Software entities … should be open for extension, but closed for modification.”

Let’s understand with the example:

class SavingAccount { private $balance; public function setBalance($balance){} public function getBalance(){} public function withdrawal(){} } class FixedDipositAccount() { private $balance; private $maturityPeriod; public function setBalance($balance){} public function getBalance(){} } class IntrestCalculator { public function calculate($account) { if ($account instanceof SavingAccount) { return $account->getBalance*3.0; } elseif ($member instanceof FixDipositAccount) { return $account->getBalance*9.5; } throw new Exception('Invalid input member'); } } $savingAccount = new SavingAccount(); $savingAccount->setBalance(15000); $fdAccount = new FixedDipositAccount(); $fdAccount->setBalance(25000); $intrestCalculator = new IntrestCalculator(); echo $intrestCalculator->calculate($savingAccount); echo $intrestCalculator->calculate($fdAccount);

In the above example, we have two simple class SavingAccount and FixedDepositAccount and a class IntrestCalculator that calculate interest based on the provided object type. But we have a problem here. Our IntrestCalculator class is not closed for modifications. Whenever we need to add a new type of account we need to change our IntrestCalculator class to support the newly introduced account type in the system. So the above solution violates the Open/Closed principle.
Here is the improved implementation:

3-Liskov Substitution principle:

This principle is named after the name of Barbara Liskov. She introduced this principle in 1987. The concept states that a subtype must be substitutable to base types without breaking the behavior. It is a particular definition of subtyping relation, called behavioral subtyping.

“Objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program.”

Let’s understand with a very basic example.

In the above example, we have two simple classes A and B, Class B inherit class A. So as per the Liskov Substitution Principle wherever we use class A in our program we should be able to replace it with its subtype class B.

3-Interface Segregation Principle:

This principle states that an interface should not enforce unwanted methods to a class. The idea here is instead of having a large interface we should have smaller interfaces, So we should not create an interface having a lot of methods If we have such an interface we should break in into smaller interfaces.

“Many client-specific interfaces are better than one general-purpose interface.”

5-Dependency Inversion Principle:

According to this principle higher level classes should not directly depend on lower level classes but abstractions. It means that a higher level class should not need to know the implementation details of the low-level class, the low-level class should be hidden behind an abstraction.

Here is an example: let’s suppose we have a Post class that represents a blog post and read/write posts from db, To do that it requires a database connection. So our first attempt may look like this.

In the above implementation, we have defined a MySqlConnection class that has a connect method to make a connection to db and we pass this MySqlConnection class to the constructor of Post class. The problem here is that our post-class depends on a concrete class MySqlConnection. Now in the future, if we need to support another DB we have to change our Post class.

To avoid this we have to refactor our Post class and instead of passing a concrete class type in the constructor we pass a DbConnection interface that is implemented by MySqlConnection class.

So now there is no need to change Post class for new DB type. The only thing that we need to support the new DB is that it must implement the interface DBConnectionInterface.

Conclusion:
Object-Oriented Programming or OOPs refers to languages that use objects in programming. Object-oriented programming aims to implement real-world entities like inheritance, abstraction, polymorphism, and encapsulation in programming. By following above principles, we increase the code productivity and performance.