以游戏玩家的视角开启设计模式

6,618 阅读16分钟

前言

最近学习设计模式和TypeScript,发现网上的资料略显硬核,不太容易理解记忆,经常看完就忘。作为一名游戏玩家,发现游戏中的很多场景都能和相应的设计模式相关联,不仅便于理解,更利于合理地运用设计模式。由于个人水平有限,只整理个人觉得比较有趣的设计模式,每个模式采用哲学三问进行讲解。如果对你有帮助的话,欢迎点赞和收藏💖,图片源自网络,侵删。

目录

Snipaste_2020-06-07_17-36-43.png

设计模式

单例模式

What

定义:一个类仅有一个实例,并提供一个访问它的全局访问点

解释:实质上就是起到一个存储全局变量的作用,其他的类和模块只能通过该类提供的接口修改其唯一实例

Game:在DNF一起组团刷本的日子中,副本BOSS看作为一个单例,玩家可以通过各种技能或者平A去消耗BOSS

并且该本中的所有玩家都是对同一个BOSS造成伤害

image

How

  • BOSS为单例,只会被实例化一次
  • 不同玩家使用不同攻击方式对BOSS造成伤害
  • BOSS受到的伤害为玩家们造成伤害的总和

TS版本

class Boss{
    private static instance: Boss = null;
    private hp: number = 1000;

    getInjured(harm: number){
        this.hp -= harm;
    }
    getHp(): number{
        return this.hp;
    }
    static getInstance(): Boss{
        // 如果已经实例化了那么直接返回
        if(!this.instance){
            this.instance = new Boss();
        }
        return this.instance;
    }
}

class Player{
    constructor(private name: string){}
    attack(harm: number,boss: Boss): void{
        boss.getInjured(harm);
        console.log(`我是一名${this.name},打出了${harm}伤害,BOSS还剩${boss.getHp()}血`)
    }
}

let p1: Player = new Player('鬼泣');
let p2: Player = new Player('街霸');
let p3: Player = new Player('阿修罗');
// 对同一个boss疯狂输出
p1.attack(100,Boss.getInstance());
p2.attack(80,Boss.getInstance());
p3.attack(120,Boss.getInstance());

// 我是一名鬼泣,打出了100伤害,BOSS还剩900血
// 我是一名街霸,打出了80伤害,BOSS还剩820血
// 我是一名阿修罗,打出了120伤害,BOSS还剩700血

JS版本

var Boss = /** @class */ (function () {
    function Boss() {
        this.hp = 1000;
    }
    Boss.prototype.getInjured = function (harm) {
        this.hp -= harm;
    };
    Boss.prototype.getHp = function () {
        return this.hp;
    };
    Boss.getInstance = function () {
        // 如果已经实例化了那么直接返回
        if (!this.instance) {
            this.instance = new Boss();
        }
        return this.instance;
    };
    Boss.instance = null;
    return Boss;
}());
var Player = /** @class */ (function () {
    function Player(name) {
        this.name = name;
    }
    Player.prototype.attack = function (harm, boss) {
        boss.getInjured(harm);
        console.log("我是一名" + this.name + ",打出了" + harm + "伤害,BOSS还剩" + boss.getHp() + "血");
    };
    return Player;
}());
var p1 = new Player('鬼泣');
var p2 = new Player('街霸');
var p3 = new Player('阿修罗');
// 对同一个boss疯狂输出
p1.attack(100, Boss.getInstance());
p2.attack(80, Boss.getInstance());
p3.attack(120, Boss.getInstance());

// 我是一名鬼泣,打出了100伤害,BOSS还剩900血
// 我是一名街霸,打出了80伤害,BOSS还剩820血
// 我是一名阿修罗,打出了120伤害,BOSS还剩700血

Why

  • 优点
    • 单例模式提供了对共享资源访问的唯一接口,避免了对共享资源的多重占用
    • 单例模式只会实例化一次对象,节约内存开销
  • 缺点
    • 滥用单例模式就跟滥用全局变量一样,未使用的引用对象会被GC自动回收
    • 单例模式中复杂的功能可能会违背单一职责原则
  • 应用场景:针对一些静态的共享资源,可以采用单例模式对其进行访问。从此处可以看出举的游戏例子本身不太适合用单例模式(因为它是一个频繁变动的对象)

策略模式

What

定义:策略模式指的是定义一系列的算法,把它们一个个封装起来。将不变的部分和变化的部分隔开是每个设计模式的主题,策略模式也不例外,策略模式的目的就是将算法的使用与算法的实现分离开来。

解释:常言道条条大路通罗马,所谓策略模式也就是为实现某种功能而采取不同的策略算法

Game:在炉石传说游戏中,盗贼的两套经典卡牌奇迹贼爆牌贼,虽然都是盗贼玩家取胜的法宝,但这两套牌的取胜之道却截然不同。奇迹贼依靠一回合的极限操作往往能转危为安,让对面突然去世;而爆牌贼则是以疲劳和爆对面key牌的运营方式取胜。

image

How

  • 奇迹贼爆牌贼都是盗贼的卡组
  • 奇迹贼靠一回合的极限操作
  • 爆牌贼靠长时间的运营

TS版本

interface Deck{
    name: string;
    kill(): void; 
}

class MiracleDeck implements Deck{
    name: string = '奇迹贼';
    kill(){
        console.log(`${this.name}:十七张牌就是能秒你`);
    }
}

class ExplosiveDeck implements Deck{
    name: string = '爆牌贼';
    kill(){
        console.log(`${this.name}:我要爆光你的牌库!`)
    }
}

class Robber{
    private deck: Deck;
    setDeck(deck: Deck){
        this.deck = deck;
    } 
    winTheGame(): void{
        this.deck.kill();
    };
 }

 let rb = new Robber();
 rb.setDeck(new MiracleDeck());
 rb.winTheGame();
 rb.setDeck(new ExplosiveDeck());
 rb.winTheGame();

// 奇迹贼:十七张牌就是能秒你
// 爆牌贼:我要爆光你的牌库!

JS版本

var MiracleDeck = /** @class */ (function () {
    function MiracleDeck() {
        this.name = '奇迹贼';
    }
    MiracleDeck.prototype.kill = function () {
        console.log(this.name + "十七张牌就是能秒你");
    };
    return MiracleDeck;
}());
var ExplosiveDeck = /** @class */ (function () {
    function ExplosiveDeck() {
        this.name = '爆牌贼';
    }
    ExplosiveDeck.prototype.kill = function () {
        console.log(this.name + "我要爆光你的牌库!");
    };
    return ExplosiveDeck;
}());
var Robber = /** @class */ (function () {
    function Robber() {
    }
    Robber.prototype.setDeck = function (deck) {
        this.deck = deck;
    };
    Robber.prototype.winTheGame = function () {
        this.deck.kill();
    };
    ;
    return Robber;
}());
var rb = new Robber();
rb.setDeck(new MiracleDeck());
rb.winTheGame();
rb.setDeck(new ExplosiveDeck());
rb.winTheGame();
// 奇迹贼:十七张牌就是能秒你
// 爆牌贼:我要爆光你的牌库!

Why

  • 优点
    • 算法可以自由切换,可扩展性好
  • 缺点
    • 策略类会增多并且每个策略类都需要向外暴露
  • 应用场景:多态场景,如分享功能,分享到不同平台的内部实现是不相同的

代理模式

What

定义:为一个对象提供一个代理者,以便控制对它的访问

解释:比如明星和经纪人的关系,经纪人会帮助明星处理商演赞助等细节问题,明星负责签字就好

Game:作为一个FM懒人玩家,只想把时间花在看模拟比赛上,其他不想做的事就很开心地甩给助理教练啦

Snipaste_2020-06-06_18-13-30.png

How

  • 玩家只需要设定战术,观看比赛
  • 助理教练负责赛前针对性设置,球队训话以及赛后的新闻发布会

TS版本

interface Match{
    play(): void;
}

class Manager implements Match{
    play(): void{
        console.log('比赛开始了,是由皇家马德里对阵拜仁慕尼黑。。。')
    }
}

class Cotch implements Match{
    private manager: Manager = new Manager();
    beforePlay(): void{
        console.log('布置战术');
        console.log('球队训话');
    }
    afterPlay(): void{
        console.log('赛后采访');
    }
    play(): void{
        this.beforePlay();
        this.manager.play();
        this.afterPlay();
    }
}

let proxy: Cotch = new Cotch();
proxy.play();

// 布置战术
// 球队训话
// 比赛开始了,是由皇家马德里对阵拜仁慕尼黑。。。
// 赛后采访

JS版本

var Manager = /** @class */ (function () {
    function Manager() {
    }
    Manager.prototype.play = function () {
        console.log('比赛开始了,是由皇家马德里对阵拜仁慕尼黑。。。');
    };
    return Manager;
}());
var Cotch = /** @class */ (function () {
    function Cotch() {
        this.manager = new Manager();
    }
    Cotch.prototype.beforePlay = function () {
        console.log('布置战术');
        console.log('球队训话');
    };
    Cotch.prototype.afterPlay = function () {
        console.log('赛后采访');
    };
    Cotch.prototype.play = function () {
        this.beforePlay();
        this.manager.play();
        this.afterPlay();
    };
    return Cotch;
}());
var proxy = new Cotch();
proxy.play();
// 布置战术
// 球队训话
// 比赛开始了,是由皇家马德里对阵拜仁慕尼黑。。。
// 赛后采访

Why

  • 优点
    • 将真实对象和目标对象分类
    • 降低代码的耦合度,扩展性好
  • 缺点
    • 类数量增加了,增大了系统复杂度,降低了系统的性能
  • 应用场景:可以拦截一些请求,如ES6中的Proxy

发布-订阅模式

What

定义:对象间的一种一对多的关系,让多个观察者对象同时监听某一个主题对象,当一个对象发生改变时,所有依赖于它的对象都将得到通知

解释:比如JS中的addEventListener

Game:进游戏后你会订阅队友和敌人的生存状态,当生存状态发生变化时,系统会及时通知你

Snipaste_2020-06-06_19-14-46.png

How

  • 玩家需要订阅队友和敌人的状态
  • 状态需要被触发
  • 系统会通知你改变后的状态

TS版本

class EventEmitter{
    private events: Object = {};    // 存储事件
    private key: number = 0;    // 事件的唯一标识key

    on(name: string,event: any): number{
        event.key = ++this.key;
        this.events[name] ? this.events[name].push(event) 
                          : (this.events[name] = []) && this.events[name].push(event);
        return this.key;
    }

    off(name: string,key: number){
        if(this.events[name]){
            this.events[name] = this.events[name].filter(x => x.key !== key);
        }else{
            this.events[name] = [];
        }
    }

    emit(name: string,key?: number){
        if(this.events[name].length === 0 ) throw Error(`抱歉,你没有定义 ${name}监听器`)
        if(key){
            this.events[name].forEach(x => x.key === key && x());
        }else {
            this.events[name].forEach(x => x());
        }
    }
}

let player: EventEmitter = new EventEmitter();
player.on('friendDied',function(): void{
    console.log('你可爱的队友已被击杀');
})
player.on('enemyDied',function(): void{
    console.log('打的好呀,小帅哥')
})
// 模拟战况
let rand: number;
let k: number = 1;
while(k < 10){
    rand = Math.floor(Math.random() * 10);
    if(rand % 2 === 0){
        player.emit('friendDied');
    }else{
        player.emit('enemyDied');
    }
    k++;
}

// 你可爱的队友已被击杀
// 打的好呀,小帅哥
// 你可爱的队友已被击杀
// 你可爱的队友已被击杀
// 打的好呀,小帅哥
// 你可爱的队友已被击杀
// 你可爱的队友已被击杀
// 打的好呀,小帅哥
// 打的好呀,小帅哥

JS版本

var EventEmitter = /** @class */ (function () {
    function EventEmitter() {
        this.events = {}; // 存储事件
        this.key = 0; // 事件的唯一标识key
    }
    EventEmitter.prototype.on = function (name, event) {
        event.key = ++this.key;
        this.events[name] ? this.events[name].push(event)
            : (this.events[name] = []) && this.events[name].push(event);
        return this.key;
    };
    EventEmitter.prototype.off = function (name, key) {
        if (this.events[name]) {
            this.events[name] = this.events[name].filter(function (x) { return x.key !== key; });
        }
        else {
            this.events[name] = [];
        }
    };
    EventEmitter.prototype.emit = function (name, key) {
        if (this.events[name].length === 0)
            throw Error("\u62B1\u6B49\uFF0C\u4F60\u6CA1\u6709\u5B9A\u4E49 " + name + "\u76D1\u542C\u5668");
        if (key) {
            this.events[name].forEach(function (x) { return x.key === key && x(); });
        }
        else {
            this.events[name].forEach(function (x) { return x(); });
        }
    };
    return EventEmitter;
}());
var player = new EventEmitter();
player.on('friendDied', function () {
    console.log('你可爱的队友已被击杀');
});
player.on('enemyDied', function () {
    console.log('打的好呀,小帅哥');
});
// 模拟战况
var rand;
var k = 1;
while (k < 10) {
    rand = Math.floor(Math.random() * 10);
    if (rand % 2 === 0) {
        player.emit('friendDied');
    }
    else {
        player.emit('enemyDied');
    }
    k++;
}
// 你可爱的队友已被击杀
// 打的好呀,小帅哥
// 你可爱的队友已被击杀
// 你可爱的队友已被击杀
// 打的好呀,小帅哥
// 你可爱的队友已被击杀
// 你可爱的队友已被击杀
// 打的好呀,小帅哥
// 打的好呀,小帅哥

Why

  • 优点
    • 很好的实现事件推送,建立一套完备的触发机制
  • 缺点
    • 订阅数量过多的话,触发有一定时延
  • 应用场景JS中的addEventListenerRedux中的数据流模型

中介者模式

What

定义:一个中介对象来封装一系列对象之间的交互,使原有对象之间的耦合松散,且可以独立地改变它们之间的交互。

解释:简单来说就是模块之间通信的中间商

Game:FIFA Online中,你可以在交易系统上上架球员卡,然后该商品会被交易系统转发给其他玩家

Snipaste_2020-06-06_19-14-46.png

How

  • 玩家1上架球员卡
  • 交易中介系统转发信息
  • 玩家2,3收到新上架商品的信息

TS版本

abstract class Shop {
    //存储
    public fifaers: Object = {}
    //注册
    public register(name: string, fifaer: Fifaer): void {
        if (this.fifaers[name]) {
            console.error(`${name}名称已存在`);
        } else {
            this.fifaers[name] = fifaer;
            fifaer.setMedium(this);
        }
    }
    //转发
    public relay(fifaer: Fifaer, message?: any): void {
        Object.keys(this.fifaers).forEach((name: string) => {
            if (this.fifaers[name] !== fifaer) {
                this.fifaers[name].receive(message);
            }
        })
    }
}
//抽象玩家类
abstract class Fifaer {
    protected mediator: Shop;
    public setMedium(mediator: Shop): void {
        this.mediator = mediator;
    }
    public receive(message?: any): void {
        console.log(this.constructor.name + "收到请求:", message);
    }
    public send(message?: any): void {
        console.log(this.constructor.name + "上架新卡:", message);
        this.mediator.relay(this, message); //请中介者转发
    }
}


//具体中介者
class ConcreteShop extends Shop {
    constructor() {
        super()
    }
}
//具体玩家1
class Fifaer1 extends Fifaer {
    constructor() {
        super()
    }
    public receive(message?: any): void {
        console.log(`${message} 对于我来说太贵了`)
    }
}
//具体玩家2
class Fifaer2 extends Fifaer {
    constructor() {
        super()
    }
    public receive(message?: any): void {
        console.log(`${this.constructor.name}: ${message} 对于我来说刚刚好`)
    }
}
//具体玩家3
class Fifaer3 extends Fifaer {
    constructor() {
        super()
    }
    public receive(message?: any): void {
        console.log(`${this.constructor.name}: ${message} 对于我来说太便宜了`)
    }
}

let shop: Shop = new ConcreteShop();
let f1: Fifaer = new Fifaer1();
let f2: Fifaer = new Fifaer2();
let f3: Fifaer = new Fifaer3();
shop.register('Ronaldo',f1);
shop.register('Messi',f2);
shop.register('Torres',f3);
f1.send('托雷斯的巅峰卡: 1E ep');

// Fifaer1上架新卡: 托雷斯的巅峰卡: 1E ep
// Fifaer2: 托雷斯的巅峰卡: 1E ep 对于我来说刚刚好
// Fifaer3: 托雷斯的巅峰卡: 1E ep 对于我来说太便宜了

JS版本

var Shop = /** @class */ (function () {
    function Shop() {
        //存储
        this.fifaers = {};
    }
    //注册
    Shop.prototype.register = function (name, fifaer) {
        if (this.fifaers[name]) {
            console.error(name + "\u540D\u79F0\u5DF2\u5B58\u5728");
        }
        else {
            this.fifaers[name] = fifaer;
            fifaer.setMedium(this);
        }
    };
    //转发
    Shop.prototype.relay = function (fifaer, message) {
        var _this = this;
        Object.keys(this.fifaers).forEach(function (name) {
            if (_this.fifaers[name] !== fifaer) {
                _this.fifaers[name].receive(message);
            }
        });
    };
    return Shop;
}());
//抽象玩家类
var Fifaer = /** @class */ (function () {
    function Fifaer() {
    }
    Fifaer.prototype.setMedium = function (mediator) {
        this.mediator = mediator;
    };
    Fifaer.prototype.receive = function (message) {
        console.log(this.constructor.name + "收到请求:", message);
    };
    Fifaer.prototype.send = function (message) {
        console.log(this.constructor.name + "上架新卡:", message);
        this.mediator.relay(this, message); //请中介者转发
    };
    return Fifaer;
}());
//具体中介者
var ConcreteShop = /** @class */ (function (_super) {
    __extends(ConcreteShop, _super);
    function ConcreteShop() {
        return _super.call(this) || this;
    }
    return ConcreteShop;
}(Shop));
//具体玩家1
var Fifaer1 = /** @class */ (function (_super) {
    __extends(Fifaer1, _super);
    function Fifaer1() {
        return _super.call(this) || this;
    }
    Fifaer1.prototype.receive = function (message) {
        console.log(message + " \u5BF9\u4E8E\u6211\u6765\u8BF4\u592A\u8D35\u4E86");
    };
    return Fifaer1;
}(Fifaer));
//具体玩家2
var Fifaer2 = /** @class */ (function (_super) {
    __extends(Fifaer2, _super);
    function Fifaer2() {
        return _super.call(this) || this;
    }
    Fifaer2.prototype.receive = function (message) {
        console.log(this.constructor.name + ": " + message + " \u5BF9\u4E8E\u6211\u6765\u8BF4\u521A\u521A\u597D");
    };
    return Fifaer2;
}(Fifaer));
//具体玩家3
var Fifaer3 = /** @class */ (function (_super) {
    __extends(Fifaer3, _super);
    function Fifaer3() {
        return _super.call(this) || this;
    }
    Fifaer3.prototype.receive = function (message) {
        console.log(this.constructor.name + ": " + message + " \u5BF9\u4E8E\u6211\u6765\u8BF4\u592A\u4FBF\u5B9C\u4E86");
    };
    return Fifaer3;
}(Fifaer));
var shop = new ConcreteShop();
var f1 = new Fifaer1();
var f2 = new Fifaer2();
var f3 = new Fifaer3();
shop.register('Ronaldo', f1);
shop.register('Messi', f2);
shop.register('Torres', f3);
f1.send('托雷斯的巅峰卡: 1E ep');
// Fifaer1上架新卡: 托雷斯的巅峰卡: 1E ep
// Fifaer2: 托雷斯的巅峰卡: 1E ep 对于我来说刚刚好
// Fifaer3: 托雷斯的巅峰卡: 1E ep 对于我来说太便宜了

Why

  • 优点
    • 减少类之间的依赖,将原本一对多的依赖变成一对一的依赖(即单个玩家买东西只需去交易市场而不需要去找持有该物品的玩家)
  • 缺点
    • 中介者可能会变得很大,逻辑复杂
  • 应用场景:多个对象解耦合,判断标准是类图中是否存在了网状结构

装饰器模式

What

定义:不改变现有对象结构的情况下,动态地给该对象增加一些职责(即增加其额外功能)的模式

解释:使用装饰器模式能在不改变源代码的基础上,对源代码的功能进行拓展

Game:鬼泣4中,但丁在暴揍各大领主获得许多道具,因此解锁几种战斗模式,如枪神模式

Snipaste_2020-06-07_18-47-50.png

How

  • 但丁刚登场时只能打招呼
  • 打败了各个领主(如炎域领主)后获得了相应的道具,从而获得不同的战斗模式

TS版本

@blademasterDecoration
@gunslingerDecoration
@tricksterDecoration 
@royalGuardDecoration
class Dante {
  sayHi() {
    console.log(`My name is: Dante`)
  }
}

// 剑圣模式
function blademasterDecoration(target: any){
  target.prototype.blademaster = function(){console.log('I am blademaster!')}
}

// 枪神模式
function gunslingerDecoration(target){
  target.prototype.gunslinger = function(){console.log('I am gunslinger!')}
}

// 骗术师模式
function tricksterDecoration(target){
  target.prototype.trickster = function(){console.log('I am trickster!')}
}

// 皇家守卫模式
function royalGuardDecoration(target){
  target.prototype.royalGuard = function(){console.log('I am royalGuard!')}
}

let dante: Dante = new Dante();
dante.blademaster();
dante.gunslinger();
dante.trickster();
dante.royalGuard();

// I am blademaster!
// I am gunslinger!
// I am trickster!
// I am royalGuard!

JS版本

var Dante = /** @class */ (function () {
    function Dante() {
    }
    Dante.prototype.sayHi = function () {
        console.log("My name is: Dante");
    };
    Dante = __decorate([
        blademasterDecoration,
        gunslingerDecoration,
        tricksterDecoration,
        royalGuardDecoration
    ], Dante);
    return Dante;
}());
// 剑圣模式
function blademasterDecoration(target) {
    target.prototype.blademaster = function () { console.log('I am blademaster!'); };
}
// 枪神模式
function gunslingerDecoration(target) {
    target.prototype.gunslinger = function () { console.log('I am gunslinger!'); };
}
// 骗术师模式
function tricksterDecoration(target) {
    target.prototype.trickster = function () { console.log('I am trickster!'); };
}
// 皇家守卫模式
function royalGuardDecoration(target) {
    target.prototype.royalGuard = function () { console.log('I am royalGuard!'); };
}
var dante = new Dante();
dante.blademaster();
dante.gunslinger();
dante.trickster();
dante.royalGuard();
// I am blademaster!
// I am gunslinger!
// I am trickster!
// I am royalGuard!

Why

  • 优点
    • 能够更加灵活的扩展类的功能
  • 缺点
    • 多层次的装饰嵌套,增大了代码的理解难度
  • 应用场景:想要添加新的功能同时不修改原生的代码

适配器模式

What

定义:将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类能一起工作

解释:简单来说就是打补丁,兼容一些旧的接口

Game:LOL中卡兹克登场时,空中可以释放w秒人,号称飞天螳螂。因为过于变态与英雄平衡机制不兼容,于是给他打了个补丁,W本身改动不大只是不允许在空中释放了。

Snipaste_2020-06-07_18-47-50.png

How

  • 螳螂原本的W可以空中释放,并且有治疗和减速的效果
  • 螳螂改版后不能在空中释放W

TS版本

interface TargetW{
    request(): void;
}
// 源接口
class OriginW{
    normalRequest(): void{
        console.log('我的w能够治疗、减速');
    }
    flyRequest(): void{
        console.log('我的w能在空中释放');
    }
}

class AdapterW extends OriginW implements TargetW{
    constructor(){
        super();
    }
    request(): void{
        console.log('取消了w在空中释放的机制');
        this.normalRequest();
    }
}

let target: TargetW = new AdapterW();
target.request();
// 取消了w在空中释放的机制
// 我的w能够治疗、减速

JS版本

var OriginW = /** @class */ (function () {
    function OriginW() {
    }
    OriginW.prototype.normalRequest = function () {
        console.log('我的w能够治疗、减速');
    };
    OriginW.prototype.flyRequest = function () {
        console.log('我的w能在空中释放');
    };
    return OriginW;
}());
var AdapterW = /** @class */ (function (_super) {
    __extends(AdapterW, _super);
    function AdapterW() {
        return _super.call(this) || this;
    }
    AdapterW.prototype.request = function () {
        console.log('取消了w在空中释放的机制');
        this.normalRequest();
    };
    return AdapterW;
}(OriginW));
var target = new AdapterW();
target.request();
// 取消了w在空中释放的机制
// 我的w能够治疗、减速

Why

  • 优点
    • 将目标类和源接口解耦,有不错的灵活性和可扩展性
  • 缺点
    • 作为补丁,出现过多增大系统的复杂度。
  • 应用场景:兼容旧的接口时

组合模式

What

定义:有时又叫作部分-整体模式,它是一种将对象组合成树状的层次结构的模式,用来表示“部分-整体”的关系,使用户对单个对象和组合对象具有一致的访问性。

解释:将对象之间的关系以🌲的形式进行表现

Game:最终幻想13中技能树的结构,每个加点方向为树干,每个技能点为树叶

Snipaste_2020-06-07_17-44-56.png

How

  • 创建一个或多个技能加点方向,比如治疗,伤害等
  • 每个加点方向添加许许多多的技能点

TS版本

interface SkillTree{
    add(st: SkillTree): void;
    operation(): void;
}

class SkillDirection implements SkillTree{
    private children: SkillTree[] = [];
    constructor(private name: string){}
    add(node: SkillTree): void{
        this.children.push(node);
    }

    operation(): void{
        console.log(`你选择${this.name}方向的技能树`)
        this.children.forEach((x: SkillTree) => x.operation())
    }
}

class SkillPoint implements SkillTree{
    constructor(private name: string){}

    add(node: SkillTree): void{};

    operation(): void{
        console.log(`你已学习技能点 ${this.name}`)
    }
}

let tree1: SkillTree = new SkillDirection('治疗');
let tree2: SkillTree = new SkillDirection('伤害');
let leaf1: SkillTree = new SkillPoint('全体加血');
let leaf2: SkillTree = new SkillPoint('单体攻击');
tree2.add(leaf2);
tree1.add(tree2);
tree1.add(leaf1);
tree1.operation();
// 你选择治疗方向的技能树
// 你选择伤害方向的技能树
// 你已学习技能点 单体攻击
// 你已学习技能点 全体加血

JS版本

var SkillDirection = /** @class */ (function () {
    function SkillDirection(name) {
        this.name = name;
        this.children = [];
    }
    SkillDirection.prototype.add = function (node) {
        this.children.push(node);
    };
    SkillDirection.prototype.operation = function () {
        console.log("\u4F60\u9009\u62E9" + this.name + "\u65B9\u5411\u7684\u6280\u80FD\u6811");
        this.children.forEach(function (x) { return x.operation(); });
    };
    return SkillDirection;
}());
var SkillPoint = /** @class */ (function () {
    function SkillPoint(name) {
        this.name = name;
    }
    SkillPoint.prototype.add = function (node) { };
    ;
    SkillPoint.prototype.operation = function () {
        console.log("\u4F60\u5DF2\u5B66\u4E60\u6280\u80FD\u70B9 " + this.name);
    };
    return SkillPoint;
}());
var tree1 = new SkillDirection('治疗');
var tree2 = new SkillDirection('伤害');
var leaf1 = new SkillPoint('全体加血');
var leaf2 = new SkillPoint('单体攻击');
tree2.add(leaf2);
tree1.add(tree2);
tree1.add(leaf1);
tree1.operation();
// 你选择治疗方向的技能树
// 你选择伤害方向的技能树
// 你已学习技能点 单体攻击
// 你已学习技能点 全体加血

Why

  • 优点
    • 局部调用和整体调用区别不大,换言之调用者不必担心调用对象是一个简单的还是一个复杂的结构
  • 缺点
    • 使得设计更加复杂。客户端需要花更多时间理清类之间的层次关系树干直接使用了实现类,限制了接口
  • 应用场景:所描述的对象符合树的结构,如文件管理系统

状态模式

What

定义:对有状态的对象,把复杂的“判断逻辑”提取到不同的状态对象中,允许状态对象在其内部状态发生改变时改变其行为

解释:不使用if-else | switch-case进行判断,而是通过传入状态对象进行状态切换

Game:只狼中苇名一心的三个状态,完美虐杀玩家

Snipaste_2020-06-06_19-14-46.png

How

  • 每打过一个阶段,苇名就切换状态

TS版本

interface State{
    change(context: WeiMingYiXin): void;
}

class StateOne implements State{
    change(context: WeiMingYiXin): void{
        console.log('苇名弦一郎阶段');
    }
}

class StateTwo implements State{
    change(context: WeiMingYiXin): void{
        console.log('剑圣苇名一心阶段');
    }
}

class StateThree implements State{
    change(context: WeiMingYiXin): void{
        console.log('雷电法王苇名一心阶段');
    }
} 

class WeiMingYiXin{
    constructor(private state: State){};
    setState(state: State): void{
        this.state = state;
    }

    request(): void{
        this.state.change(this);
    }
}

let ctx: WeiMingYiXin = new WeiMingYiXin(new StateOne());
ctx.request();
ctx.setState(new StateTwo());
ctx.request();
ctx.setState(new StateThree());
ctx.request();

// 苇名弦一郎阶段
// 剑圣苇名一心阶段
// 雷电法王苇名一心阶段

JS版本

var StateOne = /** @class */ (function () {
    function StateOne() {
    }
    StateOne.prototype.change = function (context) {
        console.log('苇名弦一郎阶段');
    };
    return StateOne;
}());
var StateTwo = /** @class */ (function () {
    function StateTwo() {
    }
    StateTwo.prototype.change = function (context) {
        console.log('剑圣苇名一心阶段');
    };
    return StateTwo;
}());
var StateThree = /** @class */ (function () {
    function StateThree() {
    }
    StateThree.prototype.change = function (context) {
        console.log('雷电法王苇名一心阶段');
    };
    return StateThree;
}());
var WeiMingYiXin = /** @class */ (function () {
    function WeiMingYiXin(state) {
        this.state = state;
    }
    ;
    WeiMingYiXin.prototype.setState = function (state) {
        this.state = state;
    };
    WeiMingYiXin.prototype.request = function () {
        this.state.change(this);
    };
    return WeiMingYiXin;
}());
var ctx = new WeiMingYiXin(new StateOne());
ctx.request();
ctx.setState(new StateTwo());
ctx.request();
ctx.setState(new StateThree());
ctx.request();
// 苇名弦一郎阶段
// 剑圣苇名一心阶段
// 雷电法王苇名一心阶段

Why

  • 优点
    • 可以很方便新增和切换状态
  • 缺点
    • 增加了很多的类,结构和实现较为复杂
  • 应用场景:对象的行为依赖于它的某些属性值,状态的改变将导致行为的变化

抽象工厂模式

What

定义:是一种为访问类提供一个创建一组相关或相互依赖对象的接口,且访问类无须指定所要产品的具体类就能得到同族的不同等级的产品的模式结构

解释:类比于现实中的工厂,抽象工厂可以生产多个品类的产品

Game:红色警戒中,盟军和苏军的空军工厂和陆军工厂分别能产出不同的军备和士兵

Snipaste_2020-06-06_19-14-46.png

How

  • 盟军陆军工厂产出幻影坦克,空军工厂产出战斗机
  • 苏军陆军工厂产出犀牛坦克,空军工厂产出飞艇

TS版本

interface AbstractAirForce{
    create(): void;
}
interface AbstractGroundForce{
    create(): void;
}
interface AbstractGroup{
    createAirForce(): AbstractAirForce;
    createGroundForce(): AbstractGroundForce;
}

class MAirForce implements AbstractAirForce{
    create(): void{
        console.log('战斗机已创立')
    }
}

class MGroundForce implements AbstractGroundForce{
    create(): void{
        console.log('幻影坦克已创立')
    }
}

class SAirForce implements AbstractAirForce{
    create(): void{
        console.log('飞艇已创立')
    }
}

class SGroundForce implements AbstractGroundForce{
    create(): void{
        console.log('犀牛坦克已创立')
    }
}

class MGroup implements AbstractGroup{
    createAirForce(): AbstractAirForce{
        return new MAirForce();
    }
    createGroundForce(): AbstractGroundForce{
        return new MGroundForce();
    }
}

class SGroup implements AbstractGroup{
    createAirForce(): AbstractAirForce{
        return new SAirForce();
    }
    createGroundForce(): AbstractGroundForce{
        return new SGroundForce();
    }
}

let mGroup: AbstractGroup = new MGroup();
let sGroup: AbstractGroup = new SGroup();
mGroup.createAirForce().create();
mGroup.createGroundForce().create();
sGroup.createGroundForce().create()
sGroup.createGroundForce().create();
// 战斗机已创立
// 幻影坦克已创立
// 犀牛坦克已创立
// 犀牛坦克已创立

JS版本

var MAirForce = /** @class */ (function () {
    function MAirForce() {
    }
    MAirForce.prototype.create = function () {
        console.log('战斗机已创立');
    };
    return MAirForce;
}());
var MGroundForce = /** @class */ (function () {
    function MGroundForce() {
    }
    MGroundForce.prototype.create = function () {
        console.log('幻影坦克已创立');
    };
    return MGroundForce;
}());
var SAirForce = /** @class */ (function () {
    function SAirForce() {
    }
    SAirForce.prototype.create = function () {
        console.log('飞艇已创立');
    };
    return SAirForce;
}());
var SGroundForce = /** @class */ (function () {
    function SGroundForce() {
    }
    SGroundForce.prototype.create = function () {
        console.log('犀牛坦克已创立');
    };
    return SGroundForce;
}());
var MGroup = /** @class */ (function () {
    function MGroup() {
    }
    MGroup.prototype.createAirForce = function () {
        return new MAirForce();
    };
    MGroup.prototype.createGroundForce = function () {
        return new MGroundForce();
    };
    return MGroup;
}());
var SGroup = /** @class */ (function () {
    function SGroup() {
    }
    SGroup.prototype.createAirForce = function () {
        return new SAirForce();
    };
    SGroup.prototype.createGroundForce = function () {
        return new SGroundForce();
    };
    return SGroup;
}());
var mGroup = new MGroup();
var sGroup = new SGroup();
mGroup.createAirForce().create();
mGroup.createGroundForce().create();
sGroup.createGroundForce().create();
sGroup.createGroundForce().create();
// 战斗机已创立
// 幻影坦克已创立
// 犀牛坦克已创立
// 犀牛坦克已创立

Why

  • 优点
    • 增加新的具体工厂和产品族很方便,无须修改已有系统,符合“开闭原则”
  • 缺点
    • 增加新的产品等级结构很复杂,需要修改抽象工厂和所有的具体工厂类
  • 应用场景:当需要创建的对象是一系列相互关联或相互依赖的产品族时,便可以使用抽象工厂模式

设计模式中应该遵循的准则

单一职责原则(SRP)

What

一个对象(方法)只做一件事情。

How

  • 并非所有的耦合对象都应该分离,如果两个职责总是同时变化,那就不必分离他们
  • 如果两个发生变化的对象并没有发生改变的预兆时,也没有必要去分离他们
  • 从上述两点可知,耦合对象应该满足有变化趋势且两者的变化并不是同步时,才应该考虑进行职责分离

Why

  • 降低了单个类或对象的复杂度
  • 利于代码的复用和进行单元测试
  • 当一个职责需求变更时,不会影响到其他的职责
  • 但是因此会增加代码编写的复杂度,同时也增大了对象之间相互联系的难度。如何平衡代码的复用性和开发的难度是该原则的难点之一

最小知识原则(LKP)

What

设计程序时,应当减少对象之间的交互,避免出现a.foo1(b).foo2(c).foo3(d)的情况出现

如果两个对象之间不必直接通信,那么这两个对象就不要发生直接联系,而是引入一个第三方对象,来承担这些对象之间的通信作用

How

  • 例如使用中介者模式时,两个对象并不直接联系,而是通过中介者的接口进行联系。就像电商网站,消费者和商家通过平台进行交易而不是直接交易

Why

  • 代码逻辑更加清晰,避免了冗杂的链式调用
  • 对象之间相互独立互不干扰,通过第三方进行联系。这样利于测试以及可扩展性好

开放-封闭原则(OCP)

What

添加新功能的时候,可以使用增加代码的方式,但是不允许改动程序的源代码

How

  • 避免使用大量if-else | switch-case等大量条件分支语句
  • 保留不变的地方,然后将变化的地方封装起来

Why

  • 修改源代码是一个难度基数很高的操作,经常会发生修改一个bug而引入更多的bug,而新增代码是一种更加明智的选择
  • 每次修改代码后都会进行整体代码回归测试,而新增模块只需要对变化的部分进行测试
  • 提高代码的可复用性

参考文献

《JS设计模式与开发实践》

DesignPatterns_TypeScript