SOLID Principles Demystified: Building Better Software Designs
Jeevachaithanyan Sivanandan
[ Python + PHP + JavaScript ] - Software Engineer / Frontend Developer / Full Stack Developer
The SOLID principles are a set of design principles that help developers create maintainable, flexible, and scalable software. SOLID stands for:
Below examples are written in PHP but if you like to read the equivalent code in Python, JavaScript or Typescript then please click links below:
Let's go through each principle with example code in PHP.
<?ph
class Bird
{
// we have a class for Bird and it has below listed functionalities
// 1. method of setting up a nest
// 2. method for hatchery
// 3. method for training
}
// but using Single Responsibility Principle (SRP):
// we shall seperate these functionalities into their own seperate classes
class NestBird
{
// method of setting up a nest
}
class HatchBird
{
// method for hatchery
}
class TrainingBird
{
// method for training
}
In the above example, the class is responsible for setting up nests, handling hatchery operations, and managing training. This mixing of concerns could lead to code that is harder to maintain and understand.
In the refactored code, you've separated the functionalities into their own separate classes (NestBird, HatchBird, TrainingBird), each handling a single responsibility. This separation of concerns aligns with the Single Responsibility Principle, making the code easier to maintain and understand.
By adhering to the SRP, you ensure that each class has a clear and distinct purpose, making the codebase more modular and flexible in case you need to modify or extend specific functionalities in the future.
2.Open-Closed Principle
The Open-Closed Principle states that software entities (classes, modules, functions, etc.) should be open for extension but closed for modification. In other words, you should be able to add new functionality without modifying existing code.
In this below example, you've extended the functionality by creating new bird classes (Emu and Duck) that implement existing interfaces (runningBirds and swimmingBirds). You haven't modified the existing code (e.g., the Ostrich class or interfaces) to accommodate these new functionalities. This adheres to the Open-Closed Principle, as the system is open for extension (you can add new bird behaviors) but closed for modification (existing code remains unchanged).
<?php
interface Bird
{
// add common features for all birds
public function getLegs();
public function getWings();
}
interface flyingBirds
{
// add flying functions
public function fly();
}
interface runningBirds
{
// add running functions
public function run();
}
interface swimmingBirds
{
// add swimming functions
public function swim();
}
class Ostrich implements runningBirds
{
public function run()
{
return "Ostrich runs";
}
}
class Emu implements runningBirds
{
public function run()
{
return "Emu runs";
}
}
class Duck implements swimmingBirds
{
public function swim()
{
return "Duck swims";
}
}
$emmanuel = new Emu();
print $emmanuel->run();
$donald = new Duck();
print $donald->swim();
3. Liskov Substitution Principle (LSP)
It states that objects of a superclass should be replaceable with objects of a subclass without affecting the correctness of the program. In simpler terms, if a class implements an interface or inherits from a base class, any instance of that subclass should be able to be used wherever an instance of the base class or interface is expected, without causing issues.
The LSP ensures that derived classes behave in a way that is consistent with the expectations of the base class or interface. Violating the LSP can lead to unexpected behaviour and can break the functionality of the program.
In this example, the makeBirdFly function expects an object that implements the flyingBirds interface.?The function is designed to work with flying birds.?The sparrow class implements both Bird and flyingBirds interfaces,?so it can be passed to the makeBirdFly function without any issues.?However, if you tried to pass an instance of the Ostrich class (which implements runningBirds interface) to the?makeBirdFly function, it would lead to a fatal error because the Ostrich class does not meet the expected contract?of the flyingBirds interface.
This demonstrates the Liskov Substitution Principle: you should be able to substitute a derived class instance?for a base class instance without altering the correctness of the program. In this case, the Sparrow class adheres to this principle,?while the Ostrich class violates it when trying to be used in a context where flying is expected.
领英推荐
<?php
interface Bird
{
// add common features for all birds
public function getLegs();
public function getWings();
}
interface flyingBirds
{
// add flying functions
public function fly();
}
interface runningBirds
{
// add running functions
public function run();
}
interface swimmingBirds
{
// add swimming functions
public function swim();
}
// lets create a trait because
trait CommonBirdFeatures
{
public function getLegs()
{
return "this bird has 2 legs";
}
public function getWings()
{
return "this bird has two wings";
}
}
class Sparrow implements flyingBirds
{
use CommonBirdFeatures;
public function fly()
{
return "this bird can fly";
}
public function makeBirdFly(flyingBirds $bird)
{
return $bird->fly();
}
}
$robin = new Sparrow();
print $robin->getLegs() . "\n";
print $robin->makeBirdFly($robin);
please note that the above code introduces another concept in PHP, which is known as 'trait'. PHP does not support multiple inheritance, meaning a class can't inherit from multiple classes. Traits provide a way to include behaviors from multiple sources into a single class without the need for multiple inheritance.
4.Interface Segregation Principle (ISP)
The Interface Segregation Principle (ISP) is one of the SOLID principles in object-oriented programming. It emphasizes that classes or clients should not be forced to depend on methods they do not use. In other words, when designing interfaces, it's better to create smaller and more specific interfaces rather than large and monolithic ones. This helps to avoid forcing implementers to provide empty or irrelevant methods.
<?php
interface Bird
{
// add common features for all birds
public function getLegs();
public function getWings();
}
interface flyingBirds
{
// add flying functions
public function fly();
}
interface runningBirds
{
// add running functions
public function run();
}
interface swimmingBirds
{
// add swimming functions
public function swim();
}
// lets create a trait
trait CommonBirdFeatures
{
public function getLegs()
{
return "this bird has 2 legs";
}
public function getWings()
{
return "this bird has two wings";
}
}
class Sparrow implements flyingBirds
{
use CommonBirdFeatures;
public function fly()
{
return "this bird can fly";
}
public function makeBirdFly(flyingBirds $bird)
{
return $bird->fly();
}
}
$robin = new Sparrow();
print $robin->getLegs()."\n";
print $robin->makeBirdFly($robin);
In the above example code, you have interfaces for different types of birds and their capabilities. However, this design could potentially violate the ISP if a class is forced to implement methods that are irrelevant to its behaviour. For instance, if you have a bird that can both fly and swim, it would need to implement the methods from both flyingBirds and swimmingBirds interfaces. But what if a bird doesn't swim? This would lead to empty methods in that class, which is not ideal.
To adhere to the Interface Segregation Principle, you could split the interfaces into more specific ones: so the above code, the Sparrow class implements the FlyingBird interface, which is more specific to its actual behaviour. It doesn't need to implement methods like swim() that are irrelevant to its behaviour. This adheres to the ISP by not forcing the Sparrow class to depend on methods it doesn't use.
By splitting interfaces into smaller, more cohesive pieces, you achieve a more flexible and maintainable design. This way, clients (classes or objects) are only required to implement methods that are relevant to their actual behaviour, avoiding unnecessary method implementations.
5.Dependency Inversion Principle (DIP)
The Dependency Inversion Principle (DIP) is one of the SOLID principles in object-oriented programming. It promotes the idea that high-level modules (classes or components) should not directly depend on low-level modules. Instead, both high-level and low-level modules should depend on abstractions (interfaces or abstract classes). Additionally, abstractions should not depend on implementation details; rather, implementation details should depend on abstractions.
?High-level and Low-level Modules: In below example, the high-level modules would be the classes that use the bird interfaces, and the low-level modules would be the classes that implement those interfaces.??
?Depend on Abstractions: The DIP suggests that both high-level and low-level modules should depend on abstractions (interfaces) rather than concrete implementations. This way, changes to the low-level modules' implementations won't affect the high-level modules.
<?php
// Abstractions (Interfaces
interface Bird
{
public function getLegs();
public function getWings();
public function performAction(); // Introduce a common method
}
interface BirdAction
{
public function execute();
}
trait CommonBirdFeatures
{
public function getLegs()
{
return "this bird has 2 legs";
}
public function getWings()
{
return "this bird has two wings";
}
}
// Concrete Implementations
class Sparrow implements Bird, BirdAction
{
use CommonBirdFeatures;
public function fly()
{
return "this bird can fly";
}
public function performAction()
{
return $this->fly();
}
public function execute()
{
return $this->performAction();
}
}
class Ostrich implements Bird, BirdAction
{
use CommonBirdFeatures;
public function run()
{
return "this bird can run";
}
public function performAction()
{
return $this->run();
}
public function execute()
{
return $this->performAction();
}
}
// High-level Module
class BirdActionExecutor
{
public function performBirdAction(BirdAction $birdAction)
{
return $birdAction->execute();
}
}
$robin = new Sparrow();
$jacob = new Ostrich();
$actionExecutor = new BirdActionExecutor();
echo $actionExecutor->performBirdAction($robin); // Output: this bird can fly
echo $actionExecutor->performBirdAction($jacob); // Output: this bird can run
??
Abstractions Should Not Depend on Details: Abstractions should define the contract that high-level and low-level modules adhere to. They should not depend on the specific implementation details of the classes that implement them.
In the above code:
This way, the Dependency Inversion Principle is adhered to by making high-level and low-level modules depend on abstractions, and abstractions define the contract without relying on implementation details.
That is all about basic concepts of SOLID principles in Software Programming. Happy coding !!