专栏首页code秘密花园基于 TypeScript 理解程序设计的 SOLID 原则

基于 TypeScript 理解程序设计的 SOLID 原则

大家好,我是 ConardLi,今天我们来基于 TypeScript 回顾学习下程序设计中的 SOLID 原则。

说到 SOLID 原则,可能写过代码的同学们应该都听过吧,这是程序设计领域最常用到的设计原则。SOLID罗伯特·C·马丁 在 21 世纪早期引入,指代了面向对象编程和面向对象设计的五个基本原则, SOLID 其实是以下五个单词的缩写:

  • Single Responsibility Principle:单一职责原则
  • Open Closed Principle:开闭原则
  • Liskov Substitution Principle:里氏替换原则
  • Interface Segregation Principle:接口隔离原则
  • Dependency Inversion Principle:依赖倒置原则

TypeScript 的出现让我们可以用面向对象的思想编写出更简洁的 JavaScript 代码,在下面的文章中,我们将用 TypeScript 编写一些示例来分别解释下这些原则。

单一职责原则(SRP)

核心思想:类的职责应该单一,不要承担过多的职责。

我们先看看下面这段代码,我们为 Book 创建了一个类,但是类中却承担了多个职责,比如把书保存为一个文件:

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 类来负责计算 RectangleCircle 类的面积。想象一下,如果我们后续要再添加一个形状,我们要创建一个新的类,同时我们也要去修改 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;
  }
}

好处:增强程序的健壮性,即使增加了子类,原有的子类还可以继续运行。

接口隔离原则(ISP)

核心思想:类间的依赖关系应该建立在最小的接口上。简单理解就是接口的内容一定要尽可能地小,能有多小就多小。我们要为各个类建立专用的接口,而不要试图去建立一个很庞大的接口供所有依赖它的类去调用。

看看下面的代码,我们有一个名为 Troll 的类,它实现了一个名为 Character 的接口,但是 Troll 既不会游泳也不会说话,所以它似乎不太适合实现我们的接口:

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 类,它初始化了 FrontendDeveloperBackendDeveloper 类:

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 接口,由于 FrontendDeveloperBackendDeveloper 是相似的类,它们都依赖于 Developer 接口。

我们不需要在 SoftwareProject 类中以单一方式初始化 FrontendDeveloperBackendDeveloper,而是将它们作为一个列表来遍历它们,分别调用每个 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();
    });
  }
}

好处:实现模块间的松耦合,更利于多模块并行开发。

参考

  • https://blog.bitsrc.io/solid-principles-in-typescript-153e6923ffdb
  • https://www.zhihu.com/search?type=content&q=SOLID%20%E5%8E%9F%E5%88%99
  • https://mp.weixin.qq.com/s/jHrdxu5oQnz2Rn4Uv5OQcw
文章分享自微信公众号:
code秘密花园

本文参与 腾讯云自媒体分享计划 ,欢迎热爱写作的你一起参与!

作者:ConardLi
原始发表时间:2022-03-24
如有侵权,请联系 cloudcommunity@tencent.com 删除。
登录 后参与评论
0 条评论

相关文章

  • 面向对象的程序设计原则之SOLID原则

    Ò程序设计领域, SOLID (单一功能、开闭原则、里氏替换、接口隔离以及依赖反转)是由罗伯特•C•马丁在21世纪早期 引入的记忆术首字母缩略字,指代了面向对象...

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

    了解有关 TypeScript 中 SOLID 原则的更多信息 TypeScript 对用 JavaScript 编写干净的代码产生了巨大的影响。但总有改进的方...

    首席架构师智库
  • 不做翻译需求的程序员 + 赠书

    最近在做大量的代码 Review 的工作,尝试整理出一些大家在写代码时要避免的一些问题,同时也在读《代码整洁之道》和《代码里的世界观》。也在知识星球(同公众号名...

    the5fire
  • 用 SOLID 原则保驾 React 组件开发

    本世纪初,美国计算机专家和作者 Robert Cecil Martin 针对 OOP 编程,提出了可以很好配合的五个独立模式;后由重构等领域的专家 Michae...

    江米小枣
  • 【译】浅谈SOLID原则

    SOLID原则是一种编码的标准,为了避免不良设计,所有的软件开发人员都应该清楚这些原则。SOLID原则是由Robert C Martin推广并被广泛引用于面向对...

    Jackeyzhe
  • C# 开发者审查代码的41条建议

    1. 确保没有任何警告(warnings)。 2.如果先执行Code Analysis(启用所有Microsoft Rules)再消除所有警告就更好了。 3. ...

    hbbliyong
  • 经典永不过时!重温设计模式

    | 导语 在软工程中,设计模式(design pattern)是对软件设计中普遍存在(反复出现)的各种问题,所提出的解决方案。这个术语是由埃里希·伽玛(Eric...

    DeROy
  • 基于jsoneditor二次封装一个可实时预览的json编辑器组件(react版)

    做为一名前端开发人员,掌握vue/react/angular等框架已经是必不可少的技能了,我们都知道,vue或react等MVVM框架提倡组件化开发,这样一方面...

    徐小夕
  • 开发那么久,才知道的 SOLID 设计原则

    无论是软件系统设计,还是代码实现,遵循有效和明确的设计原则,都利于系统软件灵活可靠,安全快速的落地,更重要的是能灵活地应对需求,简化系统扩展和维护,避免无效的加...

    闻人的技术博客
  • 前端: 从零封装一个可实时预览的json编辑器

    做为一名前端开发人员,掌握vue/react/angular等框架已经是必不可少的技能了,我们都知道,vue或react等MVVM框架提倡组件化开发,这样一方面...

    徐小夕
  • 程序員必須知道的面向对象设计六大原则

    在程序设计领域, SOLID(单一功能、开闭原则、里氏替换、接口隔离以及依赖反转)是由罗伯特·C·马丁在21世纪早期 引入的记忆术首字母缩略字,指代了面向对象编...

    居士
  • 2017年10大主流编程语言最新排行榜出炉

    前言 据美国科技公司Gizmodo报道,截至2014年9月24日,全世界采用IT操作系统的设备数量已经达到10亿台,IT推动中国移动互联网进入高速发展期,成为所...

    企鹅号小编
  • 如何避免在Vue应用中违反SOLID原则

    接下来我们看看如何在 Vue 实战中避免这些原则,我们从一个 TODO LIST 项目中去体会这些观点。

    码农小余
  • 重新温习软件设计之路(3)

    本文是我学习课程《软件设计之美》的学习总结第三部分,分享面向对象的三个特点和五个设计原则的理解。

    Edison Zhou
  • 前端之变(七): 前端的困境

    这些年,前端发生了颠覆性的变革,这种变化极大地改变了前端的生态,前端从很多年前一个不起眼的角色跨步式的演变为一个不可或缺的存在。

    御剑
  • 2017年10大主流编程语言最新排行榜出炉

    王小婷
  • 面向对象三大特性-----封装、继承、多态

      前面有文章写到了面向对象编程和面向对象设计的头五大原则(SOLID五大原则)。今天我们再来谈谈面向对象的三大特性--封装、继承、多态

    小世界的野孩子
  • TypeScript 入门教程

    本文转载自https://ts.xcatliu.com/,作者是:xcatliu,文档对应的Github托管地址为:typescript-tutorial

    ccf19881030

扫码关注腾讯云开发者

领取腾讯云代金券