Typescript 下的 SOLID 原则

S.O.L.I.D 原则

  • 由 Robert C. Martin 制定
  • 由 Michael Feathers 派生出的首字母缩写词
  • 针对 OOP (面向对象编程) 制定的设计理念
  • 旨在使开发人员可以轻松创建可读和可维护的程序

S.O.L.I.D 本意

Short Spell Full Spell Chinese Translation
S Single Responsibility Principle 单一职责原则
O Open-Closed Principle 开放封闭原则
L Liskov Substitution Principle 里氏替换原则
I Interface Segregation Principle 接口隔离原则
D Dependency Inversion Principle 依赖倒置原则

Single Responsibility Principle 单一职责原则

一个类应该只负责一个职责。如果一个类有多个职责,那么它就会变得耦合。修改一项职责会导致要去修改另一项职责。单一职责原则不仅适用于类,也适用于组件和微服务

class Hero {
  constructor(name: string) {}
  getHeroName() {}
  registerHero(hero: Hero) {}
}

Hero 类违反了 SRP。SRP 声明类应该有一个责任,在这里 Hero 类拥有 2 个职责:管理 Hero 的属性与管理 Hero 的数据。

带来的问题

如果程序以影响数据库管理功能的方式更改,必须触及并重新编译使用 Hero 属性的类以抵偿新的更改,时间越久远越容易引发多米诺效应,影响到整个程序链路。

代码的调整

class Hero {
  constructor(name: string) {}
  getHeroName() {}
}
class HeroDB {
  getHero(hero: Hero) {}
  setHero(hero: Hero) {}
}

名言的引用

我们在设计类的时候,应该将相关的功能放在一起,每当要变化的时候,它们会出于同样的原因而改变。如果功能因不同原因而发生变化,我们应该尝试将功能分开。 —— 史蒂夫芬顿

Open-Closed Principle 开放封闭原则

软件实体(类,模块,函数)应该是可以扩展的,而不是修改。

class Hero {
  constructor(name: string) {}

  whichHero(heroes: Array<Hero>) {
    const heroesArr = ["SuperMan", "IronMan"];
    for (hero of heroes) {
      if (heroesArr.inclues(hero.name)) console.log(`I am ${hero.name}!`);
    }
  }
}

带来的问题

whichHero 方法不符合开放封闭原则,因为它不能对新创造的 Hero 进行封闭。如果我们新创造另一个 Hero,就必须对 whichHero 进行修改。

class Hero {
  constructor(name: string) {}

  whichHero(heroes: Array<Hero>) {
    const heroesArr = ["SuperMan", "IronMan", "AntMan"];
    for (hero of heroes) {
      if (heroesArr.inclues(hero.name)) console.log(`I am ${hero.name}!`);
    }
  }
}

每次新增一个 Hero,whichHero 函数都需要添加新的数组元素到 heroesArr 变量中。当然这里只是简单地添加数组元素,但当应用程序应对的是更为复杂的逻辑并且有不断增长的趋势,我们将应对的不仅仅是添加新的数组元素到 heroesArr 变量中这么简单了。

代码的调整

class Hero {
  constructor(name: string) {}

  whichHero() {}
}

class SuperMan extends Hero {
  whichHero() {
    console.log(`I am ${this.name}`);
  }
}

class IronMan extends Hero {
  whichHero() {
    console.log(`I am ${this.name}`);
  }
}

class AntMan extends Hero {
  whichHero() {
    console.log(`I am ${this.name}`);
  }
}

现在 Hero 有一个虚拟方法 whichHero,供不同的 Hero (Like SuperMan)实现各自的虚拟 whichHero 方法。这样各子类不仅能实现 whichHero,并且能自由地重载 whichHero,不需要像原来一样还得往 heroesArr 中添加新的数组元素来进行修改。

Liskov Substitution Principle 里氏替换原则

子类必须可以替换其父类,任何父类可以出现的地方,子类也一定可以出现。

假如一个能提供定制的功能给终端用户的大型门户网站,系统根据用户的级别提供不同级别的设定,设计一个 ISettings 接口以及实现 ISettings 的 GlobalSettings、SectionSettings UserSettings 3 个类。GlobalSettings 为全局设定,如标题、主题等,SectionSettings 为局部设定,如新闻、天气、体育等,UserSettings 为用户设定,如电子邮件、通知偏好等。

interface ISettings {
  get(): void;
  set(): void;
}

class GlobalSettings implements ISettings {
  get(): void {}
  set(): void {}
}

class SectionSettings implements ISettings {
  get(): void {}
  set(): void {}
}

class UserSettings implements ISettings {
  get(): void {}
  set(): void {}
}

带来的问题

当单独使用 GuestSettings 时,因为我们了解游客不能设置,所以我们潜意识并不会主动调用 set 方法。但是由于多态,ISettings 接口的实现可以被替换为 GuestSettings 对象,当调用 set 方法时,可能会引发系统异常。

代码的调整

将 ISettings 拆分为两个不同的接口:IReadableSettings 与 IWritableSettings。子类根据需求实现所需的接口。

interface IReadableSettings {
  get(): void;
}

interface IWritableSettings {
  set(): void;
}

class GlobalSettings implements IReadableSettings, IWritableSettings {
  get(): void {}
  set(): void {}
}

class SectionSettings implements IReadableSettings, IWritableSettings {
  get(): void {}
  set(): void {}
}

class UserSettings implements IReadableSettings, IWritableSettings {
  get(): void {}
  set(): void {}
}

class GuestSettings implements IReadableSettings {
  get(): void {}
}

现在 GuestSettings 就只实现了 IReadableSettings 的 get 方法,不像原来一样还需要实现 ISettings 的 set 方法,确保系统不抛出异常也提供给游客的基本操作

Interface Segregation Principle 接口隔离原则

制作客户特定的细粒度接口不应强迫客户端依赖于他们不使用的接口。

interface IShape {
  drawCircle(): void;
  drawSquare(): void;
  drawRectangle(): void;
}

class Circle implements IShape {
  drawCircle() {}
  drawSquare() {}
  drawRectangle() {}
}

class Square implements IShape {
  drawCircle() {}
  drawSquare() {}
  drawRectangle() {}
}

class Rectangle implements IShape {
  drawCircle() {}
  drawSquare() {}
  drawRectangle() {}
}

带来的问题

此接口绘制正方形,圆形,矩形。实现 IShape 接口的类 Circle,Square 或 Rectangle 都必须定义 drawCircle(),drawSquare(),drawRectangle()方法。类 Rectangle 实现了它没有使用的方法 drawCircle 与 drawSquare,类 Square 实现了它没有使用的方法 drawCircle 与 drawRectangle,类 Circle 实现了它没有使用的方法 drawSquare 与 drawRectangle。如果此时向 IShape 接口添加另一个方法,比如 drawTriangle()

interface IShape {
  drawCircle();
  drawSquare();
  drawRectangle();
  drawTriangle();
}

类必须实现新方法 drawTriangle()否则将抛出错误。实现一个可以绘制圆形而不实现绘制矩形,正方形与三角形的形状是不可能的。IShape 接口的设计不符合接口隔离原则,类 Circle,Square 与 Rectangle 不应强制依赖于它们不需要或不使用的方法。此外,ISP 声明接口应该只执行一件事情(就像 SRP 原则一样),任何额外的行为都应该被抽象到另一个接口。因此 IShape 接口的执行应由其他接口独立处理操作。

代码的调整

interface IShape {
  draw();
}

interface ICircle {
  drawCircle();
}

interface ISquare {
  drawSquare();
}

interface IRectangle {
  drawRectangle();
}

interface ITriangle {
  drawTriangle();
}

class Shape implements IShape {
  draw() {}
}

class Circle implements ICircle {
  drawCircle() {}
}

class Square implements ISquare {
  drawSquare() {}
}

class Rectangle implements IRectangle {
  drawRectangle() {}
}

class Triangle implements ITriangle {
  drawTriangle() {}
}

IShape 接口可以绘制任何形状, ICircle 接口只绘制圆形,ISquare 接口只绘制正方形,IRectangle 接口只绘制矩形,ITriangle 接口只绘制三角形。

Or

类 Circle,Square,Rectangle 与 Triangle 可以从 IShape 接口继承并实现它们自己的绘制行为。

interface IShape {
  draw();
}

class Circle implements IShape {
  draw() {}
}

class Square implements IShape {
  draw() {}
}

class Rectangle implements IShape {
  draw() {}
}

class Triangle implements IShape {
  draw() {}
}

这样当需要绘制其他图形时只需要从 IShape 接口继承并实现其绘制行为即可。

Dependency Inversion Principle 依赖倒置原则

依赖应该基于抽象而不是具体,通常采用依赖注入的方法来解决违反依赖倒置原则的情况。

  • 抽象不应依赖于具体。具体应取决于抽象
  • 高级模块不应依赖于低级模块。两者都应依赖于抽象
class XMLHttpRequestService {
  request(url: string, options: any) {}
}

class XMLHttpService extends XMLHttpRequestService {}

class Http {
  constructor(private xmlhttpService: XMLHttpService) {}
  get(url: string, options: any) {
    this.xmlhttpService.request(url, options);
  }
  post(url: string, options: any) {
    this.xmlhttpService.request(url, options);
  }
}

带来的问题

这里 Http 是高级组件,而 XMLHttpService 是低级组件。此设计违反了 DIP 原则(高级模块不应依赖于低级模块。它应该取决于它的抽象),Http 类被迫依赖于 XMLHttpService 类。如果我们变更 Http 的连接服务类型为 Node.js 或者只是单纯地进行 Mock,就必须更改所有的 Http 实例代码,这也违反了 OCP 原则。Http 类应该更少关注正在使用的 Http 服务的类型。

代码的调整

interface Connection {
  request(url: string, options: any);
}

class XMLHttpService implements Connection {
  request(url: string, options: any) {}
}

class NodeHttpService implements Connection {
  request(url: string, options: any) {}
}

class MockHttpService implements Connection {
  request(url: string, options: any) {}
}

class Http {
  constructor(private httpConnection: Connection) {}
  get(url: string, options: any) {
    this.httpConnection.request(url, options);
  }
  post(url: string, options: any) {
    this.httpConnection.request(url, options);
  }
}

现在无论传递给 Http 的连接服务的类型如何,它都可以轻松地进行网络请求,并且可以看到高级模块和低级模块都依赖于抽象——Http 类(高级模块)依赖于 Connection 接口(抽象),Http Service 类(低级模块)依赖于 Connection 接口(抽象)。此外,DIP 将强制我们不能违反 LSP:连接服务类型 (XML|Node|Mock)HttpService 可替换其父类型 Connection。

结论

S.O.L.I.D 原则是每个开发者都必须遵守的,需要开发者通过在不断的实践中遵守,最终它们将成为编程的一部分,并将使得应用程序的可读性与可维护性大大增强。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,417评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,921评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,850评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,945评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,069评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,188评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,239评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,994评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,409评论 1 304
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,735评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,898评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,578评论 4 336
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,205评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,916评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,156评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,722评论 2 363
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,781评论 2 351

推荐阅读更多精彩内容