以遊戲玩家的視角開啟設計模式

Lazy.Cat發表於2021-04-03

前言

最近學習設計模式和TypeScript,發現網上的資料略顯硬核,不太容易理解記憶,經常看完就忘。作為一名遊戲玩家,發現遊戲中的很多場景都能和相應的設計模式相關聯,不僅便於理解,更利於合理地運用設計模式。由於個人水平有限,只整理個人覺得比較有趣的設計模式,每個模式採用哲學三問進行講解。如果對你有幫助的話,歡迎點贊和收藏?,圖片源自網路,侵刪。

目錄

img

設計模式

單例模式

What

  • 定義:一個類僅有一個例項,並提供一個訪問它的全域性訪問點
  • 解釋:實質上就是起到一個儲存全域性變數的作用,其他的類和模組只能通過該類提供的介面修改其唯一例項

Game:在DNF一起組團刷本的日子中,副本BOSS看作為一個單例,玩家可以通過各種技能或者平A去消耗BOSS

並且該本中的所有玩家都是對同一個BOSS造成傷害

img

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牌的運營方式取勝。

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懶人玩家,只想把時間花在看模擬比賽上,其他不想做的事就很開心地甩給助理教練啦

img

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:進遊戲後你會訂閱隊友和敵人的生存狀態,當生存狀態發生變化時,系統會及時通知你

img

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中,你可以在交易系統上上架球員卡,然後該商品會被交易系統轉發給其他玩家

img

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中,但丁在暴揍各大領主獲得許多道具,因此解鎖幾種戰鬥模式,如槍神模式

img

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本身改動不大隻是不允許在空中釋放了。

img

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中技能樹的結構,每個加點方向為樹幹,每個技能點為樹葉

img

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:只狼中葦名一心的三個狀態,完美虐殺玩家

img

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:紅色警戒中,盟軍和蘇軍的空軍工廠和陸軍工廠分別能產出不同的軍備和士兵

img

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

相關文章