用ts学习策略模式

策略模式 (Strategy Pattern)

定义:策略模式定义了一系列的算法,并将每一个算法封装起来,而且使他们可以相互替换,让算法独立于使用它的客户而独立变化。

参考书籍是 <<Head First 设计模式>>

我们通过阅读一个短故事,来学习策略模式所解决的问题。

先从简单点的模拟鸭子做起

Joe 上班的公司做了一套相当成功的模拟鸭子游戏:SimUDuck 。游戏中会出现各种鸭子,一边游泳戏水,一边呱呱叫。此系统内部设计使用了标准的 OO 技术,设计了一个鸭子超类 (Superclass),并让各种鸭子继承此超类。

abstract class Duck{ // 鸭子超类 (抽象基类)
    constructor(){}
    quack(){/*呱呱叫*/} // 每只鸭子都会呱呱叫由超类实现
    swim(){/*游泳戏水*/} // 每只鸭子都会游泳由超类实现
    public abstract display():void // 每只鸭子的颜色不一样由子类具体实现
}

class MallardDuck extends Duck{ // 绿头野鸭
    constructor(){
        super()
    }
    display(){
        // 外观是绿头
    }
}

class RedHeadDuck extends Duck{ // 红头鸭
    constructor(){
        super()
    }
    display(){
        // 外观是红头
    }
}

现在我们得让鸭子能飞

去年,公司的竞争压力加剧,公司主管认为是时候创新了,他们需要在“下周”的股东会议上展示一些“真正”让人印象深刻的东西来振奋人心。---让模拟程序的鸭子能飞来甩掉竞争者。

Joe 的经理拍着胸脯告诉主管们,Joe只需要一个星期就可以搞定。“毕竟,Joe 是一个 OO 程序员...这有什么困难?”

Joe 想:他只需要在 Duck 类中加上 fly() 方法, 然后所有鸭子都会继承 fly() ,正是他大显身手,展示 OO 才华的时候了。

abstract class Duck { // 鸭子超类 (抽象基类)
    // ...
    quack() { /*呱呱叫*/ }
    swim() { /*游泳戏水*/ }
    // 增加 fly() 方法
    fly() { /*让鸭子飞起来*/ }
    // ...
}

但是可怕的问题发生了...

在股东会议上出现很多 “橡皮鸭子” 在屏幕上飞来飞去。

原来 Joe 忽略了一件事: 并非所有的鸭子都会飞。

Joe 想到了继承

他可以把橡皮鸭类中的 fly() 方法覆盖掉:

class RubbrDuck extends Duck { // 橡皮鸭
    constructor(){
        super()
    }
    // ...
    fly(){ // 覆盖 fly() 方法
        // 什么都不做
    }
    quack(){/*吱吱叫*/}
}

总算是解决了这个 bug 了, Joe长吁了一口气。

可是这会带来什么问题吗?

比如以后想加入诱饵鸭 (DecoyDuck) , 又会如何? 诱饵鸭是木头假鸭,不会飞也不会叫...

这会带来很致命的维护问题- 当鸭子类越来越多功能也越来越丰富时,改变会牵一发而动全身,造成其他鸭子不想要的改变,并且运行时的行为不容易改变。

软件开发的一个不变真理

这是由于 Joe 的代码就是写死的,软件开发的一个不变真理是改变(CHANGE),不管软件设计的多好,一段时间之后,总是需要成长和改变,否则软件就会“死亡”。

所以我们要设计易扩展的程序将将经常要改变的代码抽离出来抽象成变量或者类来代替,不能写死。

把问题归零

继承不能很好的解决问题,因为鸭子的行为在子类不断的改变,而 继承的代码不是在父类写死,就是在子类写死

设计原则: 找出应用中可能需要改变的地方,把它们独立出来,不要和那些需要变化的代码混在一起

分开变化和不会变化的部分

从哪里开始呢?就我们目前所知,除了 fly() 和 quack() 的问题之外, Duck 类一切还算正常。

现在为了分开“变化和不会变化的部分”,我们准备建立两组类(完全远离Duck类),一个是 fly 相关,一个是 quack 相关的,每一组类将实现各自的动作。比方说,我们可能有一个类实现“呱呱叫”,另一个实现“吱吱叫”,还有一个实现“安静”

像这样

interface QuackBehavior{
    quack():void
}

class Quack implements QuackBehavior{
    quack(){/*实现呱呱叫*/}
}

class Squeak implements QuackBehavior{
    quack(){/*实现吱吱叫*/}
}

class MuteQuack implements QuackBehavior{
    quack(){/*什么都不做,不会叫*/}
}

interface FlyBehavior{
    fly():void
}

class FlyWithWings implements FlyBehavior{
    fly(){/*实现飞行*/}
}

class FlyNoWay implements FlyBehavior{
    fly(){
        return undefined // 什么都不做,不会飞
    }
}

以上代码运用了一个设计原则: 针对接口编程而不是针对实现编程

如果这样做那将来不论是维护还是扩展都会变得轻松我们来写一个 demo 体验一下

type FLYBEHAVIOR = FlyBehavior | undefined // 飞行联合类型
type QUACKBEHAVIOR = QuackBehavior | undefined // 嘎嘎叫联合类型
/**
 * 鸭子超类 (抽象基类)
 * @class Duck
 **/
abstract class Duck{
    public abstract flyBehavior:FlyBehavior // 拥有一个抽象的飞行行为不涉及具体实现
    public abstract quackBehavior:QuackBehavior // 拥有一个抽象的嘎嘎叫行为不涉及具体实现
    public abstract display():void // 拥有一个显示外观的抽象函数
    constructor(flyBehavior?:FlyBehavior,quackBehavior?:QuackBehavior){ // 新建Duck时支持可选的2个参数
        this.setFlyBehavior(flyBehavior)
        this.setQuackBehavior(quackBehavior)
    }
    setFlyBehavior(flyBehavior:FLYBEHAVIOR){ // 设置具体飞行行为
        this.flyBehavior = flyBehavior
    }
    setQuackBehavior(quackBehavior:QUACKBEHAVIOR){ // 设置具体嘎嘎叫行为
        this.quackBehavior = quackBehavior
    }
    performFly(){ // 执行飞行行为
        this.flyBehavior.fly()
    }
    performQuack(){ // 执行嘎嘎叫行为
        this.quackBehavior.quack()
    }
}

/**
 * 鸭子超类 (抽象基类)
 * @class MallardDuck
 **/
class MallardDuck extends Duck{ // 绿头野鸭
    public flyBehavior:FlyBehavior
    public quackBehavior:QuackBehavior
    constructor(...rest){
        super(...rest)
    }
    display(){
        console.log('显示绿色')
    }
}

let mallardDuck = new MallardDuck() // 新建一个不具备任何行为的绿头野鸭

let flyWithWings = new FlyWithWings() // 飞行

mallardDuck.setFlyBehavior(flyWithWings) // 设置飞行行为

let quack = new Quack() // 呱呱叫行为

mallardDuck.setQuackBehavior(quack) // 设置呱呱叫行为

// 这样我们就可以随意组合已经存在的任意飞行行为和各种叫声

// 测试一下

mallardDuck.performFly() //执行飞行

mallardDuck.performQuack() //执行叫声

//我们甚至可以更改绿头野鸭的叫声
let squeak = new Squeak() // 吱吱叫

mallardDuck.setQuackBehavior(squeak) // 设置吱吱叫到绿头野鸭上

mallardDuck.performQuack() //执行叫声

// 相关代码在同级目录有已经转义的js

设计原则: 多用组合,少用继承。“有一个”可能比“是一个”更好。

总结

1 不管软件设计的多好,一段时间之后,总是需要成长和改变,否则软件就会“死亡”,所以我们需要找出应用中可能需要改变的地方,把它们独立出来,不要和那些需要变化的代码混在一起。

2 抽离出来的经常变化的代码我们要针对他们的接口编程而不是针对实现编程。

3 多用组合,少用继承。“有一个”可能比“是一个”更好。

相关代码 demo01

有兴趣的朋友可以将 js 拷贝到浏览器控制台运行一下。

(完)

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