【软件设计】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 的更多信息。

相关文章
|
存储 人工智能 Kubernetes
【企业架构】什么是数据架构? 管理数据的框架
【企业架构】什么是数据架构? 管理数据的框架
|
Kubernetes Perl 容器
【kubernetes】ConfigMap: 在 Pod 中挂载文件(非目录)
【kubernetes】ConfigMap: 在 Pod 中挂载文件(非目录)
930 4
|
JavaScript 前端开发
vue router 如何使用params query传参,以及有什么区别
写在前面:传参是前端经常需要用的一个操作,很多场景都会需要用到上个页面的参数,本文将会详细介绍vue router 是如何进行传参的,以及一些小细节问题。有需要的朋友可以做一下参考,喜欢的可以点波赞,或者关注一下,希望可以帮到大家。 本文首发于我的个人blog:obkoro1.com Vue router如何传参params、query是什么? params:/router1/:id ,/router1/123,/router1/789 ,这里的id叫做params query:/router1?id=123 ,/router1?id=456 ,这里的id叫做query。
571 0
vue router 如何使用params query传参,以及有什么区别
|
算法 数据挖掘 C语言
Pandas数据分析之groupby妙用
今天本人在赶学校课程作业的时候突然发现groupby这个分组函数还是蛮有用的,有了这个分组之后你可以实现很多统计目标。 当然,最主要的是,他的使用非常简单 本期我们以上期作业为例,单走一篇文章来看看这个函数可以实现哪些功能:
Pandas数据分析之groupby妙用
|
4天前
|
云安全 人工智能 自然语言处理
|
8天前
|
人工智能 Java API
Java 正式进入 Agentic AI 时代:Spring AI Alibaba 1.1 发布背后的技术演进
Spring AI Alibaba 1.1 正式发布,提供极简方式构建企业级AI智能体。基于ReactAgent核心,支持多智能体协作、上下文工程与生产级管控,助力开发者快速打造可靠、可扩展的智能应用。
823 23
|
3天前
|
机器学习/深度学习 人工智能 自然语言处理
Z-Image:冲击体验上限的下一代图像生成模型
通义实验室推出全新文生图模型Z-Image,以6B参数实现“快、稳、轻、准”突破。Turbo版本仅需8步亚秒级生成,支持16GB显存设备,中英双语理解与文字渲染尤为出色,真实感和美学表现媲美国际顶尖模型,被誉为“最值得关注的开源生图模型之一”。
393 3
|
12天前
|
数据采集 人工智能 自然语言处理
Meta SAM3开源:让图像分割,听懂你的话
Meta发布并开源SAM 3,首个支持文本或视觉提示的统一图像视频分割模型,可精准分割“红色条纹伞”等开放词汇概念,覆盖400万独特概念,性能达人类水平75%–80%,推动视觉分割新突破。
812 59
Meta SAM3开源:让图像分割,听懂你的话