The 5 SOLID Principles Explained (with PHP & TypeScript examples)
SOLID is an acronym representing five principles that aid developers in creating maintainable and scalable software. These principles, introduced by Robert C. Martin (also known as Uncle Bob), are guidelines that can help improve the quality of your code, reduce the system’s complexity, and make your software easier to understand, modify, and expand.
This article will briefly overview each principle, followed by a PHP and TypeScript code example illustrating each principle.
Single Responsibility Principle (SRP)
The Single Responsibility Principle states that a class should have one, and only one, reason to change. In other words…
Each class should only handle one responsibility.
PHP
Without SRP:
// The "Order" class has either management operations
// as well as order rendering operations.
class Order {
public function calculateTotalSum() {/*...*/}
public function getItems() {/*...*/}
public function getItemCount() {/*...*/}
public function printOrder() {/*...*/}
public function showOrder() {/*...*/}
}
With SRP:
// The "Order" class has only management operations.
class Order {
public function calculateTotalSum() { /*...*/ }
public function getItems() { /*...*/ }
public function getItemCount() { /*...*/ }
}
// The "OrderViewer" class has only rendering operations.
class OrderViewer {
public function printOrder(Order $order) { /*...*/ }
public function showOrder(Order $order) { /*...*/ }
}
TypeScript
Without SRP:
// The "Order" class has either management operations
// as well as order rendering operations.
class Order {
calculateTotalSum() {/*...*/}
getItems() {/*...*/}
getItemCount() {/*...*/}
printOrder() {/*...*/}
showOrder() {/*...*/}
}
With SRP:
// The "Order" class has only management operations.
class Order {
calculateTotalSum() {/*...*/}
getItems() {/*...*/}
getItemCount() {/*...*/}
}
// The "OrderViewer" class has only rendering operations.
class OrderViewer {
printOrder(order: Order) {/*...*/}
showOrder(order: Order) {/*...*/}
}
Open-Closed Principle (OCP)
The Open-Closed Principle states that classes should be open for extension but closed for modification. In essence…
Once you’ve written a class and it’s been tested and working, you should never modify it unless for bug fixes.
PHP
Without OCP:
// The "DiscountCalculator" class has an "always growing"
// or "always changing" calculate() method because it's easy
// that, over time, there will be more customer types or
// the benefits for each type might vary.
class DiscountCalculator {
public function calculate($customerType) {
if ($customerType == 'standard') {
// calculate standard discount
} elseif ($customerType == 'premium') {
// calculate premium discount
}
}
}
With OCP:
// The "DiscountCalculator" class can be defined as an interface
// that means it defines a process that must exist but its
// implementation will be unique for each class implementing it.
// Thus, once implemented, it's less probable to make changes on them.
interface DiscountCalculator {
public function calculate();
}
class StandardDiscountCalculator implements DiscountCalculator {
public function calculate() {
// calculate standard discount
}
}
class PremiumDiscountCalculator implements DiscountCalculator {
public function calculate() {
// calculate premium discount
}
}
TypeScript
Without OCP:
// The "DiscountCalculator" class has an "always growing"
// or "always changing" calculate() method because it's easy
// that, over time, there will be more customer types or
// the benefits for each type might vary.
class DiscountCalculator {
calculate(customerType: string) {
if (customerType == 'standard') {
// calculate standard discount
} else if (customerType == 'premium') {
// calculate premium discount
}
}
}
With OCP:
// The "DiscountCalculator" class can be defined as an interface
// that means it defines a process that must exist but its
// implementation will be unique for each class implementing it.
// Thus, once implemented, it's less probable to make changes on them.
interface DiscountCalculator {
calculate(): void;
}
class StandardDiscountCalculator implements DiscountCalculator {
calculate() {
// calculate standard discount
}
}
class PremiumDiscountCalculator implements DiscountCalculator {
calculate() {
// calculate premium discount
}
}
Liskov Substitution Principle (LSP)
The Liskov Substitution Principle states that objects of a superclass can be replaced with objects of a subclass without breaking the application. In short, the Liskov Substitution Principle…
Relates to the inheritance hierarchy, and is key to achieving the reusability of code.
PHP
Without LSP:
// It is easy to assume that "any" bird (because it's a bird) should
// be able to fly. So, the "Bird" class has a "fly()" method with a
// pre-set implementation that can be generic for all birds. However,
// we can define a bird as an animal that has wings, but not all of
// them can actually fly.
class Bird {
public function fly() { /*...*/ }
}
class Duck extends Bird {}
class Ostrich extends Bird {}
With LSP:
// Similar to the Open-Closed Principle (OCP), it is better to define
// an Interface (instad of implementing a class) that allows us to
// provide a specific implementation for each type of "Bird".
interface Bird {
public function setAltitude($altitude);
}
class FlyingBird implements Bird {
public function setAltitude($altitude) { /*...*/ }
}
class NonFlyingBird implements Bird {
public function setAltitude($altitude) { throw new Exception("Can't fly"); }
}
TypeScript
Without LSP:
// It is easy to assume that "any" bird (because it's a bird) should
// be able to fly. So, the "Bird" class has a "fly()" method with a
// pre-set implementation that can be generic for all birds. However,
// we can define a bird as an animal that has wings, but not all of
// them can actually fly.
class Bird {
fly() {/*...*/}
}
class Duck extends Bird {}
class Ostrich extends Bird {}
With LSP:
// Similar to the Open-Closed Principle (OCP), it is better to define
// an Interface (instad of implementing a class) that allows us to
// provide a specific implementation for each type of "Bird".
interface Bird {
setAltitude(altitude: number): void;
}
class FlyingBird implements Bird {
setAltitude(altitude: number) {/*...*/}
}
class NonFlyingBird implements Bird {
setAltitude(altitude: number) { throw new Error("Can't fly"); }
}
Interface Segregation Principle (ISP)
The Interface Segregation Principle states that clients should not be forced to depend on interfaces they do not use. In other words…
Having many small and specific interfaces is better than one general-purpose interface.
PHP
Without ISP:
// Having a general interface that forces one or more non-needed
// implementations might lead to dirty and unnecessary code, because
// there will be one or more methods that will throw Exceptions.
interface Worker {
public function work();
public function eat();
}
class HumanWorker implements Worker {
public function work() {/*...*/}
public function eat() {/*...*/}
}
class RobotWorker implements Worker {
public function work() {/*...*/}
public function eat() { throw new Exception("I don't eat"); }
}
With ISP:
// Having smaller and individual interfaces allow their specific
// implementations without considering potential dead calls.
// It is also beneficial for type-hinting the input and output arguments.
interface Workable {
public function work();
}
interface Eatable {
public function eat();
}
class HumanWorker implements Workable, Eatable {
public function work() {/*...*/}
public function eat() {/*...*/}
}
class RobotWorker implements Workable {
public function work() {/*...*/}
}
TypeScript
Without ISP:
// Having a general interface that forces one or more non-needed
// implementations might lead to dirty and unnecessary code, because
// there will be one or more methods that will throw Exceptions.
interface Worker {
work(): void;
eat(): void;
}
class HumanWorker implements Worker {
work() {/*...*/}
eat() {/*...*/}
}
class RobotWorker implements Worker {
work() {/*...*/}
eat() { throw new Error("I don't eat"); }
}
With ISP:
// Having smaller and individual interfaces allow their specific
// implementations without considering potential dead calls.
// It is also beneficial for type-hinting the input and output arguments.
interface Workable {
work(): void;
}
interface Eatable {
eat(): void;
}
class HumanWorker implements Workable, Eatable {
work() {/*...*/}
eat() {/*...*/}
}
class RobotWorker implements Workable {
work() {/*...*/}
}
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. Also, abstractions should not depend on details. Details should depend on abstractions. In simple terms…
Don’t rely on concrete classes; rely on their abstractions.
PHP
Without DIP:
// This "PasswordReminder" class relies on the MySQLConnection
// as it might need to use it to retrieve a user to send an email
// with a password recovery link.
// However, the project might rely on different databases, or it might
// change over time, so the "PasswordReminder" class might stop working.
class MySQLConnection {
public function connect() {/*...*/}
}
class PasswordReminder {
private $dbConnection;
public function __construct(MySQLConnection $dbConnection) {
$this->dbConnection = $dbConnection;
}
}
With DIP:
// Having an abstract definition, such as using an Interface,
// allows us to change the injected service through the constructor,
// so today we can rely on Doctrine but tomorrow we can change to ADODB,
// or maybe today it can be MySQL and tomorrow Oracle.
interface Connection {
public function connect();
}
class MySQLConnection implements Connection {
public function connect() {/*...*/}
}
class PasswordReminder {
private $dbConnection;
public function __construct(Connection $dbConnection) {
$this->dbConnection = $dbConnection;
}
}
TypeScript
Without DIP:
// This "PasswordReminder" class relies on the MySQLConnection
// as it might need to use it to retrieve a user to send an email
// with a password recovery link.
// However, the project might rely on different databases, or it might
// change over time, so the "PasswordReminder" class might stop working.
class MySQLConnection {
connect() {/*...*/}
}
class PasswordReminder {
private dbConnection: MySQLConnection;
constructor(dbConnection: MySQLConnection) {
this.dbConnection = dbConnection;
}
}
With DIP:
// Having an abstract definition, such as using an Interface,
// allows us to change the injected service through the constructor,
// so today we can rely on Doctrine but tomorrow we can change to ADODB,
// or maybe today it can be MySQL and tomorrow Oracle.
interface Connection {
connect(): void;
}
class MySQLConnection implements Connection {
connect() {/*...*/}
}
class PasswordReminder {
private dbConnection: Connection;
constructor(dbConnection: Connection) {
this.dbConnection = dbConnection;
}
}
Conclusion
In conclusion, SOLID principles provide guidance for managing dependencies, decoupling software modules, and organizing code in a way that makes systems easier to understand, maintain, and expand.
By following these principles, developers can create systems that are robust, scalable, and less prone to bugs.
Please clap and follow!
👏 Enjoyed this article? Please give it a round of applause by clicking the 👏 button below. Your support means the world to me!
📚 Want to stay updated with my latest posts? Hit the “Follow” button to join my community and never miss out.
Thank you for reading and engaging! Your feedback and support inspire me to share more valuable insights with you. 🙌
Resources
Further reading:
- Uncle Bob’s website: http://cleancoder.com/products
- Robert C. Martin (Wikipedia): https://en.wikipedia.org/wiki/Robert_C._Martin
- SOLID Principles (Wikipedia): https://en.wikipedia.org/wiki/SOLID
Images: