Sure, let's regroup everything from the beginning, covering the SOLID principles with their definitions and JavaScript examples. I'll ensure to use the examples and definitions you previously provided.
### 1. Single Responsibility Principle (SRP)
**Definition:** A class should have one, and only one, reason to change. This means that a class should only have one job or responsibility.
**Example:**
Think of a chef in a restaurant. If a chef's job is to cook food, that's their single responsibility. If the chef starts taking orders, serving tables, and managing inventory all at once, it becomes messy. Just like in coding, a class should have one main purpose, like handling user authentication or managing database connections.
**Bad Example (Violating SRP):**
```javascript
class User {
constructor(username, password, email) {
this.username = username;
this.password = password;
this.email = email;
}
validatePassword(password) {
return this.password === password;
}
sendWelcomeEmail() {
console.log(`Sending welcome email to ${this.email}`);
}
saveToDatabase() {
console.log(`Saving user ${this.username} to the database`);
}
}
// Usage
const user = new User('john_doe', 'securepassword', 'john@example.com');
if (user.validatePassword('securepassword')) {
user.sendWelcomeEmail();
}
user.saveToDatabase();
```
**Good Example (Adhering to SRP):**
```javascript
class User {
constructor(username, password, email) {
this.username = username;
this.password = password;
this.email = email;
}
}
class PasswordValidator {
static validate(user, password) {
return user.password === password;
}
}
class EmailService {
static sendWelcomeEmail(user) {
console.log(`Sending welcome email to ${user.email}`);
}
}
class UserRepository {
static save(user) {
console.log(`Saving user ${user.username} to the database`);
}
}
// Usage
const user = new User('john_doe', 'securepassword', 'john@example.com');
if (PasswordValidator.validate(user, 'securepassword')) {
EmailService.sendWelcomeEmail(user);
}
UserRepository.save(user);
```
### 2. Open/Closed Principle (OCP)
**Definition:** Software entities 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.
**Example:**
Consider a remote control for a TV. If you want to add a new feature, like a volume control for the soundbar, you shouldn't have to open up the remote and modify its circuitry. Instead, you could add an external adapter that extends the functionality without altering the remote itself. In code, this means you can add new features by extending existing classes or modules without modifying their internal code.
**Bad Example (Violating OCP):**
```javascript
class RemoteControl {
turnOnTV() {
console.log('TV is ON');
}
turnOffTV() {
console.log('TV is OFF');
}
increaseVolume() {
console.log('TV volume increased');
}
decreaseVolume() {
console.log('TV volume decreased');
}
// Adding new functionality directly to the class
turnOnSoundbar() {
console.log('Soundbar is ON');
}
turnOffSoundbar() {
console.log('Soundbar is OFF');
}
}
// Usage
const remote = new RemoteControl();
remote.turnOnTV();
remote.turnOnSoundbar();
```
**Good Example (Adhering to OCP):**
```javascript
class RemoteControl {
turnOn() {
console.log('Device is ON');
}
turnOff() {
console.log('Device is OFF');
}
}
class TVRemoteControl extends RemoteControl {
increaseVolume() {
console.log('TV volume increased');
}
decreaseVolume() {
console.log('TV volume decreased');
}
}
class SoundbarRemoteControl extends RemoteControl {
adjustBass(level) {
console.log(`Soundbar bass adjusted to level ${level}`);
}
}
// Usage
const tvRemote = new TVRemoteControl();
tvRemote.turnOn();
tvRemote.increaseVolume();
const soundbarRemote = new SoundbarRemoteControl();
soundbarRemote.turnOn();
soundbarRemote.adjustBass(5);
```
### 3. Liskov Substitution Principle (LSP)
**Definition:** Objects of a superclass should be replaceable with objects of a subclass without altering the correctness of the program. This means that subclasses should behave in a way that doesn't break the expectations set by the superclass.
**Example:**
Imagine you have a toy box where each toy fits into a specific slot. You expect any toy to fit in its corresponding slot without any issues. If you suddenly have a toy that doesn't fit properly, like a round ball in a square hole, it breaks the principle. Similarly, in coding, subclasses should be usable wherever their parent class is expected without causing unexpected behavior.
**Bad Example (Violating LSP):**
```javascript
class Bird {
fly() {
console.log('Bird is flying');
}
}
class Penguin extends Bird {
fly() {
throw new Error('Penguins cannot fly');
}
}
// Usage
function makeBirdFly(bird) {
bird.fly();
}
const bird = new Bird();
const penguin = new Penguin();
makeBirdFly(bird); // Works as expected
makeBirdFly(penguin); // Throws an error: Penguins cannot fly
```
**Good Example (Adhering to LSP):**
```javascript
class Bird {
makeSound() {
console.log('Bird is making a sound');
}
}
class FlyingBird extends Bird {
fly() {
console.log('Flying bird is flying');
}
}
class NonFlyingBird extends Bird {
// No fly method
}
// Now Penguin is a NonFlyingBird, not a FlyingBird
class Penguin extends NonFlyingBird {
swim() {
console.log('Penguin is swimming');
}
}
// Usage
function makeBirdFly(bird) {
if (bird instanceof FlyingBird) {
bird.fly();
} else {
console.log('This bird cannot fly');
}
}
const bird = new FlyingBird();
const penguin = new Penguin();
makeBirdFly(bird); // Works as expected: Flying bird is flying
makeBirdFly(penguin); // Works as expected: This bird cannot fly
```
### 4. Interface Segregation Principle (ISP)
**Definition:** Clients should not be forced to depend on interfaces they do not use. Instead of having large interfaces that cater to multiple clients, it's better to have smaller, more specific interfaces tailored to the needs of individual clients. This principle prevents interface pollution and minimizes coupling between components.
**Example:**
Instead of having a large interface that covers many methods, create smaller, more specific interfaces. This way, classes can implement only the interfaces relevant to them.
**Bad Example (Violating ISP):**
```javascript
class Machine {
start() {
throw new Error('Method not implemented');
}
stop() {
throw new Error('Method not implemented');
}
print() {
throw new Error('Method not implemented');
}
scan() {
throw new Error('Method not implemented');
}
fax() {
throw new Error('Method not implemented');
}
}
class MultiFunctionPrinter extends Machine {
start() {
console.log('Printer starting...');
}
stop() {
console.log('Printer stopping...');
}
print() {
console.log('Printing document...');
}
scan() {
console.log('Scanning document...');
}
fax() {
console.log('Sending fax...');
}
}
class SimplePrinter extends Machine {
start() {
console.log('Printer starting...');
}
stop() {
console.log('Printer stopping...');
}
print() {
console.log('Printing document...');
}
// Methods not used by SimplePrinter
scan() {
throw new Error('SimplePrinter cannot scan');
}
fax() {
throw new Error('SimplePrinter cannot fax');
}
}
// Usage
const printer = new SimplePrinter();
printer.print(); // Works as expected
printer.scan(); // Throws error: SimplePrinter cannot scan
```
**Good Example (Adhering to ISP):**
```javascript
class Startable {
start() {
throw new Error('Method not implemented');
}
}
class Stoppable {
stop() {
throw new Error('Method not implemented');
}
}
class Printable {
print() {
throw new Error('Method not implemented');
}
}
class Scannable {
scan() {
throw new Error('Method not implemented');
}
}
class Faxable {
fax() {
throw new Error('Method not implemented');
}
}
// Now each machine only implements the interfaces it needs
class MultiFunctionPrinter extends Startable, Stoppable, Printable, Scannable, Faxable {
start() {
console.log('Printer starting...');
}
stop() {
console.log('Printer stopping...');
}
print() {
console.log('Printing document...');
}
scan() {
console.log('Scanning document...');
}
fax() {
console.log('Sending fax...');
}
}
class SimplePrinter extends Startable, Stoppable, Printable {
start() {
console.log('Printer starting...');
}
stop() {
console.log('Printer stopping...');
}
print() {
console.log('Printing document...');
}
}
// Usage
const printer = new SimplePrinter();
printer.print(); // Works as expected
```
### 5. Dependency Inversion Principle (DIP)
**Definition:** High-level modules should not depend on low-level modules
. Both should depend on abstractions. Abstractions should not depend on details; details should depend on abstractions. This principle encourages decoupling between modules and promotes flexibility, extensibility, and testability in the codebase.
**Example:**
High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details; details should depend on abstractions. This principle encourages decoupling between modules and promotes flexibility, extensibility, and testability in the codebase.
**Bad Example (Violating DIP):**
```javascript
class MySQLDatabase {
connect() {
console.log('Connecting to MySQL database...');
}
saveUser(user) {
console.log(`Saving user ${user.name} to MySQL database...`);
}
}
class UserService {
constructor() {
this.database = new MySQLDatabase();
}
addUser(user) {
this.database.connect();
this.database.saveUser(user);
}
}
// Usage
const userService = new UserService();
userService.addUser({ name: 'John Doe' });
```
**Good Example (Adhering to DIP):**
```javascript
// Abstraction (interface) for database operations
class Database {
connect() {
throw new Error('Method not implemented');
}
saveUser(user) {
throw new Error('Method not implemented');
}
}
// MySQLDatabase implements the Database interface
class MySQLDatabase extends Database {
connect() {
console.log('Connecting to MySQL database...');
}
saveUser(user) {
console.log(`Saving user ${user.name} to MySQL database...`);
}
}
// UserService depends on the Database abstraction
class UserService {
constructor(database) {
this.database = database;
}
addUser(user) {
this.database.connect();
this.database.saveUser(user);
}
}
// Usage
const mySQLDatabase = new MySQLDatabase();
const userService = new UserService(mySQLDatabase);
userService.addUser({ name: 'John Doe' });
// We can easily switch to another database implementation without modifying UserService
class MongoDBDatabase extends Database {
connect() {
console.log('Connecting to MongoDB database...');
}
saveUser(user) {
console.log(`Saving user ${user.name} to MongoDB database...`);
}
}
const mongoDBDatabase = new MongoDBDatabase();
const userServiceMongo = new UserService(mongoDBDatabase);
userServiceMongo.addUser({ name: 'Jane Doe' });
```
By understanding and applying the SOLID principles, you can create software that is more modular, maintainable, and scalable.
Comments
Post a Comment