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

2022-03-22 12:40:05 浏览数 (1)

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

TypeScript 对用 JavaScript 编写干净的代码产生了巨大的影响。但总有改进的方法,编写更好、更简洁的代码的一个重要方法是遵循由 Robert C. Martin(也称为 Uncle Bob)发明的所谓 SOLID 设计原则。 在本文中,我将通过使用 TypeScript 编写的示例向您介绍这些原则。我已经在这个 Github 存储库上部署了所有示例。

单一职责原则 (SRP)

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

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

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

代码语言:javascript复制
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
  }
}

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

代码语言:javascript复制
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 类以计算新类的面积。这违反了开闭原则。 我们来看一下:

代码语言:javascript复制
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 类简化为一个带参数的函数,而这个参数是基于我们刚刚创建的接口。

代码语言:javascript复制
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 类。但正如我们所见,这个扩展没有任何意义,因为我们通过覆盖属性宽度和高度来改变逻辑。

代码语言:javascript复制
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 类而不改变其用途。

代码语言:javascript复制
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 的接口。但是由于我们的巨魔既不会游泳也不会说话,这个角色界面似乎不适合我们的类。

代码语言:javascript复制
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 类依赖于我们实际需要的这些接口。

代码语言:javascript复制
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 类。但这是错误的方式,因为这两个类彼此非常相似,我的意思是,它们应该做类似的事情。因此,为了实现依赖倒置原则的目标,有更好的方法来满足需求。

代码语言:javascript复制
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() 方法。

代码语言:javascript复制
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 的更多信息。

本文

https://jiagoushi.pro/solid-principles-typescript

讨论:知识星球【首席架构师圈】或者加微信小号【cea_csa_cto】或者加QQ群【792862318】

公众号

【jiagoushipro】【超级架构师】精彩图文详解架构方法论,架构实践,技术原理,技术趋势。我们在等你,赶快扫描关注吧。

微信小号

【cea_csa_cto】50000人社区,讨论:企业架构,云计算,大数据,数据科学,物联网,人工智能,安全,全栈开发,DevOps,数字化.

QQ群

【792862318】深度交流企业架构,业务架构,应用架构,数据架构,技术架构,集成架构,安全架构。以及大数据,云计算,物联网,人工智能等各种新兴技术。加QQ群,有珍贵的报告和干货资料分享。

视频号

【超级架构师】1分钟快速了解架构相关的基本概念,模型,方法,经验。每天1分钟,架构心中熟。

知识星球

向大咖提问,近距离接触,或者获得私密资料分享。

喜马拉雅

路上或者车上了解最新黑科技资讯,架构心得。

【智能时刻,架构君和你聊黑科技】

知识星球

认识更多朋友,职场和技术闲聊。

知识星球【职场和技术】

微博

【智能时刻】

智能时刻

哔哩哔哩

【超级架构师】

抖音

【cea_cio】超级架构师

快手

【cea_cio_cto】超级架构师

小红书

【cea_csa_cto】超级架构师

谢谢大家关注,转发,点赞和点在看。

0 人点赞