【翻】SOLID Principles in JavaScript

为了提高英语水平和保持技术成长,开始按计划翻译一些短篇和博客,有问题欢迎讨论👻
原文:SOLID Principles in JavaScript
原作者:roniee

正文

SOLID原则是由Robert C. Martin提出的一套软件设计原则。这些原则指导开发人员构建出健壮的、可维护的应用程序,同时将维护的成本降到最低。在这篇文章中,我们将讨论如何在JavaScript中使用这些原则,并会展示一些代码示例

SOLID原则是由Robert C. Martin. “Bob叔叔” 提出的一套软件设计原则。这些原则指导开发人员构建出健壮的、可维护的应用程序,同时将维护的成本降到最低。

尽管SOLID原则经常被用于面向对象的编程,但我们可以将其用于其他语言,比如JavaScript。在这篇文章中,我们将讨论如何在JavaScript中使用这些原则,并会展示一些代码示例

What are the SOLID principles?

  • Single responsibility principle(单一职责原则)
  • Open-closed principle(开闭原则)
  • Liskov substitution principle(里式替换原则)
  • Interface segregation principle(接口分离原则)
  • Dependency inversion principle(依赖倒置原则)

Single responsibility principle

一个class,一个module或者一个function应该只负责一个角色。所以它应该只有一个改变原因。

单一职责原则是SOLID最简单的原则之一。然而,开发者经常误解它。认为一个模块应该只做一件事。

让我们用一个简单的例子来理解这个原则。下面的JavaScript代码片段有一个名为 ManageEmployee 的 class 和几个管理雇员的 function

class ManageEmployee {

  constructor(private http: HttpClient)
  SERVER_URL = 'http://localhost:5000/employee';

  getEmployee (empId){
     return this.http.get(this.SERVER_URL + `/${empId}`);
  }

  updateEmployee (employee){
     return this.http.put(this.SERVER_URL + `/${employee.id}`,employee);
  }

  deleteEmployee (empId){
     return this.http.delete(this.SERVER_URL + `/${empId}`);
  }

  calculateEmployeeSalary (empId, workingHours){
    var employee = this.http.get(this.SERVER_URL + `/${empId}`);
    return employee.rate * workingHours;
  }

}

前面的代码看上去完全没有问题,并且很多开发者都会按照相同的方法来做并不会有任何问题。然而,由于它是对两个角色负责,这样他就有违单一职责原则。getEmployee()updateEmployee()deleteEmployee()函数直接与 HR 管理有关,而calculateEmployeeSalary()则与财务管理有关。

将来,如果你需要为HR或财务更新一个功能时,你就必须改变 ManageEmployee class 来影响两个角色,因此, ManageEmployee class 违反了单一职责原则。你需要把HR和财务有关的功能分开,使代码符合单一职责原则。下面的代码示例证明了这一点。

class ManageEmployee {

  constructor(private http: HttpClient)
  SERVER_URL = 'http://localhost:5000/employee';

  getEmployee (empId){
     return this.http.get(this.SERVER_URL + `/${empId}`);
  }

  updateEmployee (employee){
     return this.http.put(this.SERVER_URL + `/${employee.id}`,employee);
  }

  deleteEmployee (empId){
     return this.http.delete(this.SERVER_URL + `/${empId}`);
  }

}

class ManageSalaries {

  constructor(private http: HttpClient)
  SERVER_URL = 'http://localhost:5000/employee';

  calculateEmployeeSalary (empId, workingHours){
    var employee = this.http.get(this.SERVER_URL + `/${empId}`);
    return employee.rate * workingHours;
  }

}

Open-closed principle

Functions, modules, 和 classes 应该是可扩展的,但不是可修改的。

这是实现大型应用时需要遵循的一个重要原则。根据这一原则,我们应该能够很容易地为应用添加新的功能,但同时我们不应该对现有的代码引入破坏式修改。

举个例子,假设我们实现一个 calculateSalaries() 的函数,该函数使用一个带有工作角色和时薪的数组来计算工资。

class ManageSalaries {
  constructor() {
    this.salaryRates = [
      { id: 1, role: 'developer', rate: 100 },
      { id: 2, role: 'architect', rate: 200 },
      { id: 3, role: 'manager', rate: 300 },
    ];
  }

  calculateSalaries(empId, hoursWorked) {
    let salaryObject = this.salaryRates.find((o) => o.id === empId);
    return hoursWorked * salaryObject.rate;
  }
}

const mgtSalary = new ManageSalaries();
console.log("Salary : ", mgtSalary.calculateSalaries(1, 100));

直接修改 salaryRates 数组会违反开闭原则。例如,你需要为一个新角色进行工资计算。在这种情况下,你需要创建一个单独的方法,将工资添加到 salaryRates 数组中,而不是对原代码进行修改。

class ManageSalaries {
  constructor() {
    this.salaryRates = [
      { id: 1, role: 'developer', rate: 100 },
      { id: 2, role: 'architect', rate: 200 },
      { id: 3, role: 'manager', rate: 300 },
    ];
  }

  calculateSalaries(empId, hoursWorked) {
    let salaryObject = this.salaryRates.find((o) => o.id === empId);
    return hoursWorked * salaryObject.rate;
  }

  addSalaryRate(id, role, rate) {
    this.salaryRates.push({ id: id, role: role, rate: rate });
  }
}

const mgtSalary = new ManageSalaries();
mgtSalary.addSalaryRate(4, 'developer', 250);
console.log('Salary : ', mgtSalary.calculateSalaries(4, 100));

Liskov substitution principle

假设P(y)是类型A的对象y的一个可证明属性,那么对于类型B的对象x,其中B是A的子类型,P(x)应该为真

你能在网上找到关于里式替换原则的不同定义,但它们都有相同的含义。简单的说,里式替换原则指出,如果父类的子类在应用中产生意外行为,我们就不应该用子类替换父类。

例如,有一个名为 Animal 的类,它包括一个名为 eat() 的函数。

class Animal{
  eat() {
    console.log("Animal Eats")
  }
}

现在,我将把 Animal 类扩展成一个名为Bird的新类,其函数名为 fly()

class Bird extends Animal{
  fly() {
    console.log("Bird Flies")
  }
}

var parrot = new Bird();
parrot.eat();
parrot.fly();

在之前的例子中,我根据 Bird 类创建了一个名为 parrot 的对象,并同时拥有 eat 和 fly 方法。由于鹦鹉能够完成这两个动作,因此将 Animal 类扩展到 Bird 类并不违反里式替换原则

现在我们进一步扩展 Bird ,创建了一个名为 Ostrich 的新类

class Ostrich extends Bird{
  console.log("Ostriches Do Not Fly")
}

var ostrich = new Ostrich();
ostrich.eat();
ostrich.fly();

这个对 Bird 类的扩展就违反了里式替换原则,因为鸵鸟不会飞行,这可能会在应用中引发意外行为。解决这个情况的最佳方法是从 Animal 类扩展 Ostrich 类。

class Ostrich extends Animal{

  walk() {
    console.log("Ostrich Walks")
  }

}

Interface segregation principle

客户端不应该添加他们永远不会使用的依赖接口。

这个原则和接口有关,主要是将大接口分为小接口。举个例子,假如你要去驾校学习如何驾驶汽车,他们给你一大套关于驾驶汽车、卡车、火车的说明。由于你只需要学习驾驶汽车,你不需要其他的信息。驾校就应该把说明分开,只给你专门针对汽车的说明。

由于 JavaScript 不支持 interface ,所以在基于 JavaScript 的应用中很难采用这一原则。然而,我们可以使用 JavaScript 组合来实现这一点。组合允许开发人员在不继承整个类的情况下添加功能。假设有一个名为 DrivingTest 的类,然后有两个名为 startCarTest 和 startTruckTest 的函数。然后我们有 CarDrivingTest 和 TruckDrivingTest 扩展 DrivingTest 类,然后我们必须强制这两个类实现 startCarTest 和 startTruckTest s函数。

Class DrivingTest {
  constructor(userType) {
    this.userType = userType;
  }

  startCarTest() {
    console.log(“This is for Car Drivers”’);
  }

  startTruckTest() {
    console.log(“This is for Truck Drivers”);
  }
}

class CarDrivingTest extends DrivingTest {
  constructor(userType) {
    super(userType);
  }

  startCarTest() {
    returnCar Test Started”;
  }

  startTruckTest() {
    return null;
  }
}

class TruckDrivingTest extends DrivingTest {
  constructor(userType) {
    super(userType);
  }

  startCarTest() {
    return null;
  }

  startTruckTest() {
    returnTruck Test Started”;
  }
}

const carTest = new CarDrivingTest(carDriver );
console.log(carTest.startCarTest());
console.log(carTest.startTruckTest());

const truckTest = new TruckDrivingTest( ruckdriver );
console.log(truckTest.startCarTest());
console.log(truckTest.startTruckTest());

然而,这种实现方法违反了接口分离原则,因为我们强迫那两个扩展类实现这两种功能。我们可以通过组合来为所需的类添加功能来解决这个问题,如下面的例子中所示。

Class DrivingTest {
  constructor(userType) {
    this.userType = userType;
  }
}

class CarDrivingTest extends DrivingTest {
  constructor(userType) {
    super(userType);
  }
}

class TruckDrivingTest extends DrivingTest {
  constructor(userType) {
    super(userType);
  }
}

const carUserTests = {
  startCarTest() {
    returnCar Test Started’;
  },
};

const truckUserTests = {
  startTruckTest() {
    returnTruck Test Started’;
  },
};

Object.assign(CarDrivingTest.prototype, carUserTests);
Object.assign(TruckDrivingTest.prototype, truckUserTests);

const carTest = new CarDrivingTest(carDriver );
console.log(carTest.startCarTest());
console.log(carTest.startTruckTest()); // Will throw an exception

const truckTest = new TruckDrivingTest( ruckdriver );
console.log(truckTest.startTruckTest());
console.log(truckTest.startCarTest()); // Will throw an exception

现在, carTest.startTruckTest() 将抛出一个异常,因为 startTruckTest() 函数没有分配给 CarDrivingTest 类。

Dependency inversion principle

高层模块应该使用抽象化。并且,他们不应该依赖于低层模块。

依赖倒置是关于解耦代码的。遵循这个原则将使你能够灵活地在最高级别上扩展和改变你的应用而不出现任何问题。

关于JavaScript,我们不需要考虑抽象的问题,因为JavaScript是一种动态语言。然而,我们需要确保高层模块不依赖于低层模块。

我们来用一个简单的例子来解释依赖倒置是如何工作的。假设你在你的应用中需要使用 Yahoo 的 email API ,现在你需要把它改为 Gmail 的 API 。如果你像下面的例子一样实现了没有依赖倒置的控制器,你需要对每个控制器都进行修改。这是因为多个控制器使用了 Yahoo API ,你需要找到每个实例并更新它。

class EmailController { 
  sendEmail(emailDetails) { 
    // Need to change this line in every controller that uses YahooAPI.const response = YahooAPI.sendEmail(emailDetails); 
    if (response.status == 200) { 
       return true;
    } else {
       return false;
    }
  }
}

依赖倒置原则可以帮助开发者避免这种高昂的错误,在这个原则下,将 email API 处理部分转移到一个单独的控制器。然后你只需要在 email API 发生改变时改变该控制器即可。

class EmailController { 
  sendEmail(emailDetails) { 
    const response = EmailApiController.sendEmail(emailDetails);   
    if (response.status == 200) { 
       return true;
    } else {
       return false;
    }
  }
}

class EmailApiController {
  sendEmail(emailDetails) {
    // Only need to change this controller. return YahooAPI.sendEmail(emailDetails);
  }
}

Conclusion

在这篇文章中,我们讨论了在软件设计中SOLID原则的重要性,以及我们如何在JavaScript应用中采用这些概念。作为开发人员,理解并在我们的应用中使用这些核心概念是很重要的。有时,在处理一些小的应用时,这些原则的好处可能并不明显,但一旦你开始在一个大规模的项目上工作时,你一定会知道它们的不同。