都 3202 了,你还不知道 SOLID 原则?
SOLID原则是一组面向对象设计(Object Oriented Design)的基本原则,它们旨在提高软件设计的质量和可维护性。这些原则是由Robert C. Martin在他的书籍《Agile Software Development: Principles, Patterns, and Practices》中提出的。他强调了在开发软件过程中,应该遵循这些原则,这样可以保证软件具有稳定性、可扩展性和可重用性。下面分别介绍SOLID原则的五个部分。
单一职责原则(Single Responsibility Principle, SRP)
单一职责原则指的是每个类应该有一个明确的职责,而且该职责应该被完全封装在该类的内部。这可以防止代码的耦合并增加代码的可重用性。如果一个类承担了过多的职责,它会变得复杂且难以维护。因此,每个类应该只有一个理由去改变。
// 不遵循单一职责原则的代码
class Customer {
constructor(private name: string, private age: number) {}
getName(): string {
return this.name;
}
getAge(): number {
return this.age;
}
saveToDatabase(): void {
// 保存用户数据到数据库
}
sendEmail(): void {
// 发送邮件
}
}
// 遵循单一职责原则的代码
class Customer {
constructor(private name: string, private age: number) {}
getName(): string {
return this.name;
}
getAge(): number {
return this.age;
}
}
class CustomerRepository {
saveToDatabase(customer: Customer): void {
// 保存用户数据到数据库
}
}
class EmailService {
sendEmail(customer: Customer): void {
// 发送邮件
}
}
在第一个示例中,Customer类不仅负责获取客户的姓名和年龄信息,还负责将客户信息保存到数据库和发送电子邮件。这种做法违反了单一职责原则。在第二个示例中,数据存储和电子邮件服务被分别封装成了CustomerRepository和EmailService类。每个类只有一个职责,这样可以提高代码的可维护性和可重用性。
开放封闭原则(Open-Closed Principle, OCP)
开放封闭原则指的是一个类应该是可扩展但不可修改的。这意味着在添加新功能的时候,不需要修改现有的代码,而是通过扩展现有的代码来实现。这样可以有效地避免修改代码时产生的问题,例如引入新的bug等。
// 不遵循开放封闭原则的代码
class Rectangle {
constructor(private width: number, private height: number) {}
getWidth(): number {
return this.width;
}
setWidth(width: number): void {
this.width = width;
}
getHeight(): number {
return this.height;
}
setHeight(height: number): void {
this.height = height;
}
}
class Circle {
constructor(private radius: number) {}
getRadius(): number {
return this.radius;
}
}
class AreaCalculator {
calculateArea(shape: any): number {
if (shape instanceof Rectangle) {
return shape.getWidth() * shape.getHeight();
} else if (shape instanceof Circle) {
return 3.14 * shape.getRadius() ** 2;
}
}
}
// 遵循开放封闭原则的代码
interface Shape {
calculateArea(): number;
}
class Rectangle implements Shape {
constructor(private width: number, private height: number) {}
calculateArea(): number {
return this.width * this.height;
}
}
class Circle implements Shape {
constructor(private radius: number) {}
calculateArea(): number {
return 3.14 * this.radius ** 2;
}
}
class AreaCalculator {
calculateArea(shape: Shape): number {
return shape.calculateArea();
}
}
在第一个示例中,AreaCalculator类需要根据不同的形状进行面积计算,但是这个类需要在每个新的形状上进行修改。这种做法违反了开放封闭原则。在第二个示例中,Shape接口作为一个公共接口,Rectangle和Circle类都实现了该接口,并实现自己的计算面积方法。这样,我们只需要向AreaCalculator传递一个Shape对象,它就可以计算出面积。
里氏替换原则(Liskov Substitution Principle, LSP)
里氏替换原则指的是,一个派生类可以在程序的任何一处对其基类进行替换。这意味着派生类不应该破坏父类所定义的特定行为。只有当子类在本质上是父类的子集时,才能满足这个原则。
// 不遵循里氏替换原则的代码
class Rectangle {
constructor(private width: number, private height: number) {}
getWidth(): number {
return this.width;
}
setWidth(width: number): void {
this.width = width;
}
getHeight(): number {
return this.height;
}
setHeight(height: number): void {
this.height = height;
}
getArea(): number {
return this.width * this.height;
}
}
class Square extends Rectangle {
constructor(private size: number) {
super(size, size);
}
setWidth(width: number): void {
this.size = width;
this.width = width;
this.height = width;
}
setHeight(height: number): void {
this.size = height;
this.width = height;
this.height = height;
}
}
// 遵循里氏替换原则的代码
interface Shape {
getArea(): number;
}
class Rectangle implements Shape {
constructor(private width: number, private height: number) {}
getWidth(): number {
return this.width;
}
setWidth(width: number): void {
this.width = width;
}
getHeight(): number {
return this.height;
}
setHeight(height: number): void {
this.height = height;
}
getArea(): number {
return this.width * this.height;
}
}
class Square implements Shape {
constructor(private size: number) {}
getSize(): number {
return this.size;
}
setSize(size: number): void {
this.size = size;
}
getArea(): number {
return this.size ** 2;
}
}
class AreaCalculator {
calculateArea(shape: Shape): number {
return shape.getArea();
}
}
在第一个示例中,Square类继承Rectangle类,并重写了setWidth和setHeight方法,这导致Square对象不能完全替换Rectangle对象。在第二个示例中,Square类不再继承Rectangle类,而是实现Shape接口,并自己实现自己的getArea方法。因此,Square对象可以完全替换Rectangle对象,这符合里氏替换原则。
接口隔离原则(Interface Segregation Principle, ISP)
接口隔离原则指的是,类的实现方应当只需要实现自己需要的那部分接口。这样可以避免客户端依赖于无关的实现细节,从而提高代码的可重用性和可维护性。
// 不遵循接口隔离原则的代码
interface Vehicle {
startEngine(): void;
stopEngine(): void;
accelerate(): void;
brake(): void;
changeGear(): void;
}
class Car implements Vehicle {
startEngine(): void {
console.log("启动汽车引擎");
}
stopEngine(): void {
console.log("关闭汽车引擎");
}
accelerate(): void {
console.log("加速汽车");
}
brake(): void {
console.log("刹车汽车");
}
changeGear(): void {
console.log("变速器换挡");
}
}
class Motorcycle implements Vehicle {
startEngine(): void {
console.log("启动摩托车引擎");
}
stopEngine(): void {
console.log("关闭摩托车引擎");
}
accelerate(): void {
console.log("加速摩托车");
}
brake(): void {
console.log("刹车摩托车");
}
changeGear(): void {
console.log("变速器换挡");
}
}
// 遵循接口隔离原则的代码
interface Startable {
startEngine(): void;
stopEngine(): void;
}
interface Drivable {
accelerate(): void;
brake(): void;
changeGear(): void;
}
class Car implements Startable, Drivable {
startEngine(): void {
console.log("启动汽车引擎");
}
stopEngine(): void {
console.log("关闭汽车引擎");
}
accelerate(): void {
console.log("加速汽车");
}
brake(): void {
console.log("刹车汽车");
}
changeGear(): void {
console.log("变速器换挡");
}
}
class Motorcycle implements Startable, Drivable {
startEngine(): void {
console.log("启动摩托车引擎");
}
stopEngine(): void {
console.log("关闭摩托车引擎");
}
accelerate(): void {
console.log("加速摩托车");
}
brake(): void {
console.log("刹车摩托车");
}
changeGear(): void {
console.log("变速器换挡");
}
}
在第一个示例中,Vehicle接口定义了所有交通工具都应该具备的方法,但是这些方法对于汽车和摩托车来说不是全部都需要的,这违反了接口隔离原则。在第二个示例中,将Vehicle接口拆分成了Startable和Drivable两个接口,根据不同的交通工具分别实现不同的接口。这样,汽车和摩托车只需要依赖于它们需要的接口,而不是全部的方法。
依赖倒置原则(Dependency Inversion Principle, DIP)
依赖倒置原则指的是,高层模块不应该依赖于低层模块,而是应该依赖于抽象。依赖倒置原则要求高层模块不应该依赖于低层模块,而是应该依赖于抽象。这可以通过使用接口或抽象类来实现。这样可以减少代码的耦合度,从而提高代码的可维护性和可扩展性。
// 不遵循依赖倒置原则的代码
class Database {
connect(): void {
console.log("连接到数据库");
}
disconnect(): void {
console.log("从数据库断开连接");
}
query(sql: string): void {
console.log(`执行查询 ${sql}`);
}
}
class ProductService {
private database: Database;
constructor() {
this.database = new Database();
}
getProductById(id: number): void {
this.database.connect();
this.database.query(`SELECT * FROM products WHERE id = ${id}`);
this.database.disconnect();
}
}
// 遵循依赖倒置原则的代码
interface Database {
connect(): void;
disconnect(): void;
query(sql: string): void;
}
class MySQLDatabase implements Database {
connect(): void {
console.log("连接到MySQL数据库");
}
disconnect(): void {
console.log("从MySQL数据库断开连接");
}
query(sql: string): void {
console.log(`在MySQL数据库中执行查询 ${sql}`);
}
}
class ProductService {
private database: Database;
constructor(database: Database) {
this.database = database;
}
getProductById(id: number): void {
this.database.connect();
this.database.query(`SELECT * FROM products WHERE id = ${id}`);
this.database.disconnect();
}
}
在第一个示例中,ProductService类依赖于Database类,这违反了依赖倒置原则。在第二个示例中,Database类被抽象为一个接口,并定义了connect、disconnect和query方法。MySQLDatabase类实现了Database接口,并实现自己的connect、disconnect和query方法。ProductService类的构造函数接收一个Database对象,并使用它来查询产品。这样,ProductService类不再依赖于具体的数据库实现,而是依赖于抽象的Database接口。
总结
SOLID原则是一组用于面向对象设计的基本原则,它们的目的是提高软件设计的质量和可维护性。
- 单一职责原则要求每个类只有一个职责;开放封闭原则要求软件实体应该对扩展开放,对修改关闭;
- 里氏替换原则要求子类能够替换掉它们的父类并保持程序的正确性;
- 接口隔离原则要求客户端不应该依赖于它们不需要的接口;
- 依赖倒置原则要求高层模块不应该依赖于低层模块,而是应该依赖于抽象。
遵循这些原则可以使代码更加健壮和可靠。
转载自:https://juejin.cn/post/7239173848549457977