Single Responsibility Principle (SRP)
The Single Responsibility Principle states that a class should have one, and only one, reason to change. This means that each class should have only one job or responsibility. This principle supports separation of concerns in a software system. When a class takes on more than one responsibility, it becomes coupled, potentially leading to complications when changes need to be made.
Let's take a look at a basic example in PHP:
class Book {
private $title;
private $author;
public function __construct($title, $author) {
$this->title = $title;
$this->author = $author;
}
public function getTitle() {
return $this->title;
}
public function getAuthor() {
return $this->author;
}
public function printBook() {
// print logic here
}
public function save() {
// save logic here
}
}
In this example, the Book
class violates the Single Responsibility Principle because it has two reasons to change: one for book-related properties (like title and author) and another for actions related to book management (printing and saving).
A better design would be to separate these responsibilities into different classes:
class Book {
private $title;
private $author;
public function __construct($title, $author) {
$this->title = $title;
$this->author = $author;
}
public function getTitle() {
return $this->title;
}
public function getAuthor() {
return $this->author;
}
}
class BookPrinter {
public function printBook(Book $book) {
// print logic here
}
}
class BookSaver {
public function saveBook(Book $book) {
// save logic here
}
}
In the refactored code, the Book
class is only concerned with book-related properties. The BookPrinter class handles printing books, and the BookSaver class handles saving books. Each class now has a single responsibility, making them easier to maintain and understand.
Open-Closed Principle (OCP)
The Open-Closed Principle states that software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification. This means that the behavior of a module can be extended without modifying its source code. This principle is key to achieving a good level of flexibility and reusability in code.
Consider this basic example in PHP:
class Rectangle {
public $width;
public $height;
public function __construct($width, $height) {
$this->width = $width;
$this->height = $height;
}
}
class AreaCalculator {
public function calculate($rectangle) {
return $rectangle->width * $rectangle->height;
}
}
In this example, if we want to calculate the area of different types of shapes (like a circle), we have to modify the AreaCalculator
class. This violates the Open-Closed Principle.
A better design would be to create an interface (abstract class) that can be implemented by any shape:
interface Shape {
public function area();
}
class Rectangle implements Shape {
private $width;
private $height;
public function __construct($width, $height) {
$this->width = $width;
$this->height = $height;
}
public function area() {
return $this->width * $this->height;
}
}
class Circle implements Shape {
private $radius;
public function __construct($radius) {
$this->radius = $radius;
}
public function area() {
return pi() * pow($this->radius, 2);
}
}
class AreaCalculator {
public function calculate(Shape $shape) {
return $shape->area();
}
}
Now, AreaCalculator
is closed for modification but open for extension. If we need to add a new shape, we just create a new class for that shape implementing the Shape interface. This way, we don't need to change the AreaCalculator class every time we add a new shape, following the Open-Closed Principle.
Liskov Substitution Principle (LSP)
The Liskov Substitution Principle states that if a program is using a base class, then the reference to the base class can be replaced with a derived class without affecting the functionality of the program. Essentially, derived classes must be substitutable for their base classes without causing issues.
Consider this basic example in PHP:
class Bird {
public function fly() {
// fly logic here
}
}
class Penguin extends Bird {
public function fly() {
// Penguins can't fly!
throw new Exception("Can't fly");
}
}
In this example, the Penguin
class is a subtype of Bird
, but it can't use the fly()
method that it inherited from Bird
. This violates the Liskov Substitution Principle because a Penguin
is not a fully substitutable for a Bird
.
A better design would be to separate the behaviors into different classes:
class Bird {
public function eat() {
// eat logic here
}
}
class FlyingBird extends Bird {
public function fly() {
// fly logic here
}
}
class Penguin extends Bird {
// Penguins can't fly but they can eat
}
In the refactored code, the Bird
class doesn't include the fly()
method, so Penguin
doesn't inherit a method that it can't use. Instead, a new FlyingBird
class extends Bird
and includes the fly()
method, which can be used by any bird that can fly. Now, any subtype of Bird
can be substituted for a Bird without causing any issues, adhering to the Liskov Substitution Principle.
Interface Segregation Principle (ISP)
The Interface Segregation Principle states that no client should be forced to depend on interfaces they do not use. This means that a class should not have to implement methods it doesn't use. Instead of one big interface, many small interfaces are preferred based on groups of methods, each one serving one submodule.
Consider this basic example in PHP:
interface Worker {
public function work();
public function eat();
}
class HumanWorker implements Worker {
public function work() {
// work logic here
}
public function eat() {
// eat logic here
}
}
class RobotWorker implements Worker {
public function work() {
// work logic here
}
public function eat() {
// Robots can't eat!
throw new Exception("Can't eat");
}
}
In this example, the RobotWorker
class is forced to implement a method (eat()
) that it doesn't use, violating the Interface Segregation Principle.
A better design would be to separate the behaviors into different interfaces:
interface Workable {
public function work();
}
interface Eatable {
public function eat();
}
class HumanWorker implements Workable, Eatable {
public function work() {
// work logic here
}
public function eat() {
// eat logic here
}
}
class RobotWorker implements Workable {
public function work() {
// work logic here
}
}
In the refactored code, the Workable
and Eatable
interfaces are separate, so the RobotWorker
only needs to implement Workable
and doesn't need to implement a method (eat()
) that it doesn't use. This adheres to the Interface Segregation Principle, reducing the potential for unnecessary complexity in the code.
Dependency Inversion Principle (DIP)
The Dependency Inversion Principle states that high-level modules should not depend on low-level modules. Both should depend on abstractions. Additionally, abstractions should not depend on details. Details should depend on abstractions. This principle leads to a decoupled system, making it more flexible and adaptable to future changes.
Consider this basic example in PHP:
class MySQLConnection {
public function connect() {
// MySQL connection logic here
}
}
class PasswordReminder {
private $dbConnection;
public function __construct(MySQLConnection $dbConnection) {
$this->dbConnection = $dbConnection;
}
}
In this example, the PasswordReminder
class is tightly coupled to the MySQLConnection
class. If we want to change the database technology (for example, to PostgreSQL), we would have to modify the PasswordReminder
class. This violates the Dependency Inversion Principle.
A better design would be to create an interface that any database connection class can implement:
interface DBConnectionInterface {
public function connect();
}
class MySQLConnection implements DBConnectionInterface {
public function connect() {
// MySQL connection logic here
}
}
class PostgreSQLConnection implements DBConnectionInterface {
public function connect() {
// PostgreSQL connection logic here
}
}
class PasswordReminder {
private $dbConnection;
public function __construct(DBConnectionInterface $dbConnection) {
$this->dbConnection = $dbConnection;
}
}
In the refactored code, the PasswordReminder
class depends on the DBConnectionInterface
abstraction, not a concrete class. So, if we want to change the database technology, we just need to create a new class that implements DBConnectionInterface
, and PasswordReminder
doesn't need to change. This design follows the Dependency Inversion Principle, making the code more flexible and easier to modify.