写点什么

你应该了解的 5 种 TypeScript 设计模式

2020 年 8 月 31 日

你应该了解的5种TypeScript设计模式

本文最初发布于 Medium 网站,经原作者授权由 InfoQ 中文站翻译并分享。


设计模式是解决问题的良好模板,开发人员可以在自己的项目应用这些模式处理需求。现实中应付各种需求的模式数不胜数,一篇文章无法尽述。不过它们可以大致分为三个类别:


  • 结构模式,负责处理不同组件(或类)之间的关系,并形成新结构以提供新功能。结构模式的例子有组合(Composite)、适配器(Adapter)和装饰器(Decorator)。

  • 行为模式,它们能将组件之间的通用行为抽象为一个单独的实体,进而与你的创建模式结合起来。行为模式的例子包括命令(Command)、策略(Strategy)以及我个人最喜欢的一种:观察者(Observer)模式。

  • 创建模式,它们专注于类的实例化,简化新实体的创建过程,例如工厂(Factory)方法、单例(Singleton)和抽象工厂(Abstract Factory)。


虽然它们可以直接在 JavaScript 中实现,特别是有了 ES6 后实现起来更容易了,但 TypeScript 采用的 OOP 方法使得开发人员可以简单明了地遵循通用指南(甚至来自其他 OOP 语言),从而获得这些模式的所有好处(而标准 JS 相比之下多少存在一些限制)。


单例

单例模式可能是最著名的设计模式之一。这是一种创建模式,它可以确保无论你多少次实例化一个类,你都只会有一个实例。


这是处理数据库连接之类场景的好方法,因为你可能希望一次只处理一个连接,而不必在每次用户请求时都重新连接。


//Simulate a database connectino classclass MyDBConn{    protected static instance: MyDBConn | null = null    private id: number = 0    constructor() {        //... db connection logic        this.id = Math.random() //the ID could represent the actual connection to the db    }        public getID(): number {        return this.id    }    public static getInstance(): MyDBConn {        if(!MyDBConn.instance) {            MyDBConn.instance = new MyDBConn()        }        return MyDBConn.instance    }}
const connections = [ MyDBConn.getInstance(), MyDBConn.getInstance(), MyDBConn.getInstance(), MyDBConn.getInstance(), MyDBConn.getInstance() ]connections.forEach( c => { console.log(c.getID())
复制代码


现在你不能直接实例化这个类,但使用 getInstance 方法时,你可以确保不会有多个实例。在上面的示例中,你可以看到包装数据库连接的伪类是怎样从这一模式中受益的。很容易将 id 属性视为实际连接,而这个小测试向你展示了,无论你调用 getInstance 方法多少次,“连接”总是相同的。


代码的输出是:


0.40470872509907130.40470872509907130.40470872509907130.40470872509907130.4047087250990713
复制代码


工厂方法

如前所述,工厂方法像单例一样也是一种创建模式。但这种模式不是直接针对我们要创建的对象,而只管理它们的创建过程。


解释一下:假设你要编写移动一些交通工具的代码,它们的类型有很大区别(例如汽车、自行车和飞机),移动代码应封装在每个交通工具类中,但调用这些 move 代码的方法可以是通用的。


这里的问题是如何处理对象创建?你可能有一个具有 3 个方法的 creator 类,或者一个接收一个参数的方法。无论哪种情况,要扩展这种逻辑以支持创建更多交通工具,都需要你修改同一个类。


但如果你决定使用工厂方法模式,则可以执行以下操作:



现在,创建新对象所需的代码被封装到一个新类中,每种交通工具类型都对应一个。这样如果将来需要添加新类型,则只需添加一个新的类,不必修改任何现有的类。


来看看我们如何使用 TypeScript 来实现这一点:


interface Vehicle {    move(): void}//The classes we care about, the "move" method is where our "business logic" would liveclass Car implements Vehicle {    public move(): void {        console.log("Moving the car!")    }}class Bicycle implements Vehicle {    public move(): void {        console.log("Moving the bicycle!")    }}class Plane implements Vehicle {    public move(): void {        console.log("Flying the plane!")    }}//The VehicleHandler is "abstract" because noone is going to instantiate it//We want to extend it and implement the abstract methodabstract class VehicleHandler {    //This is the method real handlers need to implement    public abstract createVehicle(): Vehicle     //This is the method we care about, the rest of the business logic resides here    public moveVehicle(): void {        const myVehicle = this.createVehicle()        myVehicle.move()    }} //Here is where we implement the custom object creationclass PlaneHandler extends VehicleHandler{    public createVehicle(): Vehicle {        return new Plane()    }}class CarHandler  extends VehicleHandler{    public createVehicle(): Vehicle {        return new Car()    }}class BicycleHandler  extends VehicleHandler{    public createVehicle(): Vehicle {        return new Bicycle()    }}/// User code...const planes = new PlaneHandler()const cars = new CarHandler()planes.moveVehicle()cars.moveVehicle()
复制代码


本质上,我们最终关心的是自定义处理程序(handler)。之所以叫它们处理程序,是因为它们不仅负责创建对象,而且具有使用它们的逻辑(如 moveVehicle 方法所示)。


这种模式的优点在于,如果要添加新的类型,你要做的就是添加其交通工具类和其处理程序类,而无需改动其他类的代码。


观察者

在所有模式中,我最喜欢的是观察者,这是因为我们可以用它来实现的行为类型。听说过 ReactJS 吗?它就是基于观察者模式的。前端 JavaScript 中的事件处理程序听过吗?也是基于它的,起码理论上是一致的。


关键在于,通过观察者模式,你可以实现它们以及更多事物。


本质上,这种模式表明你具有一组观察者对象,这些对象将对观察到的实体的状态变化做出反应。为了做到这一点,一旦观察端收到更改,就会调用一个方法来通知观察者。


实践中这种模式相对容易实现,来看代码:


type InternalState = {    event: String}abstract class Observer {     abstract update(state:InternalState): void } 
abstract class Observable { protected observers: Observer[] = [] //the list of observers protected state:InternalState = { event: "" } //the internal state observers are watching public addObserver(o:Observer):void { this.observers.push(o) } protected notify() { this.observers.forEach( o => o.update(this.state) ) }}//Actual implementationsclass ConsoleLogger extends Observer { public update(newState: InternalState) { console.log("New internal state update: ", newState) }}class InputElement extends Observable { public click():void { this.state = { event: "click" } this.notify() }}const input = new InputElement()input.addObserver(new ConsoleLogger())input.click()
复制代码


如你所见,通过两个抽象类,我们可以定义 Observer,它代表对 Observable 实体上的更改做出反应的对象。在上面的示例中,我们假装有一个被点击的 InputElement 实体(类似于你在前端的 HTML 输入字段),以及一个 ConsoleLogger,用于自动记录 Console 发生的所有事情。


这种模式的优点在于,它使我们能够了解 Observable 的内部状态并对其做出反应,而不必弄乱其内部代码。我们可以继续添加执行其他操作的 Observer,甚至包括对特定事件做出反应的观察者,然后让它们的代码决定对每个通知执行的操作。


装饰器

装饰器模式会在运行时向现有对象添加行为。从某种意义上说,你可以将其视为动态继承,因为即使你没有创建新类来添加行为,也会创建具有扩展功能的新对象。


比如你有一个带有 move 方法的 Dog 类,现在你想扩展其行为,因为你想要一只超人狗(当你让它移动时它可以飞起来)和一只游泳狗(当你告诉它移动时就钻进水里)。


一般来说,你会在 Dog 类中添加标准移动行为,然后以两种方式扩展该类,即 SuperDog 和 SwimmingDog 类。但是,如果你想将两者混合起来,则必须再创建一个新类来扩展它们的行为。其实这里有更好的方法。


组合(Composition)使你可以将自定义行为封装在不同的类中,然后使用该模式将原始对象传递给它们的构造器来创建这些类的新实例。看一下代码:


abstract class Animal {    abstract move(): void}abstract class SuperDecorator extends Animal {    protected comp: Animal        constructor(decoratedAnimal: Animal) {        super()        this.comp = decoratedAnimal    }        abstract move(): void}class Dog extends Animal {    public move():void {        console.log("Moving the dog...")    }}class SuperAnimal extends SuperDecorator {    public move():void {        console.log("Starts flying...")        this.comp.move()        console.log("Landing...")    }}class SwimmingAnimal extends SuperDecorator {    public move():void {        console.log("Jumps into the water...")        this.comp.move()    }}
const dog = new Dog()console.log("--- Non-decorated attempt: ")dog.move()console.log("--- Flying decorator --- ")const superDog = new SuperAnimal(dog)superDog.move()console.log("--- Now let's go swimming --- ")const swimmingDog = new SwimmingAnimal(dog)swimmingDog.move()
复制代码


注意一些细节:


  • 实际上,SuperDecorator 类扩展了 Animal 类,Dog 类也扩展了这个类。这是因为装饰器需要提供与其尝试装饰的类相同的公共接口。

  • SuperDecorator 类是 abstract,也就是说你实际上并没有使用它,只是用它来定义构造器,该构造器会将原始对象的副本保留在受保护的属性中。公共接口是在自定义装饰器内部完成覆盖的。

  • SuperAnimal 和 SwimmingAnimal 是实际的装饰器,它们是添加额外行为的装饰器。


这种设置的好处是,由于所有装饰器也间接扩展了 Animal 类,因此如果你要将两种行为混合在一起,则可以执行以下操作:


console.log("--- Now let's go SUPER swimming --- ")const superSwimmingDog =  new SwimmingAnimal(superDog)superSwimmingDog.move()
复制代码


如果你要使用经典继承,则动态结果会多得多。


组合

最后来看组合模式,这是打包处理多个相似对象时非常有用且有趣的模式。


这种模式使你可以将一组相似的组件作为一个组来处理,从而对它们执行特定的操作并汇总所有结果。


这种模式的有趣之处在于,它不是一个简单的对象组,它可以包含很多实体或实体组,每个组可以同时包含更多组。这就是我们所说的树。


看一个例子:


interface IProduct {        getName(): string    getPrice(): number }//The "Component" entityclass Product implements IProduct {    private price:number     private name:string        constructor(name:string, price:number) {        this.name = name        this.price = price    }        public getPrice(): number {        return this.price    }        public getName(): string {        return this.name    }}//The "Composite" entity which will group all other composites and components (hence the "IProduct" interface)class Box implements IProduct {    private products: IProduct[] = []        contructor() {        this.products = []    }        public getName(): string {        return "A box with " + this.products.length + " products"    }         add(p: IProduct):void {        console.log("Adding a ", p.getName(), "to the box")        this.products.push(p)    }    getPrice(): number {        return this.products.reduce( (curr: number, b: IProduct) => (curr + b.getPrice()),  0)    }}//Using the code...const box1 = new Box()box1.add(new Product("Bubble gum", 0.5))box1.add(new Product("Samsung Note 20", 1005))const box2 = new Box()box2.add( new Product("Samsung TV 20in", 300))box2.add( new Product("Samsung TV 50in", 800))box1.add(box2)console.log("Total price: ", box1.getPrice())
复制代码


在上面的示例中,我们可以将 Product 放入 Box 中,也可以将 Box 放入其他 Box 中。这是组合的经典示例,因为你要达到的目的是获得要交付产品的完整价格,因此你要在大 Box 中添加每个元素的价格(包括每个较小 Box 的价格)。


这样,通常称为“component”的元素是 Product 类,也称为树内的“leaf”元素。这是因为该实体没有子级。Box 类本身是组合类,具有子列表,所有子类都实现相同的接口。最后一部分代码是因为你希望能够遍历所有子项并执行相同的方法(请记住,这里的子项可以是另一个较小的组合)。


该示例的输出应为:


Adding a  Bubble gum to the boxAdding a  Samsung Note 20 to the boxAdding a  Samsung TV 20in to the boxAdding a  Samsung TV 50in to the boxAdding a  A box with 2 products to the boxTotal price:  2105.5
复制代码


因此,在处理遵循同一接口的多个对象时,请考虑使用这种模式。它将复杂性隐藏在单个实体(组合本身)中,你会发现它有助于简化与小组的互动。


小结

设计模式是用于解决问题的完美工具,但你必须先了解它们,并针对自身面对的场景做一些调整才能让它们起作用,或者修改你的业务逻辑以配合模式。无论是哪种方式,都是一项不错的投资。


你最喜欢哪种模式呢?你会经常在项目中使用它们吗?在评论中分享自己的看法吧!


原文链接:https://blog.bitsrc.io/design-patterns-in-typescript-e9f84de40449


2020 年 8 月 31 日 14:303056
用户头像
蔡芳芳 InfoQ高级编辑

发布了 582 篇内容, 共 282.1 次阅读, 收获喜欢 1841 次。

关注

评论

发布
暂无评论
发现更多内容

赞 1 收藏 分享 B站崩溃3小时引网友狂欢:A站成为最大赢家?

白亦杨

数万字总结,建议收藏慢慢看!数据库安全之MongoDB渗透

Java架构

Java spring 架构 面试 架构师

ipfs怎么个人挖矿?ipfs挖矿怎么投资?

投资矿机v:IPFS1234

ipfs怎么个人挖矿 ipfs挖矿怎么投资

JVM面试高频考点:由浅入深带你了解G1垃圾回收器!

华为云开发者社区

Java JVM 服务端 G1垃圾回收器 Java堆

网易开发三年,现跳槽蚂蚁花呗,4面顺利通过,拿下Java岗offer

周老师

Java 编程 程序员 架构 面试

存储大师班 | Linux IO 模式之 io_uring

QingStor分布式存储

Linux 文件存储 分布式存储 Linux Kenel

Water Pamola通过恶意订单对电商发起攻击

H

网络安全 安全 网络

对标阿里水准!2021年最全Java架构面试点+技术点标准手册

Java架构追梦

Java 学习 阿里巴巴 架构 面试

金九银十将至,开源一套Java面试题库,目前已成功助我拿到了3个大厂的offer

神奇小汤圆

Java 程序员 架构 面试

阿里巴巴 Java 并发系统设计笔记(全彩版)震撼来袭!

神奇小汤圆

Java 编程 架构 面试

利用WOFF模糊和电报渠道进行通信

H

网络安全 安全 网络 渗透测试·

和妹子打赌的getshell之“我不做安全了,和我一起锄大地吧”

Machine Gun

Java 数据库 网络安全 Shell 渗透测试

五面阿里巴巴拿offer后定级P6:分享Java面经及答案总结

周老师

Java 编程 程序员 架构 面试

ipfs中国授权公司都有哪些?ipfs头部矿商排名怎么看?

v:IPFS456

ipfs中国授权公司有哪些? ipfs头部矿商排名怎么看?

对标阿里水准!2021年最全Java架构面试点+技术点标准手册

架构大师

Java java面试 Java学习

最强阿里及大厂350道面试大全:框架+数据库+并发+开源+微服务

周老师

Java 编程 程序员 架构 面试

来看看CDN网络安全防护的方案

H

网络安全 安全 网络

乐活星际系统软件开发资料

开發I852946OIIO

Redisson 分布式锁源码 11:Semaphore 和 CountDownLatch

程序员小航

Java redis 源码 redssion redisson 分布式锁

fil币价格今日行情如何?fil未来价值有多大?

投资矿机v:IPFS1234

fil币价格今日行情如何 fil未来价值有多大

公开!下载量已突破新高!腾讯技术专家手撸Redis技术笔记。

马小轩

Java 编程 架构 面试 笔记

项目绩效考核管理有哪些方法?这7种考核方式值得一试!

优秀

低代码

Serverless 时代下大规模微服务应用运维的最佳实践

互联网架构师小马

想要做好微服务化,这个核心对象要管好

互联网架构师小马

当年,我是如何把微服务落地的

互联网架构师小马

阿里P7Java最全面试296题:阿里天猫、蚂蚁金服含答案文档解析

周老师

Java 编程 程序员 架构 面试

什么是 shell?

学神来啦

云计算 运维 Shell shell脚本编写

揭秘百度微服务监控:百度游戏服务监控的演进

互联网架构师小马

AQS介绍和原理分析(下)-条件中断

追风少年

并发编程 AQS JAVA;、

知识大陆软件系统开发介绍

开發I852946OIIO

2021年史上最全Java面试题:数据结构+算法+JVM+线程+finalize+GC

周老师

Java 编程 程序员 架构 面试

你应该了解的5种TypeScript设计模式-InfoQ