首页 >web前端 >js教程 >JavaScript 的坚实原则

JavaScript 的坚实原则

Linda Hamilton
Linda Hamilton原创
2024-12-28 19:34:11342浏览

OOP 范式的引入普及了继承、多态、抽象和封装等关键编程概念。 OOP 很快成为一种被广泛接受的编程范例,并以多种语言(例如 Java、C、C#、JavaScript 等)实现。随着时间的推移,面向对象编程系统变得越来越复杂,但其软件仍然难以改变。为了提高软件可扩展性并降低代码刚性,Robert C. Martin(又名 Bob 叔叔)在 2000 年代初引入了 SOLID 原则。

SOLID 是一个缩写词,由一组原则组成——单一责任原则、开闭原则、里氏替换原则、接口隔离原则和依赖倒置原则——帮助软件工程师设计和编写可维护、可扩展和灵活的软件代码。它的目的是什么?提高遵循面向对象编程(OOP)范式开发的软件的质量。

在本文中,我们将深入研究 SOLID 的所有原则,并说明如何使用最流行的 Web 编程语言之一 JavaScript 来实现它们。

单一职责原则(SRP)

SOLID中的第一个字母代表单一责任原则。这一原则表明类或模块应该只执行一个角色。

简单地说,一个类应该有单一的责任或单一的改变理由。如果一个类处理多个功能,则更新一个功能而不影响其他功能会变得很棘手。随后的复杂情况可能会导致软件性能出现故障。为了避免此类问题,我们应该尽力编写模块化软件,其中关注点是分离的。

如果一个类的职责或功能太多,修改起来就会很头疼。通过使用单一责任原则,我们可以编写模块化、更易于维护且不易出错的代码。以人物模型为例:

class Person {
    constructor(name, age, height, country){
      this.name = name
      this.age = age
      this.height = height
      this.country = country
  }
  getPersonCountry(){
    console.log(this.country)    
  }
  greetPerson(){
    console.log("Hi " + this.name)
  }
  static calculateAge(dob) { 
    const today = new Date(); 
    const birthDate = new Date(dob);

    let age = today.getFullYear() - birthDate.getFullYear(); 
    const monthDiff = today.getMonth() - birthDate.getMonth();

    if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) { 
      age--; 
    }
    return age; 
  } 
}

上面的代码看起来没问题,对吧?不完全是。示例代码违反了单一责任原则。 Person 类不是可以创建 Person 的其他实例的唯一模型,它还具有其他职责,例如calculateAge、greetPerson 和getPersonCountry。

Person 类处理的这些额外职责使得仅更改代码的一个方面变得困难。例如,如果您尝试重构calculateAge,您也可能被迫重构Person 模型。根据我们的代码库的紧凑和复杂程度,重新配置代码而不导致错误可能很困难。

让我们尝试修改错误。我们可以将职责分成不同的类,如下所示:

class Person {
    constructor(name, age, height, country){
      this.name = name
      this.age = age
      this.height = height
      this.country = country
  }
  getPersonCountry(){
    console.log(this.country)    
  }
  greetPerson(){
    console.log("Hi " + this.name)
  }
  static calculateAge(dob) { 
    const today = new Date(); 
    const birthDate = new Date(dob);

    let age = today.getFullYear() - birthDate.getFullYear(); 
    const monthDiff = today.getMonth() - birthDate.getMonth();

    if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) { 
      age--; 
    }
    return age; 
  } 
}

正如您从上面的示例代码中看到的,我们已经分离了我们的职责。 Person 类现在是一个模型,我们可以用它创建一个新的 person 对象。 PersonUtils 类只有一项职责——计算一个人的年龄。 PersonService 类处理问候语并向我们显示每个人的国家/地区。

如果我们愿意,我们仍然可以进一步减少这个过程。遵循SRP,我们希望将类的责任解耦到最低限度,以便在出现问题时,可以轻松地进行重构和调试。

通过将功能划分为单独的类,我们遵循单一职责原则并确保每个类负责应用程序的特定方面。

在我们继续下一个原则之前,应该注意的是,遵守 SRP 并不意味着每个类应该严格包含单个方法或功能。

但是,坚持单一责任原则意味着我们应该有意识地为类分配功能。一个班级所进行的每件事在任何意义上都应该是密切相关的。我们必须小心,不要让多个类分散在各处,并且我们应该尽一切努力避免代码库中出现臃肿的类。

开闭原则(OCP)

开闭原则指出软件组件(类、函数、模块等)应该对扩展开放,对修改封闭。我知道你在想什么——是的,这个想法一开始可能看起来很矛盾。但 OCP 只是要求软件的设计方式允许扩展而不必修改源代码。

OCP 对于维护大型代码库至关重要,因为该指南允许您引入新功能,而几乎没有破坏代码的风险。当出现新需求时,您不应修改现有的类或模块,而应通过添加新组件来扩展相关类。执行此操作时,请务必检查新组件是否不会给系统引入任何错误。

OC 原理可以使用 ES6 类继承功能在 JavaScript 中实现。

以下代码片段说明了如何使用前面提到的 ES6 class 关键字在 JavaScript 中实现开闭原则:

class Person {
    constructor(name, dateOfBirth, height, country){
      this.name = name
      this.dateOfBirth = dateOfBirth
      this.height = height
      this.country = country
  }
}

class PersonUtils {
  static calculateAge(dob) { 
    const today = new Date(); 
    const birthDate = new Date(dob);

    let age = today.getFullYear() - birthDate.getFullYear(); 
    const monthDiff = today.getMonth() - birthDate.getMonth();

    if(monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) { 
      age--; 
    }
    return age; 
  } 
}

const person = new Person("John", new Date(1994, 11, 23), "6ft", "USA"); 
console.log("Age: " + PersonUtils.calculateAge(person.dateOfBirth));

class PersonService {
  getPersonCountry(){
    console.log(this.country)    
  }
  greetPerson(){
    console.log("Hi " + this.name)
  }
}

上面的代码工作正常,但它仅限于计算矩形的面积。现在想象一下有一个新的计算要求。举例来说,我们需要计算圆的面积。我们必须修改 shapeProcessor 类来满足这一点。但是,遵循 JavaScript ES6 标准,我们可以扩展此功能以考虑新形状的区域,而不必修改 shapeProcessor 类。

我们可以这样做:

class Person {
    constructor(name, age, height, country){
      this.name = name
      this.age = age
      this.height = height
      this.country = country
  }
  getPersonCountry(){
    console.log(this.country)    
  }
  greetPerson(){
    console.log("Hi " + this.name)
  }
  static calculateAge(dob) { 
    const today = new Date(); 
    const birthDate = new Date(dob);

    let age = today.getFullYear() - birthDate.getFullYear(); 
    const monthDiff = today.getMonth() - birthDate.getMonth();

    if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) { 
      age--; 
    }
    return age; 
  } 
}

在上面的代码片段中,我们使用 extends 关键字扩展了 Shape 类的功能。在每个子类中,我们重写了area()方法的实现。遵循这个原则,我们可以添加更多的形状和处理区域,而无需修改 ShapeProcessor 类的功能。

为什么 OCP 很重要?

  • 减少错误:OCP 通过避免系统修改来帮助避免大型代码库中的错误。
  • 鼓励软件适应性:OCP 还提高了在不破坏或更改源代码的情况下向软件添加新功能的便利性。
  • 测试新功能:OCP 提倡代码扩展而不是修改,使新功能更容易作为一个单元进行测试,而不影响整个代码库。

里氏替换原则

里氏替换原则指出子类的对象应该能够替换超类的对象而不破坏代码。让我们用一个例子来解释它是如何工作的:如果 L 是 P 的子类,那么 L 的对象应该替换 P 的对象,而不会破坏系统。这仅仅意味着子类应该能够以不破坏系统的方式重写超类方法。

在实践中,里氏替换原则确保遵守以下条件:

  • 子类应该重写父类的方法而不破坏代码
  • 子类不应偏离父类的行为,这意味着子类只能添加功能,而不能更改或删除父类的功能
  • 与父类实例一起工作的代码应该与子类实例一起工作,而不需要知道该类已更改

是时候用 JavaScript 代码示例来说明里氏替换原理了。看看:

class Person {
    constructor(name, dateOfBirth, height, country){
      this.name = name
      this.dateOfBirth = dateOfBirth
      this.height = height
      this.country = country
  }
}

class PersonUtils {
  static calculateAge(dob) { 
    const today = new Date(); 
    const birthDate = new Date(dob);

    let age = today.getFullYear() - birthDate.getFullYear(); 
    const monthDiff = today.getMonth() - birthDate.getMonth();

    if(monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) { 
      age--; 
    }
    return age; 
  } 
}

const person = new Person("John", new Date(1994, 11, 23), "6ft", "USA"); 
console.log("Age: " + PersonUtils.calculateAge(person.dateOfBirth));

class PersonService {
  getPersonCountry(){
    console.log(this.country)    
  }
  greetPerson(){
    console.log("Hi " + this.name)
  }
}

在上面的代码片段中,我们创建了两个子类(Bicycle 和 Car)和一个超类(Vehicle)。出于本文的目的,我们为超类实现了一个方法 (OnEngine)。

LSP 的核心条件之一是子类应该覆盖父类的功能而不破坏代码。记住这一点,让我们看看我们刚刚看到的代码片段是如何违反里氏替换原则的。实际上,汽车有发动机并且可以打开发动机,但自行车从技术上讲没有发动机,因此无法打开发动机。因此,Bicycle 无法在不破坏代码的情况下重写 Vehicle 类中的 OnEngine 方法。

我们现在已经确定了违反里氏替换原则的代码部分。 Car 类可以重写超类中的 OnEngine 功能,并以区别于其他车辆(例如飞机)的方式实现它,并且代码不会中断。 Car 类满足里氏替换原则。

在下面的代码片段中,我们将说明如何构建符合里氏替换原则的代码:

class Person {
    constructor(name, age, height, country){
      this.name = name
      this.age = age
      this.height = height
      this.country = country
  }
  getPersonCountry(){
    console.log(this.country)    
  }
  greetPerson(){
    console.log("Hi " + this.name)
  }
  static calculateAge(dob) { 
    const today = new Date(); 
    const birthDate = new Date(dob);

    let age = today.getFullYear() - birthDate.getFullYear(); 
    const monthDiff = today.getMonth() - birthDate.getMonth();

    if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) { 
      age--; 
    }
    return age; 
  } 
}

这是具有通用功能“移动”的 Vehicle 类的基本示例。人们普遍认为所有车辆都会移动;它们只是通过不同的机制移动。我们要说明 LSP 的一种方法是重写 move() 方法并以描述特定车辆(例如汽车)如何移动的方式实现它。

为此,我们将创建一个 Car 类来扩展 Vehicle 类并重写 move 方法以适应汽车的移动,如下所示:

class Person {
    constructor(name, dateOfBirth, height, country){
      this.name = name
      this.dateOfBirth = dateOfBirth
      this.height = height
      this.country = country
  }
}

class PersonUtils {
  static calculateAge(dob) { 
    const today = new Date(); 
    const birthDate = new Date(dob);

    let age = today.getFullYear() - birthDate.getFullYear(); 
    const monthDiff = today.getMonth() - birthDate.getMonth();

    if(monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) { 
      age--; 
    }
    return age; 
  } 
}

const person = new Person("John", new Date(1994, 11, 23), "6ft", "USA"); 
console.log("Age: " + PersonUtils.calculateAge(person.dateOfBirth));

class PersonService {
  getPersonCountry(){
    console.log(this.country)    
  }
  greetPerson(){
    console.log("Hi " + this.name)
  }
}

我们仍然可以在另一个子车辆类(例如飞机)中实现 move 方法。

我们的做法如下:

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

class ShapeProcessor { 
    calculateArea(shape) { 
    if (shape instanceof Rectangle) { 
    return shape.area(); 
    } 
  }
}  
const rectangle = new Rectangle(10, 20); const shapeProcessor = new ShapeProcessor(); console.log(shapeProcessor.calculateArea(rectangle)); 

在上面的两个示例中,我们说明了继承和方法重写等关键概念。

注意:允许子类实现父类中已定义的方法的编程功能称为方法重写。

让我们做一些家务工作并将所有东西放在一起,就像这样:

class Shape {
  area() {
    console.log("Override method area in subclass");
  }
}

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

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

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

  area() {
    return Math.PI * this.radius * this.radius;
  }
}

class ShapeProcessor {
  calculateArea(shape) {
    return shape.area();
  }
}

const rectangle = new Rectangle(20, 10);
const circle = new Circle(2);
const shapeProcessor = new ShapeProcessor();

console.log(shapeProcessor.calculateArea(rectangle));
console.log(shapeProcessor.calculateArea(circle));

现在,我们有 2 个子类继承并重写父类的单个功能,并根据它们的要求实现它。这个新的实现不会破坏代码。

接口隔离原则(ISP)

接口隔离原则规定,任何客户端都不应被迫依赖于它不使用的接口。它希望我们创建与特定客户端相关的更小、更具体的接口,而不是拥有一个大型、单一的接口,迫使客户端实现他们不需要的方法。

保持接口紧凑使得代码库更易于调试、维护、测试和扩展。如果没有 ISP,大型接口的某个部分的更改可能会迫使代码库的不相关部分发生更改,从而导致我们进行代码重构,这在大多数情况下取决于代码库的大小可能是一项艰巨的任务。

JavaScript 与 Java 等基于 C 的编程语言不同,它没有内置的接口支持。然而,有一些技术可以在 JavaScript 中实现接口。

接口是类必须实现的一组方法签名。

在 JavaScript 中,您将接口定义为具有方法名称和函数签名的对象,如下所示:

class Person {
    constructor(name, age, height, country){
      this.name = name
      this.age = age
      this.height = height
      this.country = country
  }
  getPersonCountry(){
    console.log(this.country)    
  }
  greetPerson(){
    console.log("Hi " + this.name)
  }
  static calculateAge(dob) { 
    const today = new Date(); 
    const birthDate = new Date(dob);

    let age = today.getFullYear() - birthDate.getFullYear(); 
    const monthDiff = today.getMonth() - birthDate.getMonth();

    if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) { 
      age--; 
    }
    return age; 
  } 
}

要在 JavaScript 中实现接口,请创建一个类并确保它包含与接口中指定的名称和签名相同的方法:

class Person {
    constructor(name, dateOfBirth, height, country){
      this.name = name
      this.dateOfBirth = dateOfBirth
      this.height = height
      this.country = country
  }
}

class PersonUtils {
  static calculateAge(dob) { 
    const today = new Date(); 
    const birthDate = new Date(dob);

    let age = today.getFullYear() - birthDate.getFullYear(); 
    const monthDiff = today.getMonth() - birthDate.getMonth();

    if(monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) { 
      age--; 
    }
    return age; 
  } 
}

const person = new Person("John", new Date(1994, 11, 23), "6ft", "USA"); 
console.log("Age: " + PersonUtils.calculateAge(person.dateOfBirth));

class PersonService {
  getPersonCountry(){
    console.log(this.country)    
  }
  greetPerson(){
    console.log("Hi " + this.name)
  }
}

现在我们已经弄清楚如何在 JavaScript 中创建和使用接口。我们需要做的下一件事是说明如何在 JavaScript 中隔离接口,以便我们可以看到它们如何组合在一起并使代码更易于维护。

在下面的例子中,我们将使用打印机来说明接口隔离的原理。

假设我们有打印机、扫描仪和传真机,让我们创建一个定义这些对象功能的接口:

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

class ShapeProcessor { 
    calculateArea(shape) { 
    if (shape instanceof Rectangle) { 
    return shape.area(); 
    } 
  }
}  
const rectangle = new Rectangle(10, 20); const shapeProcessor = new ShapeProcessor(); console.log(shapeProcessor.calculateArea(rectangle)); 

在上面的代码中,我们创建了一系列分离或隔离的接口,以反对使用一个定义所有这些功能的大型接口。通过将这些功能分解为更小的部分和更具体的接口,我们允许不同的客户端仅实现他们需要的方法,并保留所有其他部分。

下一步,我们将创建实现这些接口的类。遵循接口隔离原则,每个类只会实现它需要的方法。

如果我们想实现一个只能打印文档的基本打印机,我们可以通过printerInterface实现print()方法,如下所示:

class Shape {
  area() {
    console.log("Override method area in subclass");
  }
}

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

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

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

  area() {
    return Math.PI * this.radius * this.radius;
  }
}

class ShapeProcessor {
  calculateArea(shape) {
    return shape.area();
  }
}

const rectangle = new Rectangle(20, 10);
const circle = new Circle(2);
const shapeProcessor = new ShapeProcessor();

console.log(shapeProcessor.calculateArea(rectangle));
console.log(shapeProcessor.calculateArea(circle));

该类仅实现PrinterInterface。它不实现扫描或传真方法。通过遵循接口隔离原则,客户端(在本例中为 Printer 类)降低了其复杂性并提高了软件的性能。

依赖倒置原则(DIP)

现在我们的最后一个原则:依赖倒置原则。该原则表示较高级别的模块(业务逻辑)应该依赖于抽象,而不是直接依赖于较低级别的模块(具体)。它帮助我们减少代码依赖性,并为开发人员提供在更高级别修改和扩展应用程序的灵活性,而不会遇到复杂性。

为什么依赖倒置原则支持抽象而不是直接依赖?这是因为抽象的引入减少了更改的潜在影响,提高了可测试性(模拟抽象而不是具体实现),并在代码中实现了更高程度的灵活性。这条规则使得通过模块化方法扩展软件组件变得更容易,也帮助我们在不影响高层逻辑的情况下修改低层组件。

遵守 DIP 使代码更易于维护、扩展和扩展,从而阻止因代码更改而可能出现的错误。它建议开发人员在类之间使用松耦合而不是紧耦合。一般来说,通过采用优先考虑抽象而不是直接依赖的思维方式,团队将获得适应和添加新功能或更改旧组件的敏捷性,而不会造成连锁反应。在 JavaScript 中,我们可以使用依赖注入方法来实现 DIP,如下所示:

class Person {
    constructor(name, age, height, country){
      this.name = name
      this.age = age
      this.height = height
      this.country = country
  }
  getPersonCountry(){
    console.log(this.country)    
  }
  greetPerson(){
    console.log("Hi " + this.name)
  }
  static calculateAge(dob) { 
    const today = new Date(); 
    const birthDate = new Date(dob);

    let age = today.getFullYear() - birthDate.getFullYear(); 
    const monthDiff = today.getMonth() - birthDate.getMonth();

    if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) { 
      age--; 
    }
    return age; 
  } 
}

在上面的基本示例中,Application 类是依赖于数据库抽象的高级模块。我们创建了两个数据库类:MySQLDatabase 和 MongoDBDatabase。数据库是低级模块,它们的实例被注入到应用程序运行时中,而无需修改应用程序本身。

结论

SOLID 原则是可扩展、可维护和稳健的软件设计的基本构建块。这套原则可以帮助开发人员编写干净、模块化且适应性强的代码。

SOLID 原则促进内聚功能、无需修改的可扩展性、对象替换、接口分离以及对具体依赖项的抽象。请务必将 SOLID 原则集成到您的代码中,以防止错误并获得其所有好处。


LogRocket:通过了解上下文更轻松地调试 JavaScript 错误

调试代码始终是一项乏味的任务。但你越了解自己的错误,就越容易纠正它们。

LogRocket 允许您以新的、独特的方式理解这些错误。我们的前端监控解决方案跟踪用户与 JavaScript 前端的互动,使您能够准确查看用户的操作导致了错误。

SOLID principles for JavaScript

LogRocket 记录控制台日志、页面加载时间、堆栈跟踪、带有标头正文的慢速网络请求/响应、浏览器元数据和自定义日志。了解 JavaScript 代码的影响从未如此简单!

免费试用。

以上是JavaScript 的坚实原则的详细内容。更多信息请关注PHP中文网其他相关文章!

声明:
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn