【软件设计】TypeScript 中的 SOLID 原则

简介: 【软件设计】TypeScript 中的 SOLID 原则

了解有关 TypeScript 中 SOLID 原则的更多信息

TypeScript 对用 JavaScript 编写干净的代码产生了巨大的影响。但总有改进的方法,编写更好、更简洁的代码的一个重要方法是遵循由 Robert C. Martin(也称为 Uncle Bob)发明的所谓 SOLID 设计原则。

在本文中,我将通过使用 TypeScript 编写的示例向您介绍这些原则。我已经在这个 Github 存储库上部署了所有示例。



单一职责原则 (SRP)

“一个类改变的原因不应该超过一个。”

一个类应该有一个目的/责任,因此只有一个改变的理由。遵循这一原则可以更好地维护代码并最大限度地减少潜在的副作用。

在以下不好的示例中,您会看到如何存在多重责任。首先,我们为一本书建模,而且,我们将这本书保存为一个文件。我们在这里遇到了两个目的:

class Book {
  public title: string;
  public author: string;
  public description: string;
  public pages: number;
  // constructor and other methods
  public saveToFile(): void {
    // some fs.write method to save book to file
  }
}

第二个示例向您展示了如何通过遵循单一职责原则来处理这个问题。我们最终有两个类,而不是只有一个类。每个目的一个。

 

class Book {
  public title: string;
  public author: string;
  public description: string;
  public pages: number;
  // constructor and other methods
}
class Persistence {
  public saveToFile(book: Book): void {
    // some fs.write method to save book to file
  }
}

开闭原则 (OCP)

“软件实体……应该对扩展开放,但对修改关闭。”

与其重写你的类,不如扩展它。通过不接触旧代码的新功能应该很容易扩展代码。例如,实现一个接口或类在这里非常有帮助。

在下一个示例中,您将看到错误的操作方式。我们使用了名为 AreaCalculator 的第三个类来计算 Rectangle 和 Circle 类的面积。想象一下我们稍后会添加另一个形状,这意味着我们需要创建一个新类,在这种情况下,我们还需要修改 AreaCalculator 类以计算新类的面积。这违反了开闭原则。

我们来看一下:

class Rectangle {
  public width: number;
  public height: number;
  constructor(width: number, height: number) {
    this.width = width;
    this.height = height;
  }
}
class Circle {
  public radius: number;
  constructor(radius: number) {
    this.radius = radius;
  }
}
class AreaCalculator {
  public calculateRectangleArea(rectangle: Rectangle): number {
    return rectangle.width * rectangle.height;
  }
  public calculateCircleArea(circle: Circle): number {
    return Math.PI * (circle.radius * circle.radius);
  }
}

那么,我们可以做些什么来改进这段代码呢?为了遵循开闭原则,我们只需添加一个名为 Shape 的接口,因此每个形状类(矩形、圆形等)都可以通过实现它来依赖该接口。这样,我们可以将 AreaCalculator 类简化为一个带参数的函数,而这个参数是基于我们刚刚创建的接口。

interface Shape {
  calculateArea(): number;
}
class Rectangle implements Shape {
  public width: number;
  public height: number;
  constructor(width: number, height: number) {
    this.width = width;
    this.height = height;
  }
  public calculateArea(): number {
    return this.width * this.height;
  }
}
class Circle implements Shape {
  public radius: number;
  constructor(radius: number) {
    this.radius = radius;
  }
  public calculateArea(): number {
    return Math.PI * (this.radius * this.radius);
  }
}
class AreaCalculator {
  public calculateArea(shape: Shape): number {
    return shape.calculateArea();
  }
}

里氏替换原则 (LSP)

“使用指向基类的指针或引用的函数必须能够在不知情的情况下使用派生类的对象。”

使用指向上层类的指针或引用的下层类必须能够在不知情的情况下使用派生类的对象。这些低级类应该只是扩展上级类,而不是改变它。

那么我们在下一个坏例子中看到了什么?我们上了两节课。Square 类扩展了 Rectangle 类。但正如我们所见,这个扩展没有任何意义,因为我们通过覆盖属性宽度和高度来改变逻辑。

class Rectangle {
  public width: number;
  public height: number;
  constructor(width: number, height: number) {
    this.width = width;
    this.height = height;
  }
  public calculateArea(): number {
    return this.width * this.height;
  }
}
class Square extends Rectangle {
  public _width: number;
  public _height: number;
  constructor(width: number, height: number) {
    super(width, height);
    this._width = width;
    this._height = height;
  }
}

因此,我们不需要覆盖,而是简单地删除 Square 类并将其逻辑带到 Rectangle 类而不改变其用途。

class Rectangle {
  public width: number;
  public height: number;
  constructor(width: number, height: number) {
    this.width = width;
    this.height = height;
  }
  public calculateArea(): number {
    return this.width * this.height;
  }
  public isSquare(): boolean {
    return this.width === this.height;
  }
}

接口隔离原则

“许多特定于客户端的接口都比一个通用接口好。”

简单地说,更多的接口总比接口少的好。让我解释下一个不好的例子。

我们有一个名为 Troll 的类,它实现了一个名为 Character 的接口。但是由于我们的巨魔既不会游泳也不会说话,这个角色界面似乎不适合我们的类。

interface Character {
  shoot(): void;
  swim(): void;
  talk(): void;
  dance(): void;
}
class Troll implements Character {
  public shoot(): void {
    // some method
  }
  public swim(): void {
    // a troll can't swim
  }
  public talk(): void {
    // a troll can't talk
  }
  public dance(): void {
    // some method
  }
}

那么我们可以通过遵循这个特定的原则来做些什么呢?我们删除了 Character 接口并将其功能拆分为四个接口,并且仅将我们的 Troll 类依赖于我们实际需要的这些接口。

interface Talker {
  talk(): void;
}
interface Shooter {
  shoot(): void;
}
interface Swimmer {
  swim(): void;
}
interface Dancer {
  dance(): void;
}
class Troll implements Shooter, Dancer {
  public shoot(): void {
    // some method
  }
  public dance(): void {
    // some method
  }
}

依赖倒置原则(DIP)

“取决于抽象,[不是]具体。”

这到底是什么意思,对吧?嗯,其实很简单。让我们揭开它的神秘面纱!

在这个糟糕的例子中,我们有一个 SoftwareProject 类,它初始化 FrontendDeveloper 和 BackendDeveloper 类。但这是错误的方式,因为这两个类彼此非常相似,我的意思是,它们应该做类似的事情。因此,为了实现依赖倒置原则的目标,有更好的方法来满足需求。

class FrontendDeveloper {
  public writeHtmlCode(): void {
    // some method
  }
}
class BackendDeveloper {
  public writeTypeScriptCode(): void {
    // some method
  }
}
class SoftwareProject {
  public frontendDeveloper: FrontendDeveloper;
  public backendDeveloper: BackendDeveloper;
  constructor() {
    this.frontendDeveloper = new FrontendDeveloper();
    this.backendDeveloper = new BackendDeveloper();
  }
  public createProject(): void {
    this.frontendDeveloper.writeHtmlCode();
    this.backendDeveloper.writeTypeScriptCode();
  }
}

那么我们该怎么办呢?正如我所说,它实际上非常简单,而且更容易,因为我们之前已经学习了所有其他原则。

首先,我们创建一个名为 Developer 的接口,由于 FrontendDeveloper 和 BackendDeveloper 是相似的类,我们依赖于 Developer 接口。

我们不是在 SoftwareProject 类中以单一方式初始化 FrontendDeveloper 和 BackendDeveloper,而是将它们作为一个列表来遍历它们,以便调用每个 develop() 方法。

interface Developer {
  develop(): void;
}
class FrontendDeveloper implements Developer {
  public develop(): void {
    this.writeHtmlCode();
  }
  private writeHtmlCode(): void {
    // some method
  }
}
class BackendDeveloper implements Developer {
  public develop(): void {
    this.writeTypeScriptCode();
  }
  private writeTypeScriptCode(): void {
    // some method
  }
}
class SoftwareProject {
  public developers: Developer[];
  public createProject(): void {
    this.developers.forEach((developer: Developer) => {
      developer.develop();
    });
  }
}

感谢您阅读我关于 Medium 的第一篇文章。我希望,我已经能够刷新你的知识。您可以在 Wikipedia 上阅读有关 SOLID 的更多信息。

相关文章
|
安全 程序员 C++
代码规范:函数设计
除非告诉人们“危险”是什么,否则这个警告牌难以起到积极有效的作用。难以理解的断言常常被程序员忽略,甚至被删除。 ↩︎
87 0
|
2月前
|
设计模式 Java 测试技术
优雅代码,建议掌握这 11个编程原则!
高质量的代码不仅让人信服,还能提升开发效率。本文总结了多位高手的经验,提炼出11条编码基本原则:DRY(避免重复)、KISS(简洁至上)、重构(优化代码)、SOLID(设计原则)、文档编写、创建优于继承、YAGNI(避免过度设计)、委托原则、始终保持代码清洁、封装变化以及优先使用组合而非继承。遵循这些原则,你的代码将更加优雅和高效。
36 3
|
4月前
|
前端开发 JavaScript 关系型数据库
|
5月前
|
开发者 Python
软件开发中的 DRY、KISS 和 SOLID 原则
**软件开发中的DRY、KISS和SOLID原则概览** - **DRY (Don't Repeat Yourself)**: 避免代码重复,确保每项知识在系统中有唯一表示,减少冗余,提高可维护性。例如,通过封装重复逻辑到函数或类。
|
7月前
|
前端开发 开发者
【专栏】BEM(Block-Element-Modifier)是一种前端命名规范和架构方法,旨在创建清晰、可维护的代码结构。
【4月更文挑战第29天】BEM(Block-Element-Modifier)是一种前端命名规范和架构方法,旨在创建清晰、可维护的代码结构。它包括Block(独立功能单元)、Element(Block的子元素)和Modifier(表示状态或变体)。BEM的特点包括命名一致性、模块化设计、清晰结构和可复用性,适用于代码组织、样式管理、组件化开发和团队协作。虽然命名较长和学习成本是其局限性,但BEM在提升代码质量和效率方面具有显著优势,是前端开发的重要工具。
125 0
|
设计模式 SQL 算法
【Java设计模式 规范与重构】 六 代码重构小结
【Java设计模式 规范与重构】 六 代码重构小结
198 0
|
JavaScript 前端开发 内存技术
彻底搞清楚 ECMAScript 的模块化(三)
彻底搞清楚 ECMAScript 的模块化
392 0
|
JavaScript 前端开发 Go
彻底搞清楚 ECMAScript 的模块化(一)
彻底搞清楚 ECMAScript 的模块化
123 0
|
JavaScript 前端开发
彻底搞清楚 ECMAScript 的模块化(二)
彻底搞清楚 ECMAScript 的模块化
144 0
|
设计模式 消息中间件 存储
软件设计 软件设计模式之SOLID原则
软件设计 软件设计模式之SOLID原则
99 0