JavaScript内核与高级编程之:`JavaScript`的`SOLID`原则:其在 `JavaScript` 面向对象编程中的实践。

嘿,各位靓仔靓女,今天咱们聊点硬核的——JavaScript 的 SOLID 原则!别一听“SOLID”就觉得枯燥,这玩意儿其实就像是武林秘籍,掌握了它,你的代码功力就能蹭蹭往上涨,写出来的代码不仅自己看着舒服,别人维护起来也想给你点个赞。

咱们先简单粗暴地解释一下 SOLID 是啥:

  • S – Single Responsibility Principle (单一职责原则)
  • O – Open/Closed Principle (开闭原则)
  • L – Liskov Substitution Principle (里氏替换原则)
  • I – Interface Segregation Principle (接口隔离原则)
  • D – Dependency Inversion Principle (依赖倒置原则)

别慌,一个一个来,保证你听完之后,觉得这玩意儿其实也没那么神秘。

1. 单一职责原则 (Single Responsibility Principle – SRP)

想象一下,你有一个瑞士军刀,既能开瓶盖,又能剪指甲,还能当螺丝刀。听起来很棒,但如果开瓶盖的时候把指甲剪崩了,或者用螺丝刀的时候弄脏了瓶盖,是不是就很尴尬?

单一职责原则就是说,一个类或者一个模块,应该只有一个引起它变化的原因。换句话说,一个类应该只负责一个职责。

错误示范:

class User {
  constructor(name, email) {
    this.name = name;
    this.email = email;
  }

  saveToDatabase() {
    // 保存用户信息到数据库
    console.log(`Saving user ${this.name} to database...`);
  }

  sendWelcomeEmail() {
    // 发送欢迎邮件
    console.log(`Sending welcome email to ${this.email}...`);
  }
}

const user = new User("张三", "[email protected]");
user.saveToDatabase();
user.sendWelcomeEmail();

在这个例子里,User 类既负责管理用户信息,又负责保存用户信息到数据库,还负责发送欢迎邮件。如果修改了数据库连接方式,或者修改了邮件发送逻辑,都需要修改 User 类。这违反了单一职责原则。

正确示范:

class User {
  constructor(name, email) {
    this.name = name;
    this.email = email;
  }
}

class UserRepository {
  save(user) {
    // 保存用户信息到数据库
    console.log(`Saving user ${user.name} to database...`);
  }
}

class EmailService {
  sendWelcomeEmail(email) {
    // 发送欢迎邮件
    console.log(`Sending welcome email to ${email}...`);
  }
}

const user = new User("张三", "[email protected]");
const userRepository = new UserRepository();
const emailService = new EmailService();

userRepository.save(user);
emailService.sendWelcomeEmail(user.email);

现在,User 类只负责管理用户信息,UserRepository 类负责保存用户信息到数据库,EmailService 类负责发送邮件。每个类都只负责一个职责,修改其中一个类不会影响其他类。

好处:

  • 代码更清晰易懂: 每个类都只专注于一个任务,逻辑更简单。
  • 更容易维护: 修改某个功能时,只需要修改对应的类,降低了出错的风险。
  • 可复用性更高: 独立的类更容易在其他地方复用。

2. 开闭原则 (Open/Closed Principle – OCP)

开闭原则的意思是,软件实体(类、模块、函数等等)应该对扩展开放,对修改关闭。也就是说,当需要增加新的功能时,应该通过扩展现有代码来实现,而不是修改现有代码。

错误示范:

class Shape {
  constructor(type, width, height) {
    this.type = type;
    this.width = width;
    this.height = height;
  }

  draw() {
    if (this.type === "rectangle") {
      console.log(`Drawing a rectangle with width ${this.width} and height ${this.height}`);
    } else if (this.type === "circle") {
      console.log(`Drawing a circle with radius ${this.width / 2}`); // 假设 width 是直径
    }
    // 如果要添加新的形状,比如三角形,就需要在这里添加新的 else if 分支
  }
}

const rectangle = new Shape("rectangle", 10, 5);
rectangle.draw();

const circle = new Shape("circle", 8);
circle.draw();

如果我们要添加新的形状,比如三角形,就需要修改 Shape 类的 draw 方法。这违反了开闭原则。

正确示范:

class Shape {
  draw() {
    throw new Error("Method 'draw()' must be implemented.");
  }
}

class Rectangle extends Shape {
  constructor(width, height) {
    super();
    this.width = width;
    this.height = height;
  }

  draw() {
    console.log(`Drawing a rectangle with width ${this.width} and height ${this.height}`);
  }
}

class Circle extends Shape {
  constructor(radius) {
    super();
    this.radius = radius;
  }

  draw() {
    console.log(`Drawing a circle with radius ${this.radius}`);
  }
}

// 添加新的形状,只需要创建一个新的类,继承 Shape 类,并实现 draw 方法
class Triangle extends Shape {
    constructor(base, height) {
        super();
        this.base = base;
        this.height = height;
    }

    draw() {
        console.log(`Drawing a triangle with base ${this.base} and height ${this.height}`);
    }
}

const rectangle = new Rectangle(10, 5);
rectangle.draw();

const circle = new Circle(8);
circle.draw();

const triangle = new Triangle(6, 4);
triangle.draw();

现在,我们定义了一个抽象的 Shape 类,它有一个抽象的 draw 方法。RectangleCircle 类都继承了 Shape 类,并实现了自己的 draw 方法。如果要添加新的形状,只需要创建一个新的类,继承 Shape 类,并实现 draw 方法,不需要修改现有的代码。

好处:

  • 代码更稳定: 不需要修改现有代码,降低了引入 bug 的风险。
  • 更容易扩展: 可以方便地添加新的功能,而不会影响现有功能。

3. 里氏替换原则 (Liskov Substitution Principle – LSP)

里氏替换原则说的是,所有引用父类的地方必须能透明地使用其子类的对象。简单来说,就是子类必须能够替换掉父类,并且程序的功能不会受到影响。

错误示范:

class Rectangle {
  constructor(width, height) {
    this.width = width;
    this.height = height;
  }

  setWidth(width) {
    this.width = width;
  }

  setHeight(height) {
    this.height = height;
  }

  getArea() {
    return this.width * this.height;
  }
}

class Square extends Rectangle {
  constructor(side) {
    super(side, side);
    this.side = side;
  }

  setWidth(width) {
    this.side = width;
    super.setWidth(width);
    super.setHeight(width); // 为了保证正方形,需要同时设置 width 和 height
  }

  setHeight(height) {
    this.side = height;
    super.setWidth(height);
    super.setHeight(height); // 为了保证正方形,需要同时设置 width 和 height
  }
}

function printArea(rectangle) {
  rectangle.setWidth(5);
  rectangle.setHeight(4);
  console.log(`Area: ${rectangle.getArea()}`);
}

const rectangle = new Rectangle(2, 3);
printArea(rectangle); // 输出 Area: 20

const square = new Square(2);
printArea(square); // 输出 Area: 16,与预期不符

在这个例子里,Square 类继承了 Rectangle 类。但是,当我们使用 Square 对象作为 Rectangle 对象传递给 printArea 函数时,结果与预期不符。因为 Square 类的 setWidthsetHeight 方法会同时设置 width 和 height,而 printArea 函数希望分别设置 width 和 height。这违反了里氏替换原则。

正确示范:

class Shape {
  getArea() {
    throw new Error("Method 'getArea()' must be implemented.");
  }
}

class Rectangle extends Shape {
  constructor(width, height) {
    super();
    this.width = width;
    this.height = height;
  }

  getArea() {
    return this.width * this.height;
  }
}

class Square extends Shape {
  constructor(side) {
    super();
    this.side = side;
  }

  getArea() {
    return this.side * this.side;
  }
}

function printArea(shape) {
  console.log(`Area: ${shape.getArea()}`);
}

const rectangle = new Rectangle(5, 4);
printArea(rectangle); // 输出 Area: 20

const square = new Square(4);
printArea(square); // 输出 Area: 16

现在,RectangleSquare 类都继承了 Shape 类,并实现了自己的 getArea 方法。printArea 函数接收一个 Shape 对象,并调用其 getArea 方法。无论传递的是 Rectangle 对象还是 Square 对象,结果都符合预期。

好处:

  • 代码更可靠: 子类可以安全地替换父类,不会导致程序出错。
  • 更容易维护: 可以方便地修改和扩展子类,而不会影响父类的功能。

4. 接口隔离原则 (Interface Segregation Principle – ISP)

接口隔离原则的意思是,客户端不应该被强迫依赖于它不使用的接口。也就是说,应该将大的接口拆分成更小的、更具体的接口,使得客户端只需要依赖于它需要的接口。

错误示范:

class Machine {
  print() {
    throw new Error("Method 'print()' must be implemented.");
  }

  scan() {
    throw new Error("Method 'scan()' must be implemented.");
  }

  fax() {
    throw new Error("Method 'fax()' must be implemented.");
  }
}

class SimplePrinter extends Machine {
  print() {
    console.log("Printing...");
  }

  scan() {
    //  SimplePrinter 不需要 scan 功能,但是必须实现这个方法
    throw new Error("Scan is not supported.");
  }

  fax() {
    // SimplePrinter 不需要 fax 功能,但是必须实现这个方法
    throw new Error("Fax is not supported.");
  }
}

const printer = new SimplePrinter();
printer.print(); // 输出 Printing...
printer.scan(); // 抛出错误 Scan is not supported.

在这个例子里,Machine 类定义了一个大的接口,包含了 printscanfax 三个方法。SimplePrinter 类只需要 print 方法,但是它必须实现 scanfax 方法,即使它不需要这些方法。这违反了接口隔离原则。

正确示范:

interface Printable {
  print(): void;
}

interface Scannable {
  scan(): void;
}

interface Faxable {
  fax(): void;
}

class SimplePrinter implements Printable {
  print() {
    console.log("Printing...");
  }
}

class MultiFunctionPrinter implements Printable, Scannable, Faxable {
  print() {
    console.log("Printing...");
  }

  scan() {
    console.log("Scanning...");
  }

  fax() {
    console.log("Faxing...");
  }
}

const printer = new SimplePrinter();
printer.print(); // 输出 Printing...

const multiFunctionPrinter = new MultiFunctionPrinter();
multiFunctionPrinter.print(); // 输出 Printing...
multiFunctionPrinter.scan(); // 输出 Scanning...
multiFunctionPrinter.fax(); // 输出 Faxing...

现在,我们定义了三个小的接口:PrintableScannableFaxableSimplePrinter 类只需要实现 Printable 接口,MultiFunctionPrinter 类实现了所有三个接口。这样,客户端只需要依赖于它需要的接口,避免了不必要的依赖。

好处:

  • 代码更灵活: 客户端只需要依赖于它需要的接口,可以更容易地进行组合和扩展。
  • 耦合度更低: 客户端和接口之间的耦合度降低,修改接口不会影响客户端。

5. 依赖倒置原则 (Dependency Inversion Principle – DIP)

依赖倒置原则的意思是,高层模块不应该依赖于低层模块,两者都应该依赖于抽象。抽象不应该依赖于细节,细节应该依赖于抽象。

简单来说,就是我们要面向接口编程,而不是面向实现编程。

错误示范:

class LightBulb {
  turnOn() {
    console.log("Light bulb turned on.");
  }

  turnOff() {
    console.log("Light bulb turned off.");
  }
}

class Switch {
  constructor(lightBulb) {
    this.lightBulb = lightBulb;
  }

  on() {
    this.lightBulb.turnOn();
  }

  off() {
    this.lightBulb.turnOff();
  }
}

const lightBulb = new LightBulb();
const switchButton = new Switch(lightBulb);

switchButton.on(); // 输出 Light bulb turned on.
switchButton.off(); // 输出 Light bulb turned off.

在这个例子里,Switch 类直接依赖于 LightBulb 类。如果我们要更换灯泡的类型,比如换成 LED 灯泡,就需要修改 Switch 类。这违反了依赖倒置原则。

正确示范:

interface Switchable {
  turnOn(): void;
  turnOff(): void;
}

class LightBulb implements Switchable {
  turnOn() {
    console.log("Light bulb turned on.");
  }

  turnOff() {
    console.log("Light bulb turned off.");
  }
}

class LEDBulb implements Switchable {
    turnOn() {
        console.log("LED bulb turned on.");
    }

    turnOff() {
        console.log("LED bulb turned off.");
    }
}

class Switch {
  constructor(device) {
    this.device = device;
  }

  on() {
    this.device.turnOn();
  }

  off() {
    this.device.turnOff();
  }
}

const lightBulb = new LightBulb();
const switchButton = new Switch(lightBulb);

switchButton.on(); // 输出 Light bulb turned on.
switchButton.off(); // 输出 Light bulb turned off.

const ledBulb = new LEDBulb();
const switchButton2 = new Switch(ledBulb);

switchButton2.on(); // 输出 LED bulb turned on.
switchButton2.off(); // 输出 LED bulb turned off.

现在,我们定义了一个 Switchable 接口,LightBulb 类和 LEDBulb 类都实现了这个接口。Switch 类依赖于 Switchable 接口,而不是具体的 LightBulb 类。这样,我们可以方便地更换灯泡的类型,而不需要修改 Switch 类。

好处:

  • 代码更灵活: 可以方便地更换依赖,而不需要修改高层模块。
  • 耦合度更低: 高层模块和低层模块之间的耦合度降低,修改低层模块不会影响高层模块。
  • 可测试性更好: 可以使用 mock 对象来测试高层模块,而不需要依赖于具体的低层模块。

总结

原则 描述 目标
单一职责原则 (SRP) 一个类应该只有一个引起它变化的原因。 提高类的内聚性,降低类的复杂度,使其更容易理解和维护。
开闭原则 (OCP) 软件实体应该对扩展开放,对修改关闭。 提高代码的稳定性和可扩展性,减少修改现有代码的风险。
里氏替换原则 (LSP) 所有引用父类的地方必须能透明地使用其子类的对象。 保证继承关系正确,子类可以安全地替换父类,不会导致程序出错。
接口隔离原则 (ISP) 客户端不应该被强迫依赖于它不使用的接口。 提高代码的灵活性和可复用性,降低客户端和接口之间的耦合度。
依赖倒置原则 (DIP) 高层模块不应该依赖于低层模块,两者都应该依赖于抽象。抽象不应该依赖于细节,细节应该依赖于抽象。 提高代码的灵活性和可测试性,降低高层模块和低层模块之间的耦合度。

掌握 SOLID 原则不是一蹴而就的,需要在实践中不断体会和应用。一开始可能会觉得有些困难,但当你看到代码变得更清晰、更易于维护和扩展时,你会发现这些努力都是值得的。

希望今天的讲座能对你有所帮助!记住,写代码就像练武,需要不断练习,才能成为真正的高手!下次再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注