TypeScript 中的 SOLID 原则

C#

浏览数:260

2019-6-30

下面的文章解释了正确使用 TypeScrip的 SOLID原则。

原文地址:https://samueleresca.net/2016/08/solid-principles-using-typescript/
作者:Samuele Resca
翻译:杨晓东(Savorboard)

前言

SOLID 是由 Robert C. Martin 在面向对象设计的(OOD)中提出的五个原则,你可以在这里更一步了解关于@UncleBob,这五个原则(SOLID)就是:

  • 单一职责原则(Single Responsibility Principle):当需要修改某个类的时候原因有且只有一个
  • 开放封闭原则(Open Closed Principle):软件实体应该是可扩展,而不能可修改的
  • 里氏替换原则(Liskov Substitution Principle):子类的实例应该能够替换任何其超类的实例
  • 接口分离原则(Interface Segregation Principle):使用多个专门的接口比使用单一的总接口总要好
  • 依赖倒置原则(Dependency Inversion Principle):依赖于抽象不应该依赖于细节

这些原则使得程序员可以轻松地开发易于维护和扩展的软件。它们还使开发人员的代码能够容易地避免坏气味,轻松重构代码,并且也是敏捷或自适应软件开发的一部分。

单一责任原则(SRP)

SRP要求类只能有一个更改的原因。遵循这个原则来执行一些特定的相关任务。在考虑SRP时,你不需要将你的思维限制到类。你可以将这个原则应用到方法或者模块,确保他们仅仅只是做一件事情并且只有一个理由可以修改它们

例子 – 错误的方式

这个 Task 类定义了一些于模型相关的属性,但是它也在一个基本的数据操作上定义了一些保存实体的数据访问的方法

UML

代码

// 这个类没有遵循 SRP 原则
class Task {
    private db: Database;

    constructor(private title: string, private deadline: Date) {
        this.db = Database.connect("admin:password@fakedb", ["tasks"]);
    }

    getTitle() {
        return this.title + "(" + this.deadline + ")";
    }
    save() {
        this.db.tasks.save({ title: this.title, date: this.deadline });
    }
}

例子 – 正确的方式

UML

代码

class Task {

    constructor(private title: string, private deadline: Date) {
    }

    getTitle() {
        return this.title + "(" + this.deadline + ")";
    }


}


class TaskRepository {
    private db: Database;

    constructor() {
        this.db = Database.connect("admin:password@fakedb", ["tasks"]);
    }

    save(task: Task) {
        this.db.tasks.save(JSON.stringify(task));
    }
}

开放封闭原则(OCP)

软件实体应该对扩展开放,对修改关闭。

改变现有类的风险是,你会引入一个无意的行为变化。解决方案是创建另一个类,覆盖原始类的行为。通过OCP原则,一个组件应尽可能包含可维护并且可重复使用的代码

例子 – 正确的方式

CreditCard 类描述了一个计算 monthlyDiscount()的方法。这个 monthlyDiscount() 依赖了具体的Card类型,也就是:Silver 或者 Gold。如果要改变月度折扣计算(monthlyDiscount)那么应该建立另外一个类,重写monthlyDiscount()方法。目前这个的解决方案是新建两个类:每个类型一个类。

UML

代码

class CreditCard {
    private Code: String;
    private Expiration: Date;
    protected MonthlyCost: number;

    constructor(code: String, Expiration: Date, MonthlyCost: number) {
        this.Code = code;
        this.Expiration = Expiration;
        this.MonthlyCost = MonthlyCost;
    }

    getCode(): String {
        return this.Code;
    }

    getExpiration(): Date {
        return this.Expiration;
    }

    monthlyDiscount(): number {
        return this.MonthlyCost * 0.02;
    }

}



class GoldCreditCard extends CreditCard {

    monthlyDiscount(): number {
        return this.MonthlyCost * 0.05;
    }
}


class SilverCreditCard extends CreditCard {

    monthlyDiscount(): number {
        return this.MonthlyCost * 0.03;
    }
}

里氏替换原则(LSP)

子类不应该破坏父类的类型定义

这一原则的概念是由 Barbara Liskov 在1987年大会上发表,随后与 Jannette Wing 一起在1994年发表论文。

就这么简单,一个子类应当有一种方式覆写它的父类的方法,但是从客户的角度来看没有破坏它的功能。

例子

在下面的例子中,ItalyPostalAddress, UKPostalAddressUSAPostalAddress 继承了一个公共的基类:PostalAddress

AddressWriter 类有一个引用指向了 PostalAddress 这个基类:也就是说 参数可以被三个不同的之类替换

代码

abstract class PostalAddress {
    Addressee: string;
    Country: string
    PostalCode: string;
    City: string;
    Street: string
    House: number;

    /*
    * @returns Formatted full address
    */
    abstract WriteAddress(): string;
}

class ItalyPostalAddress extends PostalAddress {
    WriteAddress(): string {
        return "Formatted Address Italy" + this.City;
    }
}
class UKPostalAddress extends PostalAddress {
    WriteAddress(): string {
        return "Formatted Address UK" + this.City;
    }
}
class USAPostalAddress extends PostalAddress {
    WriteAddress(): string {
        return "Formatted Address USA" + this.City;
    }
}


class AddressWriter {
    PrintPostalAddress(writer: PostalAddress): string {
        return writer.WriteAddress();
    }
}

接口分离原则(ISP)

有一个很常见的现象就是,在描述一个类的时候,基本上一个接口就把它覆盖完了,就是一个接口就描述了一整个类。ISP 原则指出,我们应该写一系列更加小并且具体的接口,交给该类来实现。而每个接口只提供单一的行为

示例 – 错误的方式

下面的 Printer 接口,它有一个实现的类 SimplePrinter,该接口具有 Copy 和 Print 的功能。

interface Printer {
    copyDocument();
    printDocument(document: Document);
    stapleDocument(document: Document, tray: Number);
}


class SimplePrinter implements Printer {

    public copyDocument() {
        //...
    }

    public printDocument(document: Document) {
        //...
    }

    public stapleDocument(document: Document, tray: Number) {
        //...
    }

}

例子 – 正确的方式

下面的示例显示了将方法分组到更加具体的接口和可以被替代的方法,它描述了一些契约,他们可以被一个单独的 SimplePrinter 类,或 SimpleCopier 类,或 SuperPrinter 类实现。

interface Printer {
    printDocument(document: Document);
}


interface Stapler {
    stapleDocument(document: Document, tray: number);
}


interface Copier {
    copyDocument();
}

class SimplePrinter implements Printer {
    public printDocument(document: Document) {
        //...
    }
}


class SuperPrinter implements Printer, Stapler, Copier {
    public copyDocument() {
        //...
    }

    public printDocument(document: Document) {
        //...
    }

    public stapleDocument(document: Document, tray: number) {
        //...
    }
}

依赖倒置原则(DIP)

DIP 简单的说就超类不应该依赖于低级的组件,而应该依赖于抽象。

例子 – 错误的方式

高级的 WindowSwitch 依赖于底层低级的 CarWindow 类。

UML

代码

class CarWindow {
    open() {
        //... 
    }

    close() {
        //...
    }
}


class WindowSwitch {
    private isOn = false;

    constructor(private window: CarWindow) {

    }

    onPress() {
        if (this.isOn) {
            this.window.close();
            this.isOn = false;
        } else {
            this.window.open();
            this.isOn = true;
        }
    }
}

总结

TypeScript 可以将所有的OOP原则和实践带入到你的软件中,使用 SOLID 原则来指导你的设计模式吧。

GitHub 完整的示例代码

作者:Savorboard