- Last updated: 22 Jan, 2025
The SOLID principles are a set of five design principles that guide object-oriented programming and design. They help developers create systems that are easy to maintain, scale, and extend. These principles are particularly useful for writing clean and maintainable code in PHP. It was Conceptualized by Robert C. Martin, also known as Uncle Bob. Five Design principles of SOLID principles,
Single Responsibility Principle (SRP):
A class should have only one reason to change, meaning it should perform a single responsibility or task.
Example, Without SRP,
class UserManager {
public function addUser($user) {
// Add user logic
$this->validateUser($user);
$this->saveUserToDatabase($user);
$this->sendWelcomeEmail($user);
}
public function validateUser($user) {
// Validation logic
echo 'Validating user: ' . print_r($user, true) . "\n";
}
public function saveUserToDatabase($user) {
// Database logic
echo 'Saving user to the database: ' . print_r($user, true) . "\n";
}
public function sendWelcomeEmail($user) {
// Email logic
echo 'Sending welcome email to: ' . $user['email'] . "\n";
}
}
// Example usage
$userManager = new UserManager();
$user = ['name' => 'John Doe', 'email' => 'john.doe@example.com'];
$userManager->addUser($user);
With SRP,
class UserManager {
private $userRepository;
private $emailService;
public function __construct(UserRepository $userRepository, EmailService $emailService) {
$this->userRepository = $userRepository;
$this->emailService = $emailService;
}
public function addUser($user) {
$this->userRepository->save($user);
$this->emailService->sendWelcomeEmail($user);
}
}
class UserRepository {
public function save($user) {
// Save user to database logic
echo 'Saving user to the database: ' . print_r($user, true) . "\n";
}
}
class EmailService {
public function sendWelcomeEmail($user) {
// Email sending logic
echo 'Sending welcome email to: ' . $user['email'] . "\n";
}
}
// Example usage
$userRepository = new UserRepository();
$emailService = new EmailService();
$userManager = new UserManager($userRepository, $emailService);
$user = ['name' => 'John Doe', 'email' => 'john.doe@example.com'];
$userManager->addUser($user);
Here, the UserManager, UserValidator, and EmailService each have a single responsibility.
Open/Closed Principle (OCP):
Software entities (classes, modules, functions) should be open for extension but closed for modification.
Without OCP,
class PaymentService {
public function processPayment($paymentType) {
if ($paymentType === "CREDIT_CARD") {
// Credit card processing logic
echo "Processing credit card payment...\n";
} elseif ($paymentType === "PAYPAL") {
// PayPal processing logic
echo "Processing PayPal payment...\n";
}
}
}
// Example usage
$paymentService = new PaymentService();
$paymentService->processPayment("CREDIT_CARD");
$paymentService->processPayment("PAYPAL");
// Define a PaymentProcessor interface
interface PaymentProcessor {
public function processPayment();
}
// CreditCardProcessor class
class CreditCardProcessor implements PaymentProcessor {
public function processPayment() {
// Credit card processing logic
echo "Processing credit card payment...\n";
}
}
// PayPalProcessor class
class PayPalProcessor implements PaymentProcessor {
public function processPayment() {
// PayPal processing logic
echo "Processing PayPal payment...\n";
}
}
// PaymentService class
class PaymentService {
private $processors;
public function __construct(array $processors) {
$this->processors = $processors;
}
public function processPayments() {
foreach ($this->processors as $processor) {
$processor->processPayment();
}
}
}
// Example usage
$processors = [new CreditCardProcessor(), new PayPalProcessor()];
$paymentService = new PaymentService($processors);
$paymentService->processPayments();
New payment types can be added without modifying PaymentService.
Liskov Substitution Principle (LSP):
Subtypes must be substitutable for their base types without altering the correctness of the program.
Without LSP,
// Base Rectangle class
class Rectangle {
private $width;
private $height;
public function setWidth(int $width) {
$this->width = $width;
}
public function setHeight(int $height) {
$this->height = $height;
}
public function getArea(): int {
return $this->width * $this->height;
}
}
// Square class that violates LSP
class Square extends Rectangle {
public function setWidth(int $width) {
parent::setWidth($width);
parent::setHeight($width);
}
public function setHeight(int $height) {
parent::setWidth($height);
parent::setHeight($height);
}
}
// Example usage
$rectangle = new Rectangle();
$rectangle->setWidth(4);
$rectangle->setHeight(5);
echo "Rectangle area: " . $rectangle->getArea() . "\n"; // 20
$square = new Square();
$square->setWidth(4);
echo "Square area: " . $square->getArea() . "\n"; // 16 (Expected behavior for a square)
$substitutedRectangle = new Square();
$substitutedRectangle->setWidth(4);
$substitutedRectangle->setHeight(5);
echo "Substituted rectangle area: " . $substitutedRectangle->getArea() . "\n"; // Incorrect behavior, 25
Problem:The Square class overrides the setWidth and setHeight methods to enforce its specific behavior (equal width and height). However, when substituted in place of the Rectangle class, it violates the principle of substitutability since the behavior is no longer consistent with what a Rectangle should do.
With LSP,
// Define the Shape interface
interface Shape {
public function getArea(): int;
}
// Rectangle class implementing Shape
class Rectangle implements Shape {
private $width;
private $height;
public function __construct(int $width, int $height) {
$this->width = $width;
$this->height = $height;
}
public function getArea(): int {
return $this->width * $this->height;
}
}
// Square class implementing Shape
class Square implements Shape {
private $side;
public function __construct(int $side) {
$this->side = $side;
}
public function getArea(): int {
return $this->side * $this->side;
}
}
// Example usage
$rectangle = new Rectangle(4, 5);
echo "Rectangle area: " . $rectangle->getArea() . "\n"; // 20
$square = new Square(4);
echo "Square area: " . $square->getArea() . "\n"; // 16
// Both classes follow LSP and can be used interchangeably as Shape
function printArea(Shape $shape) {
echo "Shape area: " . $shape->getArea() . "\n";
}
printArea($rectangle); // 20
printArea($square); // 16
Both
Rectangle
andSquare
implement theShape
interface.The
getArea
method is implemented independently in both classes without altering the expected behavior of either.The
Shape
interface ensures that both types can be used interchangeably, adhering to the LSP.
Here, Square and Rectangle both implement Shape without breaking substitutability.
Interface Segregation Principle (ISP):
Clients should not be forced to implement interfaces they do not use.
Without ISP,
class Vehicle {
startEngine() {
throw new Error("startEngine method must be implemented");
}
fly() {
throw new Error("fly method must be implemented");
}
}
class Car extends Vehicle {
startEngine() {
// Start engine logic
console.log("Car engine started.");
}
fly() {
throw new Error("Cars can't fly"); // Violates ISP
}
}
// Example usage
const car = new Car();
car.startEngine(); // Works fine
car.fly(); // Throws an error: "Cars can't fly"
Problem:The Car class is forced to implement the fly method from the Vehicle interface, even though it doesn't make sense for a car to fly. This violates the Interface Segregation Principle.
With ISP,
// Define the EngineVehicle interface (base class)
class EngineVehicle {
startEngine() {
throw new Error("startEngine method must be implemented");
}
}
// Define the FlyingVehicle interface (base class)
class FlyingVehicle {
fly() {
throw new Error("fly method must be implemented");
}
}
// Car class implements only EngineVehicle
class Car extends EngineVehicle {
startEngine() {
// Start engine logic
console.log("Car engine started.");
}
}
// Airplane class implements both EngineVehicle and FlyingVehicle
class Airplane extends EngineVehicle {
startEngine() {
// Start engine logic
console.log("Airplane engine started.");
}
fly() {
// Fly logic
console.log("Airplane is flying.");
}
}
// Example usage
const car = new Car();
car.startEngine(); // "Car engine started."
const airplane = new Airplane();
airplane.startEngine(); // "Airplane engine started."
airplane.fly(); // "Airplane is flying."
- Separate Interfaces:
EngineVehicle
defines thestartEngine
method.FlyingVehicle
defines thefly
method.
- Class Implementation:
Car
implements onlyEngineVehicle
since it doesn't need flying functionality.Airplane
implements bothEngineVehicle
andFlyingVehicle
since it needs both functionalities.
Adheres to ISP: Classes implement only the methods they need, avoiding unnecessary implementation of unused methods.
Dependency Inversion Principle (DIP):
High-level modules should not depend on low-level modules; both should depend on abstractions.
Without DIP,
class UserRepository {
public function save($user) {
// Save logic
echo "Saving user: " . $user['name'] . "\n";
}
}
class UserService {
private $userRepository;
public function __construct() {
$this->userRepository = new UserRepository(); // High-level module directly depends on a low-level module
}
public function saveUser($user) {
$this->userRepository->save($user);
}
}
// Example usage
$userService = new UserService();
$userService->saveUser(['name' => 'John Doe']);
Problem: In this example, the UserService class (high-level module) directly depends on the UserRepository class (low-level module). This tight coupling between the high-level and low-level modules violates the Dependency Inversion Principle (DIP).
With DIP,
// Define the UserRepository interface (abstraction)
interface UserRepository {
public function save($user);
}
// Implement the JpaUserRepository class (low-level module)
class JpaUserRepository implements UserRepository {
public function save($user) {
// Save logic
echo "Saving user: " . $user['name'] . "\n";
}
}
// UserService depends on the abstraction (UserRepository), not the concrete implementation
class UserService {
private $userRepository;
public function __construct(UserRepository $userRepository) {
$this->userRepository = $userRepository; // Dependency injected
}
public function saveUser($user) {
$this->userRepository->save($user);
}
}
// Example usage
$userRepository = new JpaUserRepository(); // Low-level module
$userService = new UserService($userRepository); // High-level module depends on abstraction
$userService->saveUser(['name' => 'John Doe']); // Output: Saving user: John Doe
By depending on UserRepository, the UserService can work with any repository implementation.
Abstraction: The UserRepository interface defines the save method. This is the abstraction that allows UserService to interact with any repository without needing to know the specifics of how the repository works.
Low-Level Module: The JpaUserRepository class implements the UserRepository interface and provides the concrete implementation of the save method.
High-Level Module: The UserService class depends on the UserRepository interface rather than a concrete class, making it flexible and decoupled from the implementation details.
Dependency Injection: The UserRepository dependency is passed into the UserService class through its constructor, allowing you to inject different implementations of UserRepository without modifying UserService.
By following these SOLID principles, you ensure your Java code is modular, reusable, and easier to maintain.
Similar Post
SOLID Principles Using PHP
- 22 Jan, 2025
- 3 min read
Master SOLID principles to build resilient, scalable software with maintainable, extensible, and decoupled code that is easier to test and evolve.
SOLID Principles Using Javascript
- 22 Jan, 2025
- 3 min read
Master the SOLID principles in JavaScript to create robust, maintainable, and scalable software. Ensure clean code through single responsibility, extensibility, substitutability, focused interfaces, and effective dependency management.