高级进阶之JavaScript中的SOLID原则

前言

在软件开发领域,构建可维护、可扩展和可重用的软件系统一直是开发人员追求的目标。然而,随着项目的增长和复杂性的提高,代码变得越来越难以理解、修改和扩展。为了应对这些挑战,面向对象设计中的SOLID原则应运而生。

SOLID原则为开发人员提供了一套有力的工具和指导原则,帮助他们构建可维护、可扩展和可重用的软件系统。通过遵循这些原则,我们能够编写高质量的代码,提高开发效率,降低维护成本,并为未来的功能扩展奠定坚实的基础。在接下来的文章中,我们将深入探讨每个原则的概念和实践,并展示它们如何共同协作,构建出优秀的软件系统。

SOLID原则是什么?

  • 单一职责原则(Single Responsibility Principle,SRP):一个类应该只有一个引起它变化的原因。换句话说,一个类应该只有一个职责。这样可以提高类的内聚性,使其更易于理解、修改和测试。
  • 开放封闭原则(Open-Closed Principle,OCP):软件实体(类、模块、函数等)应该对扩展开放,对修改封闭。意味着在添加新功能时,不应该修改现有的代码,而是通过扩展现有代码来实现新功能。
  • 里式替换原则(Liskov Substitution Principle,LSP):子类应该能够替换掉父类并且不会破坏程序的正确性。也就是说,子类应该能够在不改变程序正确性的前提下扩展父类的功能。
  • 接口隔离原则(Interface Segregation Principle,ISP):客户端不应该强迫依赖于它们不使用的接口。接口应该精确地定义客户端所需的功能,避免定义冗余的接口。
  • 依赖倒置原则(Dependency Inversion Principle,DIP):高层模块不应该依赖于低层模块,两者都应该依赖于抽象。抽象不应该依赖于具体实现细节,具体实现细节应该依赖于抽象。

单一职责原则

一个类、一个模块或一个函数应该只负责一个角色。因此,它应该只有一个改变的原因。

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

让我们来考虑一个简单的例子来理解这个原则。下面的JavaScript代码片段有一个名为ManageEmployee的类,以及几个用于管理员工的函数。

js 复制代码
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()函数直接与人力资源管理相关,而calculateEmployeeSalary()函数与财务管理相关。

将来,如果需要为人力资源或财务部门更新功能,将不得不更改ManageEmployee类,从而影响到两个角色。因此,ManageEmployee类违反了单一职责原则。你需要将与人力资源和财务部门相关的功能分离,以使代码符合单一职责原则。以下代码示例演示了这一点。

js 复制代码
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;
  }

}

开闭原则

函数、模块和类应该是可扩展的,但不可修改的。

在实施大规模应用程序时,遵循这一重要原则非常关键。根据这一原则,我们能够轻松地向应用程序添加新功能,但不应该对现有代码引入破坏性的变更。

例如,假设我们已经实现了一个名为calculateSalaries()的函数,它使用一个包含定义的职位角色和小时工资的数组来计算工资。

js 复制代码
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数组中,而不对原始代码进行修改。

js 复制代码
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));

里氏替换原则

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

在互联网上,你会找到关于Liskov替换原则的不同定义,但它们都暗示着相同的意义。简单来说,Liskov原则指出,如果子类在应用程序中产生了意外行为,我们就不应该用子类替换父类。

例如,考虑一个名为Animal的类,其中包含一个名为eat()的函数。

js 复制代码
class Animal{
  eat() {
    console.log("Animal Eats")
  }
}

现在我将Animal类扩展为一个名为Bird的新类,其中包含一个名为fly()的函数。

js 复制代码
class Bird extends Animal{
  fly() {
    console.log("Bird Flies")
  }
}

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

在之前的例子中,我创建了一个名为parrot的对象,它是从Bird类继承而来的,并调用了eat()和fly()方法。由于鹦鹉能够执行这两个动作,将Animal类扩展到Bird类并不违反Liskov原则。

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

js 复制代码
class Ostrich extends Bird{
  console.log("Ostriches Do Not Fly")
}

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

这个对Bird类的扩展违反了Liskov原则,因为鸵鸟不能飞行------这可能会在应用程序中产生意外的行为。解决这个问题的最佳方法是从Animal类扩展Ostrich类。

js 复制代码
class Ostrich extends Animal{

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

}

接口隔离原则

客户不应被迫依赖于他们永远不会使用的接口。

这个原则与接口有关,重点是将大的接口分解为小的接口。例如,假设你要去驾校学习开车,他们给你一大堆关于开车、卡车和火车的指令。由于你只需要学习开车,不需要其他所有的信息。驾校应该将指令分开,只给你关于汽车的指令。

由于JavaScript不支持接口,因此在基于JavaScript的应用程序中采用这一原则较为困难。然而,我们可以使用JavaScript组合来实现这一点。组合允许开发人员向类中添加功能,而无需继承整个类。例如,假设有一个名为DrivingTest的类,其中包含两个名为startCarTest和startTruckTest的函数。如果我们为CarDrivingTest和TruckDrivingTest扩展DrivingTest类,我们必须强制这两个类都实现startCarTest和startTruckTest函数。

js 复制代码
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() {
    return "Car Test Started";
  }

  startTruckTest() {
    return null;
  }
}

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

  startCarTest() {
    return null;
  }

  startTruckTest() {
    return "Truck 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());

然而,这种实现违反了接口隔离原则,因为我们强制两个扩展类都实现了两个功能。我们可以通过使用组合来为所需的类附加功能来解决这个问题,如下面的示例所示。

js 复制代码
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() {
    return 'Car Test Started';
  },
};

const truckUserTests = {
  startTruckTest() {
    return 'Truck 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类。

依赖倒置原则

高级模块应该使用抽象化。然而,它们不应该依赖于低级模块。

依赖倒置的核心是解耦你的代码。遵循这个原则将使你的应用在最高层面上具备灵活性,可以轻松扩展和修改,而不会出现任何问题。

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

让我们来考虑一个简单的例子来解释依赖倒置是如何工作的。假设你在应用程序中使用了Yahoo邮件API,现在你需要将其更改为Gmail API。如果你在控制器中没有使用依赖倒置,就像下面的示例一样,你需要对控制器进行一些更改。这是因为多个控制器使用了Yahoo API,你需要找到每个实例并进行更新。

js 复制代码
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;
    }
  }
}

依赖倒置原则能够帮助开发者避免这种昂贵的错误,通过将电子邮件API处理部分移动到一个独立的控制器中。这样,只需要在电子邮件API发生变化时修改该控制器即可。

js 复制代码
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);
  }
}

结论

在本文中,我们讨论了SOLID原则在软件设计中的重要性以及如何在JavaScript应用程序中采用这些概念。作为开发人员,理解并运用这些核心概念对我们的应用程序至关重要。有时,在处理小型应用程序时,这些原则的好处可能并不明显,但一旦开始处理大型项目,您肯定会意识到它们所带来的差异。

相关推荐
崔庆才丨静觅25 分钟前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60611 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了1 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅1 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅2 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅2 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment2 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅2 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊2 小时前
jwt介绍
前端
爱敲代码的小鱼3 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax