المخاليط في TypeScript

من موسوعة حسوب

مقدمة

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

عينة لمخلاط

يُمكنك التعرف على كيفية إنشاء المخاليط في TypeScript من الشيفرة أدناه. سنشرح مكونات الشيفرة تاليًا:

// Disposable Mixin
class Disposable {
    isDisposed: boolean;
    dispose() {
        this.isDisposed = true;
    }

}

// Activatable Mixin
class Activatable {
    isActive: boolean;
    activate() {
        this.isActive = true;
    }
    deactivate() {
        this.isActive = false;
    }
}

class SmartObject implements Disposable, Activatable {
    constructor() {
        setInterval(() => console.log(this.isActive + " : " + this.isDisposed), 500);
    }

    interact() {
        this.activate();
    }

    // Disposable
    isDisposed: boolean = false;
    dispose: () => void;
    // Activatable
    isActive: boolean = false;
    activate: () => void;
    deactivate: () => void;
}
applyMixins(SmartObject, [Disposable, Activatable]);

let smartObj = new SmartObject();
setTimeout(() => smartObj.interact(), 1000);

////////////////////////////////////////
// ضع هذه الدالة في مكتبة زمن التنفيذ الخاصة بك
////////////////////////////////////////

function applyMixins(derivedCtor: any, baseCtors: any[]) {
    baseCtors.forEach(baseCtor => {
        Object.getOwnPropertyNames(baseCtor.prototype).forEach(name => {
            derivedCtor.prototype[name] = baseCtor.prototype[name];
        });
    });
}

فهم العيّنة

تبدأ الشيفرة بالصنفين اللذان سيُمثّلان المخلاطين. يمكنك ملاحظة أن كلا منهما يُركّز على قدرة أو نشاط معيّن (Disposable يشير إلى أن الكائن جاهز للاستعمال وقابل للتخلص منه، أمّا Activatable فيسمح بتفعيل الكائن أو تعطيله). سنخلط بعد قليل هاذين الصنفين معًا لتشكيل صنف جديد من كلا الصفتين (أي أن الصنف النهائي سيكون جاهزًا للاستعمال وقابلًا للتفعيل والتعطيل في نفس الوقت):

// Disposable Mixin
class Disposable {
    isDisposed: boolean;
    dispose() {
        this.isDisposed = true;
    }

}

// Activatable Mixin
class Activatable {
    isActive: boolean;
    activate() {
        this.isActive = true;
    }
    deactivate() {
        this.isActive = false;
    }
}

سنُنشئ بعد ذلك الصنف الذي سيحمل نتيجة دمج المخلاطين. لنلق نظرة تفصيلية على آلية العمل هذه:

class SmartObject implements Disposable, Activatable {

أوّل ما قد تلحظه في السطر أعلاه أنّنا نستخدم الكلمة المفتاحية ‎implements‎ عوضًا عن ‎extends‎. يُعامِل هذا الأصنافَ على أنّها واجهات، وستُستَعمل الأنواع فقط من ‎Disposable‎ و‎Activatable‎ عوضًا عن شيفرة التطبيق (implementation) الموجودة داخل الصنفين. ما يعني أن علينا توفير شيفرة التطبيق في الصنف. لكن، هذا ما نريد تفاديه باستعمال المخاليط، فما العمل؟

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

// Disposable
isDisposed: boolean = false;
dispose: () => void;
// Activatable
isActive: boolean = false;
activate: () => void;
deactivate: () => void;

وفي الأخير نخلط مخاليطنا مع الصنف، وبالتالي سيكون التطبيق كاملًا:

applyMixins(SmartObject, [Disposable, Activatable]);

ننشئ بعد ذلك دالة مساعدةً لإجراء عمليّة الخلط. ستمرّ هذه الدالة على خاصيات كل مخلاط وستنسخها إلى هدف المخاليط مع إعطاء القيم للخاصيات التي عُرّفت دون قيم (أي أنها سنسخ التطبيقات).

function applyMixins(derivedCtor: any, baseCtors: any[]) {
    baseCtors.forEach(baseCtor => {
        Object.getOwnPropertyNames(baseCtor.prototype).forEach(name => {
            derivedCtor.prototype[name] = baseCtor.prototype[name];
        });
    });
}

مصادر