Last updated on September 18th, 2023
Introduction
SOLID principles are fundamental concepts for building maintainable and scalable object-oriented software. In this comprehensive guide, we will explore implementing the SOLID principles in TypeScript through concrete examples.
Introduction to SOLID Principles
SOLID is an acronym representing five key design principles:
- S – Single Responsibility Principle
- O – Open/Closed Principle
- L – Liskov Substitution Principle
- I – Interface Segregation Principle
- D – Dependency Inversion Principle
These principles encourage modular, loosely coupled code that is resilient to change. Let’s look at each principle individually and how to apply it in TypeScript.
Single Responsibility Principle (SRP)
SRP states that a class or function should only have one reason to change. It should focus on a single functionality.
For example, a UserHandler class with multiple responsibilities:
class UserHandler {
// 1. Get user data
async getUser(id) {
// db fetch
}
// 2. Format display
formatUser(user) {
// format
}
// 3. Save user
saveUser(user) {
// save
}
}
This can be refactored into multiple classes with unique responsibilities:
class UserRepository {
async getUser(id) {}
async saveUser(user) {}
}
class UserFormatter {
formatUser(user) {}
}
Each class now has a single responsibility.
Open/Closed Principle (OCP)
OCP states classes should be open for extension but closed for modification. Behavior can be extended without modifying existing code.
Instead of modifying code to add validation:
class UserService {
saveUser(user) {
// Validate
// Save
}
}
We inject validators through the constructor using abstraction:
interface Validator {
validate(user);
}
class UserService {
constructor(private validator: Validator) {}
saveUser(user) {
this.validator.validate(user);
// Save
}
}
This allows adding new validation without changing UserService
.
Liskov Substitution Principle (LSP)
LSP specifies that subclasses should be substitutable for their parents without altering correctness.
Let’s define an abstract TemperatureSensor class:
abstract class TemperatureSensor {
abstract getTemperature();
}
class CelsiusSensor extends TemperatureSensor {
getTemperature() {
// returns temperature in Celsius
}
}
Since CelsiusSensor
maintains the parent class contract, we can substitute it cleanly:
let sensor: TemperatureSensor = new CelsiusSensor();
This ensures substitutability through abstraction.
Interface Segregation Principle (ISP)
ISP states interfaces should be broken down into smaller, specific ones rather than monolithic interfaces.
Instead of a large printer interface:
interface Printer {
print(document);
fax(document);
scan(document);
}
We can break it into separate interfaces:
interface Printer {
print(document);
}
interface Fax {
fax(document);
}
interface Scanner {
scan(document);
}
Now classes can implement only functionality needed.
Dependency Inversion Principle (DIP)
DIP states high-level code should not depend on low-level details. Depend on abstractions rather than concretions.
Instead of:
class PasswordReminder {
private db = new PostgreSQLDatabase();
// ...
}
We depend on abstraction:
interface DBClient {
// ...
}
class PasswordReminder {
constructor(private db: DBClient) {}
}
This decouples the components through an interface.
Frequently Asked Questions
Q: What is the single responsibility principle and how does it help with maintainability?
A: The single responsibility principle states that a class or function should have only one reason to change. This separation of concerns makes code more maintainable and modular.
Q: How can the open/closed principle be implemented using abstraction and interfaces?
A: The open/closed principle can be implemented by using interfaces and abstract classes to provide common APIs that can have multiple concrete implementations. This allows extending behavior without modifying existing code.
Q: What is the Liskov substitution principle and how does it relate to subclassing?
A: The Liskov substitution principle states that subclasses should be substitutable for their parent classes. To follow this, subclasses must uphold their parent class contracts and invariants.
Q: How does the interface segregation principle help prevent bloated interfaces?
A: The interface segregation principle suggests that interfaces should be broken down into smaller, focused interfaces rather than monolithic interfaces. This avoids forcing implementations to depend on and implement unnecessary methods.
Q: What is dependency inversion and how does it decouple code?
A: Dependency inversion involves depending on abstractions rather than concrete details and implementations. This decouples code by ensuring modules depend only on generic interfaces.
Q: How do SOLID principles encourage loose coupling?
A: Principles like single responsibility, interface segregation and dependency inversion promote abstraction, well-defined interfaces, and limited dependencies between modules. This enables changing one module without impacting others.
Q: How can SOLID principles make code more resilient to change?
A: SOLID principles modularize classes and functions, reducing interdependencies. This segmentation into focused units prevents ripple effects from code changes.
Q: Which SOLID principles relate to using abstraction and interfaces?
A: The interface segregation principle and dependency inversion principle specifically promote the use of interfaces and abstraction to reduce coupling. The open/closed principle also leverages interfaces for extending behavior.
Q: What are some examples applying SOLID principles in TypeScript?
A: Examples include separating classes by responsibility, using abstract classes and interfaces for loose coupling, avoiding oversized interfaces, and constructing objects with interfaces rather than concretions.
Q: How can SOLID principles help scale application complexity?
A: SOLID principles allow complexity to be managed incrementally by segmenting monoliths into well-defined units with singular purposes that can be maintained independently. This makes growing codebases more adaptable and scalable.
Conclusion
SOLID principles encourage building robust, maintainable object-oriented code by promoting high cohesion, loose coupling, and well-defined abstractions. This guide provided examples demonstrating applying SOLID principles like single responsibility, open/closed, and dependency inversion in TypeScript. These principles are key for scaling application complexity and managing change over time. I hope this overview helps explain SOLID in a beginner-friendly way. Let me know if you have any other questions!