likes
comments
collection
share

都 3202 了,你还不知道 SOLID 原则?

作者站长头像
站长
· 阅读数 13

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原则是一组用于面向对象设计的基本原则,它们的目的是提高软件设计的质量和可维护性。

  • 单一职责原则要求每个类只有一个职责;开放封闭原则要求软件实体应该对扩展开放,对修改关闭;
  • 里氏替换原则要求子类能够替换掉它们的父类并保持程序的正确性;
  • 接口隔离原则要求客户端不应该依赖于它们不需要的接口;
  • 依赖倒置原则要求高层模块不应该依赖于低层模块,而是应该依赖于抽象。

遵循这些原则可以使代码更加健壮和可靠。