TypeScript 代码整洁之道 - SOLID原则

2019-03-01  本文已影响10人  小校有来有去

将 Clean Code 的概念适用到 TypeScript,灵感来自 clean-code-javascript
原文地址: clean-code-typescript
中文地址: clean-code-typescript

简介

image

这不是一份 TypeScript 设计规范,而是将 Robert C. Martin 的软件工程著作 《Clean Code》 适用到 TypeScript,指导读者使用 TypeScript 编写易读、可复用和易重构的软件。

SOLID原则

单一职责原则 (SRP)

正如 Clean Code 中所述,“类更改的原因不应该超过一个”。将很多功能打包在一个类看起来很诱人,就像在航班上您只能带一个手提箱。这样带来的问题是,在概念上类不具有内聚性,且有很多原因去修改类。而我们应该尽量减少修改类的次数。如果一个类功能太多,修改了其中一处很难确定对代码库中其他依赖模块的影响。

反例:


class UserSettings {

  constructor(private readonly user: User) {

  }

  changeSettings(settings: UserSettings) {

    if (this.verifyCredentials()) {

      // ...

    }

  }

  verifyCredentials() {

    // ...

  }

}

正例:


class UserAuth {

  constructor(private readonly user: User) {

  }

  verifyCredentials() {

    // ...

  }

}

class UserSettings {

  private readonly auth: UserAuth;

  constructor(private readonly user: User) {

    this.auth = new UserAuth(user);

  }

  changeSettings(settings: UserSettings) {

    if (this.auth.verifyCredentials()) {

      // ...

    }

  }

}

开闭原则 (OCP)

正如 Bertrand Meyer 所说,“软件实体(类、模块、函数等)应该对扩展开放,对修改封闭。” 换句话说,就是允许在不更改现有代码的情况下添加新功能。

反例:


class AjaxAdapter extends Adapter {

  constructor() {

    super();

  }

  // ...

}

class NodeAdapter extends Adapter {

  constructor() {

    super();

  }

  // ...

}

class HttpRequester {

  constructor(private readonly adapter: Adapter) {

  }

  async fetch<T>(url: string): Promise<T> {

    if (this.adapter instanceof AjaxAdapter) {

      const response = await makeAjaxCall<T>(url);

      // transform response and return

    } else if (this.adapter instanceof NodeAdapter) {

      const response = await makeHttpCall<T>(url);

      // transform response and return

    }

  }

}

function makeAjaxCall<T>(url: string): Promise<T> {

  // request and return promise

}

function makeHttpCall<T>(url: string): Promise<T> {

  // request and return promise

}

正例:


abstract class Adapter {

  abstract async request<T>(url: string): Promise<T>;

}

class AjaxAdapter extends Adapter {

  constructor() {

    super();

  }

  async request<T>(url: string): Promise<T>{

    // request and return promise

  }

  // ...

}

class NodeAdapter extends Adapter {

  constructor() {

    super();

  }

  async request<T>(url: string): Promise<T>{

    // request and return promise

  }

  // ...

}

class HttpRequester {

  constructor(private readonly adapter: Adapter) {

  }

  async fetch<T>(url: string): Promise<T> {

    const response = await this.adapter.request<T>(url);

    // transform response and return

  }

}

里氏替换原则 (LSP)

对一个非常简单的概念来说,这是个可怕的术语。

它的正式定义是:“如果 S 是 T 的一个子类型,那么类型 T 的对象可以被替换为类型 S 的对象,而不会改变程序任何期望的属性(正确性、执行的任务等)“。这是一个更可怕的定义。

更好的解释是,如果您有一个父类和一个子类,那么父类和子类可以互换使用,而不会出现问题。这可能仍然令人困惑,所以让我们看一看经典的正方形矩形的例子。从数学上讲,正方形是矩形,但是如果您通过继承使用 “is-a” 关系对其建模,您很快就会遇到麻烦。

反例:


class Rectangle {

  constructor(

    protected width: number = 0, 

    protected height: number = 0) {

  }

  setColor(color: string) {

    // ...

  }

  render(area: number) {

    // ...

  }

  setWidth(width: number) {

    this.width = width;

  }

  setHeight(height: number) {

    this.height = height;

  }

  getArea(): number {

    return this.width * this.height;

  }

}

class Square extends Rectangle {

  setWidth(width: number) {

    this.width = width;

    this.height = width;

  }

  setHeight(height: number) {

    this.width = height;

    this.height = height;

  }

}

function renderLargeRectangles(rectangles: Rectangle[]) {

  rectangles.forEach((rectangle) => {

    rectangle.setWidth(4);

    rectangle.setHeight(5);

    const area = rectangle.getArea(); // BAD: Returns 25 for Square. Should be 20.

    rectangle.render(area);

  });

}

const rectangles = [new Rectangle(), new Rectangle(), new Square()];

renderLargeRectangles(rectangles);

正例:


abstract class Shape {

  setColor(color: string) {

    // ...

  }

  render(area: number) {

    // ...

  }

  abstract getArea(): number;

}

class Rectangle extends Shape {

  constructor(

    private readonly width = 0, 

    private readonly height = 0) {

    super();

  }

  getArea(): number {

    return this.width * this.height;

  }

}

class Square extends Shape {

  constructor(private readonly length: number) {

    super();

  }

  getArea(): number {

    return this.length * this.length;

  }

}

function renderLargeShapes(shapes: Shape[]) {

  shapes.forEach((shape) => {

    const area = shape.getArea();

    shape.render(area);

  });

}

const shapes = [new Rectangle(4, 5), new Rectangle(4, 5), new Square(5)];

renderLargeShapes(shapes);

接口隔离原则 (ISP)

“客户不应该被迫依赖于他们不使用的接口。” 这一原则与单一责任原则密切相关。这意味着不应该设计一个大而全的抽象,否则会增加客户的负担,因为他们需要实现一些不需要的方法。

反例:


interface ISmartPrinter {

  print();

  fax();

  scan();

}

class AllInOnePrinter implements ISmartPrinter {

  print() {

    // ...

  }  

  

  fax() {

    // ...

  }

  scan() {

    // ...

  }

}

class EconomicPrinter implements ISmartPrinter {

  print() {

    // ...

  }  

  

  fax() {

    throw new Error('Fax not supported.');

  }

  scan() {

    throw new Error('Scan not supported.');

  }

}

正例:


interface IPrinter {

  print();

}

interface IFax {

  fax();

}

interface IScanner {

  scan();

}

class AllInOnePrinter implements IPrinter, IFax, IScanner {

  print() {

    // ...

  }  

  

  fax() {

    // ...

  }

  scan() {

    // ...

  }

}

class EconomicPrinter implements IPrinter {

  print() {

    // ...

  }

}

依赖反转原则(Dependency Inversion Principle)

这个原则有两个要点:

  1. 高层模块不应该依赖于低层模块,两者都应该依赖于抽象。
  2. 抽象不依赖实现,实现应依赖抽象。

一开始这难以理解,但是如果你使用过 Angular,你就会看到以依赖注入(DI)的方式实现了这一原则。虽然概念不同,但是 DIP 阻止高级模块了解其低级模块的细节并进行设置。它可以通过 DI 实现这一点。这样做的一个巨大好处是减少了模块之间的耦合。耦合非常糟糕,它让代码难以重构。

DIP 通常是通过使用控制反转(IoC)容器来实现的。比如:TypeScript 的 IoC 容器 InversifyJs

反例:


import { readFile as readFileCb } from 'fs';

import { promisify } from 'util';

const readFile = promisify(readFileCb);

type ReportData = {

  // ..

}

class XmlFormatter {

  parse<T>(content: string): T {

    // Converts an XML string to an object T

  }

}

class ReportReader {

  // BAD: We have created a dependency on a specific request implementation.

  // We should just have ReportReader depend on a parse method: `parse`

  private readonly formatter = new XmlFormatter();

  async read(path: string): Promise<ReportData> {

    const text = await readFile(path, 'UTF8');

    return this.formatter.parse<ReportData>(text);

  }

}

// ...

const reader = new ReportReader();

await report = await reader.read('report.xml');

正例:


import { readFile as readFileCb } from 'fs';

import { promisify } from 'util';

const readFile = promisify(readFileCb);

type ReportData = {

  // ..

}

interface Formatter {

  parse<T>(content: string): T;

}

class XmlFormatter implements Formatter {

  parse<T>(content: string): T {

    // Converts an XML string to an object T

  }

}

class JsonFormatter implements Formatter {

  parse<T>(content: string): T {

    // Converts a JSON string to an object T

  }

}

class ReportReader {

  constructor(private readonly formatter: Formatter){

  }

  async read(path: string): Promise<ReportData> {

    const text = await readFile(path, 'UTF8');

    return this.formatter.parse<ReportData>(text);

  }

}

// ...

const reader = new ReportReader(new XmlFormatter());

await report = await reader.read('report.xml');

// or if we had to read a json report:

const reader = new ReportReader(new JsonFormatter());

await report = await reader.read('report.json');

上一章:TypeScript 代码整洁之道 - 类
下一章:TypeScript 代码整洁之道 - 测试

上一篇 下一篇

猜你喜欢

热点阅读