أنماط التصميم الإنشائي في ES6 + على سبيل المثال لعبة Game of Thrones



يوم جيد يا اصدقاء!

تستخدم أنماط التصميم الإنشائي لبناء أنظمة كبيرة من العلاقات بين الأشياء من أجل الحفاظ على المرونة والكفاءة. دعونا نلقي نظرة على بعضها مع إشارات إلى لعبة العروش.

في تطوير البرمجيات ، تُستخدم أنماط التصميم لحل المشاكل الأكثر شيوعًا. تمثل أفضل الممارسات التي تم تطويرها على مدى فترة طويلة من قبل المطورين في عملية اختبار التطبيقات وإصلاح الأخطاء.

في هذه المقالة سنتحدث عن الأنماط الهيكلية. وهي مصممة لتصميم التطبيقات من خلال تحديد طريقة بسيطة للتفاعل مع الأمثلة.

الأكثر شيوعًا هي الأنماط التالية:

  • مشترك كهربائي
  • ديكور
  • مظهر زائف
  • التكيف (خفيف الوزن (العنصر) ، وزن الذبابة)
  • الوكيل

مشترك كهربائي


المحول عبارة عن قالب يسمح لك بترجمة (نقل) واجهة فئة إلى أخرى. يسمح للفصول بالعمل معًا ، وهو أمر مستحيل في الغالب بسبب الهياكل غير المتوافقة.

تخيل أن Targaryen قرر القتال مع جميع القوات الموجودة تحت تصرفهم (Flawless and Dragons) ، و Daenerys تبحث عن طريقة للتفاعل مع كل منهم.

سنحتاج إلى ما يلي:

  • فئة غير ملطخة
  • فئة التنين
  • فئة المحول لتمرير طريقة حرق فئة التنين إلى طريقة القتل الشائعة

class Unsullied {
    constructor(name) {
        this.name = name
        this.kill = this.kill.bind(this)
    }

    kill() {
        console.log(`Unsullied ${this.name} kill`)
    }
}

class Dragon {
    constructor(name) {
        this.name = name
        this.burn = this.burn.bind(this)
    }

    burn() {
        console.log(`Dragon ${this.name} burn`)
    }
}

class DragonAdapter extends Dragon {
    kill() {
        this.burn()
    }
}

(() => {
    const Army = [
        new DragonAdapter('Rhaegal'),
        new Unsullied('Grey worm'),
        new DragonAdapter('Drogon')
    ]
    Army.forEach(soldier => soldier.kill())
})()

حالات الاستخدام


  • عندما يجب أن تعمل المكونات أو الوحدات الجديدة مع الموجودة في التطبيق أو عندما يتم تحسين جزء من التعليمات البرمجية نتيجة لإعادة الهيكلة ، ولكن يجب أن تتفاعل مع الأجزاء القديمة.
  • عندما وجدت مكتبة لحل المشكلات الثانوية وترغب في دمجها في تطبيقك.

ديكور


الديكور هو قالب مصمم لإضافة سلوك أو وظيفة إلى الفئات الموجودة ديناميكيًا. هذا بديل عن التصنيف الفرعي.

لنفترض أننا نريد التحقق مما إذا كان المشروب المقدم للملك جوفري مسموما.

سنحتاج إلى ما يلي:

  • فئة الملك
  • مثال الملك جوفري
  • فئة الشراب
  • مشروب تسمم الطبقة
  • وظيفة isNotPoisoned لإنقاذ حياة الملك

function isNotPoisoned(t, n, descriptor) {
    const original = descriptor.value
    if(typeof original === 'function') {
        descriptor.value = function(...args) {
            const [drink] = args
            if(drink.constructor.name === 'poisonedDrink') throw new Error('Someone wants to kill the king')
            return original.apply(this, args)
        }
    }
    return descriptor
}

class PoisonedDrink {
    constructor(name) {
        this.name = name
    }
}

class Drink {
    constructor(name) {
        this.name = name
    }
}

class King {
    constructor(name) {
        this.name = name
    }

    //     
    //      
    @isNotPoisoned
    drink(drink) {
        console.log(`The king drank ${drink}`)
    }
}

(() => {
    const joffrey = new King('Joffrey Baratheon')
    const normalDrink = new Drink('water')
    const poisonedDrink = new Drink('poisoned water')
    joffrey.drink(normalDrink)
    joffrey.drink(poisonedDrink)
})()

حالات استخدام الديكور


  • عندما نريد إضافة دالة إلى عدد كبير من الفئات أو الأساليب ذات السياقات المختلفة
  • عندما نريد تحسين فصل تم إنشاؤه سابقًا ، ولكن ليس لدينا الوقت الكافي لإعادة البيع بالكامل

مظهر زائف


واجهة - قالب يستخدم على نطاق واسع في المكتبات. يتم استخدامه لتوفير واجهة موحدة وبسيطة وإخفاء تعقيد أنظمتها الفرعية أو الفئات الفرعية.

تخيل أننا نريد السيطرة على جيش في معركة مع البرابرة. لحالات جيشنا طرق لتحريك الفرسان والجنود والعمالقة. لكننا مضطرون إلى استدعاء هذه الأساليب بشكل منفصل ، الأمر الذي يستغرق الكثير من الوقت. كيف يمكننا أن نجعل إدارة الجيش أسهل؟

سنحتاج إلى ما يلي:

  • عينات الجيش
  • فئة ArmyFacade

class Horse {
    constructor(name) {
        this.name = name
    }
    attack() {
        console.log(`Infantry ${this.name} attack`)
    }
}

class Soldier {
    constructor(name) {
        this.name = name
    }
    attack() {
        console.log(`Soldier ${this.name} attack`)
    }
}

class Giant {
    constructor(name) {
        this.name = name
    }
    attack() {
        console.log(`Giant ${this.name} attack`)
    }
}

class ArmyFacade {
    constructor() {
        //  ,           
        this.army = [];
        (new Array(10)).fill().forEach((_, i) => this.army.push(new Horse(i + 1)));
        (new Array(10)).fill().forEach((_, i) => this.army.push(new Soldier(i + 1)));
        (new Array(1)).fill().forEach((_, i) => this.army.push(new Giant(i + 1)));
        this.getByType = this.getByType.bind(this)
    }
    getByType(type, occurency) {
        return this.army.filter(el => {
            return el.constructor.name === type && occurency-- > 0
        })
    }
    attack(armyInfo = {}) {
        const keys = Object.keys(armyInfo)
        let subArmy = []
        keys.forEach(soldier => {
            switch(soldier) {
                case 'horse': {
                    subArmy = [...subArmy, ...this.getByType('Horse', armyInfo.horse)]
                    break;
                }
                    case 'soldier': {
                    subArmy = [...subArmy, ...this.getByType('Soldier', armyInfo.soldier)]
                    break;
                }
                    case 'giant': {
                    subArmy = [...subArmy, ...this.getByType('Giant', armyInfo.giant)]
                    break;
                }
            }
        })
        subArmy.forEach(soldier => soldier.attack())
    }
}

(() => {
    const army = new ArmyFacade()
    army.attack({
        horse: 3,
        soldier: 5,
        giant: 1
    })
})()

استخدم حالات


  • عندما نريد تحويل العديد من أسطر التعليمات البرمجية ، وربما تكرارها ، إلى وظيفة واحدة بسيطة.

الانتهازي


تكيفي - قالب مصمم لنقل البيانات بكفاءة بين العديد من الأشياء الصغيرة. يتم استخدامه لتحسين الأداء وحفظ الذاكرة.

لنفترض أننا نريد السيطرة على جيش من المشاة البيض. في الوقت نفسه ، يمكن أن يكون لدى المشاة ثلاث ولايات:

  • على قيد الحياة
  • ميت
  • إحياء

سنحتاج إلى ما يلي:

  • فئة WhiteWalker
  • فئة WhiteWalkerFlyweight
  • العميل لقيامة المشاة البيض

class WhiteWalker {
    constructor({
        sprite,
        someOtherBigInformation
    }) {
        this.sprite = sprite
        this.someOtherBigInformation = someOtherBigInformation
        this.state = 'alive'
        this.resurrect = this.resurrect.bind(this)
        this.kill = this.kill.bind(this)
        this.getState = this.getState.bind(this)
    }
    kill() {
        this.state = 'dead'
    }
    resurrect() {
        this.state = 'resurrected'
    }
    getState() {
        return this.state
    }
}

const whiteWalker = new WhiteWalker({
    sprite: Date.now()
})

class WhiteWalkerFlyweight {
    constructor(position, name) {
        this.position = position
        this.name = name
        this.whiteWalker = whiteWalker
    }
    getInfo() {
        console.log(`The White Walker ${this.name} whit sprite ${this.whiteWalker.sprite} is ${this.whiteWalker.state}`)
    }
    getFatherInstance() {
        return this.whiteWalker
    }
}

(() => {
    const myArmy = []
    for(let i = 0; i < 10; i++) {
        myArmy.push(new WhiteWalkerFlyweight({
            x: Math.floor(Math.random() * 200),
            y: Math.floor(Math.random() * 200),
        }, i + 1))
    }
    myArmy.forEach(soldier => soldier.getInfo())
    console.log('KILL ALL')
    const [onOffWhiteWalker] = myArmy
    onOffWhiteWalker.getFatherInstance().kill()
    myArmy.forEach(soldier => soldier.getInfo())
    console.log('RESURRECT ALL')
    onOffWhiteWalker.getFatherInstance().resurrect()
    myArmy.forEach(soldier => soldier.getInfo())
})()

استخدم حالات


  • عندما نريد تجنب إنشاء عدد كبير من الكائنات
  • عندما نريد إنشاء كائنات تستهلك مساحة كبيرة من الذاكرة
  • عندما نحتاج إلى كائنات يتطلب تكوينها حسابات معقدة
  • عندما يكون لدينا موارد محدودة: قوة الحوسبة ، والذاكرة ، والمساحة ، إلخ.

الوكلاء


يتم استخدام قالب الوكيل ، كما يوحي اسمه ، كوظيفة إضافية أو كبديل لكائن آخر للتحكم في الوصول إلى هذا الكائن.

تخيل أن الملكة سيرسي أصدرت مرسوما يحظر تجنيد أكثر من 100 جندي دون إذنها. كيف نطبق هذا؟

سنحتاج إلى ما يلي:

  • جندي الطبقة
  • فئة ArmyProxy للتحكم في العمليات
  • أمثلة من فئة Cercei للحصول على إذن

class Soldier {
    constructor(name) {
        this.name = name
    }
    attack() {
        console.log(`Soldier ${this.name} attack`)
    }
}

class Queen {
    constructor(name) {
        this.name = name
    }
    getConsent(casualNumber) {
        return casualNumber %2 === 0
    }
}

class ArmyProxy {
    constructor() {
        this.army = []
        this.available = 0
        this.queen = new Queen('Cercei')
        this.getQueenConsent = this.getQueenConsent.bind(this)
    }

    getQueenConsent() {
        return this.queen.getConsent(Math.floor(Math.random() * 200))
    }

    enrollSoldier() {
        if(!this.available) {
            const consent = this.getQueenConsent()
            if(!consent) {
                console.error(`The queen ${this.queen.name} deny the consent`)
                return
            }
            this.available = 100
        }
        this.army.push(new Soldier(this.army.length))
        this.available--
    }
}

(() => {
    const myArmy = new ArmyProxy()
    for(let i = 0; i < 1000; i++) {
        myArmy.enrollSoldier()
    }
    console.log(`I have ${myArmy.army.length} soldiers`)
})()

استخدم حالات


  • عندما يكون الكائن الذي نريد استخدامه بعيدًا (متداخلًا بعمق) وحفظ المنطق في الوكيل حتى لا يؤثر على العميل
  • عندما نريد إعطاء نتيجة تقريبية توقعًا لنتيجة حقيقية ، يستغرق حسابها الكثير من الوقت
  • عندما نريد التحكم في الوصول أو إنشاء كائن دون التدخل في منطقه

كود جيثب .

ملحوظة العابرة: هنا فيديو رائع عن أنماط التصميم.

شكرآ لك على أهتمامك.

All Articles