function Extends(clazz) {
return class extends clazz {
// ...
}
}
اسمحوا لي أن أشرح كيف يعمل. بدلاً من الميراث العادي ، نستخدم الآلية المذكورة أعلاه. ثم نحدد الفئة الأساسية فقط عند إنشاء كائن:
const Class = Extends(Base)
const object = new Class(...args)
سأحاول إقناعك بأن هذا هو ابن صديقة والدتي من أجل الميراث الطبقي وطريقة لإعادة الميراث إلى عنوان أداة true-OOP (مباشرة بعد الميراث النموذجي ، بالطبع).
تقريبا ليس خارج الموضوع
, , , pet project , pet project'. , .
دعنا نتفق على الأسماء: سأطلق على هذه التقنية اسم mixin ، على الرغم من أن هذا لا يزال يعني اختلافًا بسيطًا . قبل إخباري أن هذه مزيج من TS / JS ، استخدمت اسم LBC (فئات ملزمة مؤخرًا).
"مشاكل" الميراث الطبقي
نعلم جميعًا كيف أن "الجميع" "لا يحبون" الميراث الطبقي. ما هي مشاكله؟ دعنا نفهمها ونفهم في نفس الوقت كيف تحلها الخلطات.
تنفيذ التوريث فواصل التغليف
تتمثل المهمة الرئيسية لـ OOP في ربط البيانات والعمليات عليها (التغليف). عندما ترث فئة من فئة أخرى ، تنقطع هذه العلاقة: البيانات موجودة في مكان واحد (الأصل) ، والعمليات - في مكان آخر (وراثي). علاوة على ذلك ، يمكن للوارث أن يثقل كاهل الواجهة العامة للفئة ، بحيث لا يمكن لرمز الفئة الأساسية ولا رمز الفئة الموروثة بشكل منفصل معرفة ما سيحدث لحالة الكائن. أي أن الطبقات متقاربة.
تقلل Mixins ، بدورها ، بشكل كبير من الاقتران: على سلوك أي فئة أساسية يجب أن يعتمد عليها المورث ، إذا لم يكن هناك فئة أساسية في وقت إعلان الفئة الموروثة؟ ومع ذلك ، وبفضل هذا التقيد المتأخر والطريقة الزائدة ، فإن "مشكلة اليويو"بقايا. إذا كنت تستخدم الميراث في التصميم الخاص بك، من أنه لا يمكن الهرب، ولكن، على سبيل المثال، في كلمات Kotlin
openو overrideيجب أن تخفف كثيرا من الوضع (أنا لا أعرف، لا تعرف بشكل وثيق جدا مع Kotlin).
وراثة طرق غير ضرورية
مثال كلاسيكي مع قائمة ومكدس: إذا ورثت المكدس من قائمة ، فستدخل الطرق من واجهة القائمة إلى واجهة المكدس ، والتي يمكن أن تنتهك ثابت المكدس. لن أقول إن هذه مشكلة وراثية ، لأنه ، على سبيل المثال ، في C ++ هناك وراثة خاصة لهذا (ويمكن جعل الطرق الفردية متاحة للعامة
using) ، لذا فهذه مشكلة تتعلق باللغات الفردية.
عدم المرونة
- , : . , , : , , cohesion . , .
- ( ), . , : , .
- . - , . , , ? - — , .. .
- . - , - . , , .
إذا كان الفصل يرث من تطبيق فئة أخرى ، فقد يؤدي تغيير هذا التطبيق إلى كسر الفئة الموروثة. في هذه المقالة هناك توضيح جيد جدا للمشاكل
Stackو MonitorableStack.
مع mixins ، يجب على المبرمج أن يأخذ في الاعتبار أن الفئة الموروثة التي يكتبها يجب أن تعمل ليس فقط مع فئة أساسية معينة ، ولكن أيضًا مع الفئات الأخرى التي تتوافق مع واجهة الفئة الأساسية.
الموز والغوريلا والغابة
يعد OOP بالتركيب ، أي القدرة على إعادة استخدام الفصول الفردية في مواقف مختلفة وحتى في مشاريع مختلفة. ومع ذلك ، إذا ورثت فئة من فئة أخرى ، من أجل إعادة استخدام المورث ، فأنت بحاجة إلى نسخ جميع التبعيات ، والفئة الأساسية وجميع تبعياتها ، والفئة الأساسية الخاصة بها…. أولئك. أراد موزة ، وأخرج غوريلا ، ثم الغابة. إذا تم إنشاء الكائن مع مراعاة مبدأ انعكاس التبعية ، فإن التبعيات ليست سيئة للغاية - فقط انسخ واجهاتها. ومع ذلك ، لا يمكن القيام بذلك مع سلسلة الميراث.
تجعل Mixins ، بدورها ، من الممكن (والإلزامي) استخدام DIPs فيما يتعلق بالميراث.
المرافق الأخرى من Mixins
لا تنتهي مزايا mixins عند هذا الحد. دعونا نرى ما يمكنك فعله معهم.
موت الميراث التسلسل الهرمي
لم تعد الفئات تعتمد على بعضها البعض: إنها تعتمد فقط على الواجهات. أولئك. يصبح التنفيذ أوراق الرسم البياني للتبعية. هذا من شأنه أن يجعل إعادة البناء أسهل - أصبح نموذج المجال الآن مستقلاً عن تنفيذه.
موت الطبقات المجردة
لم تعد هناك حاجة لفصول الملخص. لنلقِ نظرة على مثال لنمط Factory Method في Java مستعار من خبير إعادة البناء :
interface Button {
void render();
void onClick();
}
abstract class Dialog {
void renderWindow() {
Button okButton = createButton();
okButton.render();
}
abstract Button createButton();
}
نعم ، بالطبع ، تتطور أساليب المصنع إلى أنماط Builder و Strategy. ولكن يمكنك القيام بذلك باستخدام mixins (دعنا نتخيل لثانية أن Java بها مزيج من الدرجة الأولى):
interface Button {
void render();
void onClick();
}
interface ButtonFactory {
Button createButton();
}
class Dialog extends ButtonFactory {
void renderWindow() {
Button okButton = createButton();
okButton.render();
}
}
يمكنك القيام بهذه الحيلة مع أي فئة مجردة تقريبًا. مثال حيث لا يعمل:
abstract class Abstract {
void method() {
abstractMethod();
}
abstract void abstractMethod();
}
class Concrete extends Abstract {
private encapsulated = new Encapsulated();
@Override
void method() {
encapsulated.method();
super.method();
}
void abstractMethod() {
encapsulated.otherMethod();
}
}
هنا
encapsulatedهناك حاجة إلى المجال في كل من التحميل الزائد methodوالتنفيذ abstractMethod. أي ، بدون كسر التغليف ، Concreteلا يمكن تقسيم الفصل إلى طفل Abstractوإلى "طبقة عليا" Abstract. لكنني لست متأكدًا مما إذا كان هذا مثالًا على التصميم الجيد.
مرونة مقارنة بالأنواع
سيلاحظ القارئ اليقظ أن هذه كلها تشبه إلى حد كبير سمات Smalltalk / Rust. هناك نوعان من الاختلافات:
- يمكن أن تحتوي مثيلات Mixin على بيانات لم تكن موجودة في الفئة الأساسية ؛
- لا تقوم Mixins بتعديل الفئة التي ترث منها: لاستخدام وظيفة mixin ، تحتاج إلى إنشاء كائن mixin بشكل صريح ، وليس كائن فئة أساسية.
يقود الاختلاف الثاني إلى حقيقة أن mixins يعمل محليًا ، على عكس السمات التي تعمل في جميع حالات الفئة الأساسية. مدى ملاءمة ذلك يعتمد على المبرمج وعلى المشروع ، لن أقول إن الحل الخاص بي أفضل بالتأكيد.
هذه الاختلافات تقرب الخلطات من الميراث الطبيعي ، لذلك يبدو لي أن هذا الشيء هو مقايضة مضحكة بين الميراث والسمات.
سلبيات mixins
أوه ، لو كان الأمر بهذه البساطة. هناك بالتأكيد مشكلة صغيرة ومشكلة واحدة ناقص الدهون.
تنفجر واجهات
إذا كنت تستطيع أن ترث فقط من الواجهة ، فمن الواضح أنه سيكون هناك المزيد من الواجهات في المشروع. بالطبع ، إذا تم احترام DIP في المشروع ، فلن تعمل بعض الواجهات الأخرى على التعامل مع الطقس ، ولكن لن تتبع جميعها SOLID. يمكن حل هذه المشكلة إذا تم ، بناءً على كل فئة ، إنشاء واجهة تحتوي على جميع الطرق العامة ، وعند ذكر اسم الفئة ، حدد ما إذا كان المقصود بالفئة مصنع للكائنات أو كواجهة. يتم إجراء شيء مماثل في TypeScript ، ولكن لسبب ما تم ذكر الحقول والأساليب الخاصة في الواجهة التي تم إنشاؤها.
بناة مجمع
باستخدام mixins ، فإن أصعب مهمة هي إنشاء كائن. ضع في اعتبارك خيارين اعتمادًا على ما إذا كان المُنشئ مضمّنًا في واجهة الفئة الأساسية:
- , , . , - . , .
- , . :
interface Base { new(values: Array<int>) } class Subclass extends Base { // ... } class DoesntFit { new(values: Array<int>, mode: Mode) { // ... } }
DoesntFitSubclass, - .SubclassDoesntFit,Base. - في الواقع ، هناك خيار آخر - لتمرير قائمة الوسيطات إلى المُنشئ وليس القاموس. هذا يحل المشكلة أعلاه لأنه من
{ values: Array<int>, mode: Mode }الواضح أنه يطابق النمط{ values: Array<int> }، لكنه يؤدي إلى تصادم غير متوقع للأسماء في مثل هذا القاموس: على سبيل المثال ، يستخدم كل من الطبقة الفائقةAوالوراثةBنفس المعلمات ، ولكن هذا الاسم غير محدد في واجهة الفئة الأساسية لـB.
بدلا من الاستنتاج
أنا متأكد من أنني فاتني بعض جوانب هذه الفكرة. أو حقيقة أن هذا هو بالفعل زر أكورديون بري وقبل عشرين عامًا كانت هناك لغة تستخدم هذه الفكرة. على أي حال ، أنا في انتظارك في التعليقات!
قائمة المصادر
neethack.com/2017/04/Why-inheritance-is-bad
www.infoworld.com/article/2073649/why-extends-is-evil.html
www.yegor256.com/2016/09/13/inheritance-is- procedural.html
refactoring.guru/ru/design-patterns/factory-method/java/example
scg.unibe.ch/archive/papers/Scha03aTraits.pdf