发布于 2025-03-06 30 阅读
0

SOLID - 简单易懂

嗨!你好吗?你还好吗?希望你还好!

今天我要谈论一个每个人都在谈论或写到的主题。但有时很难理解每一个原则。我说的是 SOLID。

当我问起 SOLID 时,很多人可能总是记得第一个原则(单一职责原则)。但当我问起另一个原则时,有些人不记得了,或者觉得很难解释。我明白。

确实,如果不编写代码或不重述每个原则的定义,解释起来会很困难。但在本文中,我想以简单的方式介绍每个原则。所以我将使用 Typescript 来举例说明。

那么我们开始吧!

单一职责原则 - SRP

更容易理解和记住的原则。
当我们编码时,很容易识别我们何时忘记了原则。

假设我们有一个 TaskManager 类:

class TaskManager {
  constructor() {}
  connectAPI(): void {}
  createTask(): void {
    console.log("Create Task");
  }
  updateTask(): void {
    console.log("Update Task");
  }
  removeTask(): void {
    console.log("Remove Task");
  }
  sendNotification(): void {
    console.log("Send Notification");
  }
  sendReport(): void {
    console.log("Send Report");
  }
}

好吧!你可能注意到了这个问题,不是吗?
TaskManager 类有很多不属于她自己的职责。例如:sendNotification 和 sendReport 方法。

现在,让我们重新实施并应用该解决方案:

class APIConnector {
  constructor() {}
  connectAPI(): void {}
}

class Report {
  constructor() {}
  sendReport(): void {
    console.log("Send Report");
  }
}

class Notificator {
  constructor() {}
  sendNotification(): void {
    console.log("Send Notification");
  }
}

class TaskManager {
  constructor() {}
  createTask(): void {
    console.log("Create Task");
  }
  updateTask(): void {
    console.log("Update Task");
  }
  removeTask(): void {
    console.log("Remove Task");
  }
}

很简单,不是吗?我们只需将通知和报告分离到指定的类中。现在我们遵守单一原则职责!

定义:Each class must have one, and only one, reason to change

开放封闭原则-OCP

第二个原则。我认为也很容易理解。给你一个提示,如果你注意到在某种方法中有很多条件来验证某件事,那么你可能是 OCP 的受害者。

让我们想象一下以下考试类的例子:

type ExamType = {
  type: "BLOOD" | "XRay";
};

class ExamApprove {
  constructor() {}
  approveRequestExam(exam: ExamType): void {
    if (exam.type === "BLOOD") {
      if (this.verifyConditionsBlood(exam)) {
        console.log("Blood Exam Approved");
      }
    } else if (exam.type === "XRay") {
      if (this.verifyConditionsXRay(exam)) {
        console.log("XRay Exam Approved!");
      }
    }
  }

  verifyConditionsBlood(exam: ExamType): boolean {
    return true;
  }
  verifyConditionsXRay(exam: ExamType): boolean {
    return false;
  }
}

是的,你可能已经看过这段代码好几次了。首先,我们打破了第一原则 SRP,并制定了许多条件。

现在想象一下,如果出现另一种检查,比如超声波。我们需要增加另一种方法来验证,增加另一种条件。

让我们重新回顾一下这段代码:

type ExamType = {
  type: "BLOOD" | "XRay";
};

interface ExamApprove {
  approveRequestExam(exam: NewExamType): void;
  verifyConditionExam(exam: NewExamType): boolean;
}

class BloodExamApprove implements ExamApprove {
  approveRequestExam(exam: ExamApprove): void {
    if (this.verifyConditionExam(exam)) {
      console.log("Blood Exam Approved");
    }
  }
  verifyConditionExam(exam: ExamApprove): boolean {
    return true;
  }
}

class RayXExamApprove implements ExamApprove {
  approveRequestExam(exam: ExamApprove): void {
    if (this.verifyConditionExam(exam)) {
      console.log("RayX Exam Approved");
    }
  }
  verifyConditionExam(exam: NewExamType): boolean {
    return true;
  }
}

哇,好多了!现在,如果出现另一种考试类型,我们只需实现接口ExamApprove。如果出现另一种考试验证类型,我们只需更新接口。

定义:Software entities (such as classes and methods) must be open for extension but closed for modification

里氏替换原则 - LSP

理解和解释起来比较复杂。但是我所说的,会让你更容易理解。

假设你有一所大学和两种类型的学生:学生和研究生。

class Student {
  constructor(public name: string) {}

  study(): void {
    console.log(`${this.name} is studying`);
  }

  deliverTCC() {
    /** Problem: Post graduate Students don't delivery TCC */
  }
}

class PostgraduateStudent extends Student {
  study(): void {
    console.log(`${this.name} is studying and searching`);
  }
}

我们这里有一个问题,我们正在延长学生学业,但研究生不需要提交 TCC。他只需学习和搜索。

那我们该如何解决这个问题呢?很简单!我们创建一个 Student 类,并将毕业的学生和毕业后的学生区分开来:

class Student {
  constructor(public name: string) {}

  study(): void {
    console.log(`${this.name} is studying`);
  }
}

class StudentGraduation extends Student {
  study(): void {
    console.log(`${this.name} is studying`);
  }

  deliverTCC() {}
}

class StudentPosGraduation extends Student {
  study(): void {
    console.log(`${this.name} is studying and searching`);
  }
}

现在我们有了更好的方法来分离各自的职责。这个原则的名字可能很吓人,但其原理很简单。

定义:Derived classes (or child classes) must be able to replace their base classes (or parent classes)

接口隔离原则 - ISP

要理解这个原则,诀窍就是记住定义。不应强迫类实现不会使用的方法。

想象一下,你有一个类,它实现了一个永远不会被使用的接口。

假设有一家商店,有一位卖家和一位接待员。卖家和接待员都有工资,但只有卖家有佣金。

让我们看看问题所在:

interface Employee {
  salary(): number;
  generateCommission(): void;
}

class Seller implements Employee {
  salary(): number {
    return 1000;
  }
  generateCommission(): void {
    console.log("Generating Commission");
  }
}

class Receptionist implements Employee {
  salary(): number {
    return 1000;
  }
  generateCommission(): void {
    /** Problem: Receptionist don't have commission  */
  }
}

两者都实现了 Employee 接口,但是接待员没有佣金。所以我们被迫实现一个永远不会使用的方法。

因此解决方案是:

interface Employee {
  salary(): number;
}

interface Commissionable {
  generateCommission(): void;
}

class Seller implements Employee, Commissionable {
  salary(): number {
    return 1000;
  }

  generateCommission(): void {
    console.log("Generating Commission");
  }
}

class Receptionist implements Employee {
  salary(): number {
    return 1000;
  }
}

很简单!现在我们有两个接口!雇主类和佣金接口。现在只有卖家会实现两个接口,它们会收取佣金。接待员不仅会实现员工。因此,接待员不必被迫实现永远不会使用的方法。

定义:A class should not be forced to implement interfaces and methods that will not be used.

依赖倒置原则-DIP

最后一个!听名字你可能觉得很难记住!但你可能每次都见过这个原则。

假设您有一个与 Repository 类集成的服务类,该类将调用数据库,例如 Postgress。但是,如果存储库类发生变化,数据库也会更改为 MongoDB。

我们来看一个例子:

interface Order {
  id: number;
  name: string;
}

class OrderRepository {
  constructor() {}
  saveOrder(order: Order) {}
}

class OrderService {
  private orderRepository: OrderRepository;

  constructor() {
    this.orderRepository = new OrderRepository();
  }

  processOrder(order: Order) {
    this.orderRepository.saveOrder(order);
  }
}

我们注意到,存储库 OrderService 类与 OrderRepository 类的具体实现直接耦合。

让我们重新回顾一下这个例子:

interface Order {
  id: number;
  name: string;
}

class OrderRepository {
  constructor() {}
  saveOrder(order: Order) {}
}

class OrderService {
  private orderRepository: OrderRepository;

  constructor(repository: OrderRepository) {
    this.orderRepository = repository;
  }

  processOrder(order: Order) {
    this.orderRepository.saveOrder(order);
  }
}

很好!好多了!现在我们在构造函数中接收存储库作为参数来实例化和使用。现在我们依赖于抽象,我们不需要知道我们正在使用哪个存储库。

定义:depend on abstractions rather than concrete implementations

精加工

那么你现在感觉如何?我希望通过这些简单的例子,你可以记住并理解在代码中使用这些原则的原因和内容。它使理解和扩展变得更容易,此外你正在应用干净的代码。