Today I had an interview it didn’t go well … So instead of wallowing about it I decided to proactively learn the “jargon” seeing as I already knew about Design Patterns (over 10 years ago I have read multiple books about the subject) also I have implemented them into practice depending on what I am building most of the time. So it comes naturally for me as I have been coding that long. I’ll chalk it up to the new “process” of finding a job 🙂
Seems like people are so caught up with Best Practices and they loose sight on what’s actually important. SHIP THE CODE. Then refactor and improve later.
Implementing design patterns and practices aren’t always feasible with tight budgets & deadlines on smaller projects. So unit testing isn’t even a choice in most instances.
SOLID principles provide a framework for crafting code that’s modular, easily extendable, and more manageable in the long run. Adhering to these principles supports the development of software that’s resilient, adaptable, and amenable to evolving needs.
In practical applications, it’s valuable to strive for adherence to SOLID principles, but an absolute refusal to modify code can lead to detrimental outcomes in the real world. Overuse of extensive switch statements and intricate if conditions often breaches the Open-Closed principle. It’s not always feasible to strictly adhere to these principles, especially on smaller, time-critical projects.
Five design principles for writing maintainable and scalable software following the SOLID principles. These principles were introduced by Robert C. Martin “Uncle Bob” and are widely used in object-oriented programming to guide developers in creating software that is easy to understand, maintain, and extend. Each letter in the acronym “SOLID” represents one of these principles:
- Single Responsibility Principle (SRP): This principle states that a class should have only one reason to change, meaning it should have only one responsibility or job. This promotes a clean and focused design, making the class easier to understand and maintain.
SRP uses theObserver
pattern. - Open/Closed Principle (OCP): This principle suggests that software entities (classes, modules, functions, etc.) should be open for extension but closed for modification. In other words, you should be able to extend the behavior of a class without modifying its source code.
OCP uses:Strategy
&Decorator
patterns. - Liskov Substitution Principle (LSP): This principle states that objects of a derived class should be able to replace objects of the base class without affecting the correctness of the program. In essence, a derived class should be substitutable for its base class.
LSP uses theFactory Method
pattern. - Interface Segregation Principle (ISP): This principle advocates for creating specific and narrow interfaces for clients, rather than having a single large interface. Clients should not be forced to depend on interfaces they do not use. This promotes decoupling and better maintainability.
ISP uses:Adapter
&Bridge
patterns. - Dependency Inversion Principle (DIP): This principle emphasizes high-level modules should not depend on low-level modules but both should depend on abstractions. It also states that abstractions should not depend on details; rather, details should depend on abstractions. This promotes decoupling and flexibility in the design.
DIP uses:Dependency Injection
(DI),Inversion of Control
(IoC) container.
Cheat sheet (for remembering all this):
- Single Responsibility Principle (SRP):
- Corresponding design pattern: Observer pattern.
- Open/Closed Principle (OCP):
- Corresponding design patterns: Strategy pattern, Decorator pattern.
- Liskov Substitution Principle (LSP):
- Corresponding design pattern: Factory Method pattern.
- Interface Segregation Principle (ISP):
- Corresponding design patterns: Adapter pattern, Bridge pattern.
- Dependency Inversion Principle (DIP):
- Corresponding design pattern: Dependency Injection (DI), Inversion of Control (IoC) container.
More about Design Patterns here.
Code Examples 🤓
Single Responsibility Principle (SRP):
SPR states that a class should have only one reason to change, meaning it should have only one responsibility or job. This principle encourages a clean and focused design where each class is responsible for a specific functionality, making the code easier to understand, maintain, and extend.
class User {
constructor(name, email) {
this.name = name;
this.email = email;
}
getName() {
return this.name;
}
getEmail() {
return this.email;
}
}
class UserDB {
saveUser(user) {
// Logic to save the user to the database
console.log('User saved:', user.getName());
}
deleteUser(user) {
// Logic to delete the user from the database
console.log('User deleted:', user.getName());
}
}
const user = new User('John Doe', 'john@example.com');
const userDB = new UserDB();
userDB.saveUser(user);
userDB.deleteUser(user);
In this example, we have two separate classes: User
and UserDB
. The User
class is responsible for representing a user and providing methods to access user data. The UserDB
class is responsible for saving and deleting users from a database.
By adhering to the Single Responsibility Principle, we ensure that each class has a clear and focused purpose, making the code more maintainable and easier to reason about.
Open/Closed Principle (OCP):
OCP is one of the SOLID principles in object-oriented programming (OOP). It was introduced by Bertrand Meyer in 1988 and is a fundamental concept for writing maintainable and extensible software.
Example. Let’s consider a scenario where we have a Shape
class and we want to calculate the area of different shapes like rectangles and circles, following the OCP.
class Shape {
area() {
throw new Error("This method should be overridden in subclasses");
}
}
class Square extends Shape {
constructor(side) {
super();
this.side = side;
}
area() {
return this.side * this.side;
}
}
class Circle extends Shape {
constructor(radius) {
super();
this.radius = radius;
}
area() {
return Math.PI * this.radius * this.radius;
}
}
Liskov Substitution Principle (LSP):
In this example, Duck
and Penguin
are subclasses of Bird
, and they can be used interchangeably with Bird
without affecting the program’s correctness.
class Bird {
fly() {
console.log("The bird is flying.");
}
}
class Duck extends Bird {
quack() {
console.log("Quack quack!");
}
}
class Penguin extends Bird {
// Penguins cannot fly, so we omit the fly method
}
Interface Segregation Principle (ISP):
In this example, we define separate interfaces for working and eating behaviors, allowing classes to implement only the behaviours they need.
class Worker {
work() {
// Perform work
}
}
class Eater {
eat() {
// Perform eating
}
}
class RobotWorker extends Worker {
work() {
// Perform work as a robot
}
}
class HumanWorker extends Worker, Eater {
work() {
// Perform work as a human
}
eat() {
// Perform eating as a human
}
}
Dependency Inversion Principle (DIP):
In this example, the UserRepository
depends on the Database
abstraction rather than a specific database implementation, following the Dependency Inversion Principle.
class Database {
save(data) {
// Logic to save data to the database
}
}
class UserRepository {
constructor(database) {
this.database = database;
}
saveUser(user) {
this.database.save(user);
}
}
Leave a Reply