لماذا الحد من الميراث مع النهائي؟

ربما سمعت هذا البيان الشهير من GoF: "أفضل تكوين لميراث الصف". وبعد ذلك ، وكقاعدة عامة ، استمرت الأفكار الطويلة حول كيف أن الميراث المحدد بشكل ثابت ليس مرنًا مقارنةً بالتركيبة الديناميكية.


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


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


مشكلة الطبقة الأساسية الهشة


مشكلة الطبقة الأساسية الهشة


واحدة من المعايير الرئيسية للبنية الجيدة هي الاقتران الفضفاض ، الذي يميز درجة الترابط بين وحدات البرامج. ليس لشيء أن المشاركة الضعيفة هي جزء من قائمة أنماط GRASP التي تصف المبادئ الأساسية لتقاسم المسؤولية بين الطبقات.


المشاركة الضعيفة لها العديد من المزايا.


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

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


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


ارتباط قوي فيما يتعلق بالميراث


ارتباط قوي فيما يتعلق بالميراث


حجم الكود المشترك بين فئتي الوالدين والطفل كبير جدًا. تبدأ هذه المشكلة في الظهور بقوة خاصة عند إساءة استخدام مفهوم الميراث - استخدام الميراث حصريًا لإعادة استخدام الشفرة الأفقية ، وليس لإنشاء فئات فرعية متخصصة. لأن الميراث هو أسهل طريقة لإعادة استخدام الكود. ما عليك سوى الكتابة extends ParentClass وهذا كل شيء! بعد كل شيء ، هو أبسط بكثير من التجميع ، حقن التبعية (DI) ، تخصيص الواجهة.


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


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


لنأخذ "الأرنب التجريبي" أبسط فئة من مجموعة التعليقات مع مجموعة من التعليقات في الداخل. تم العثور على فئات مماثلة مع مجموعة في الداخل بأعداد كبيرة في أي مشروع.


 class CommentBlock { /** @var Comment[]   */ private $comments = []; } 

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


قضايا الميراث


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


الميراث ينتهك مبدأ الإخفاء


نظرًا لأن الفئة الفرعية لديها إمكانية الوصول إلى تفاصيل تنفيذ الفصل الأصل ، يُقال إن الميراث ينتهك التغليف.

أنماط الحياة

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


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


  • الواجهة العامة المستخدمة من قبل جميع العملاء من هذه الفئة ؛
  • واجهة محمية تستخدمها جميع الطبقات التابعة.

أي الطبقة الفرعية لديها ترسانة من القدرات أكبر بكثير مما توفره واجهة برمجة التطبيقات العامة. على سبيل المثال ، يمكن أن يؤثر على الحالة الداخلية للفئة الأصل المخفية في خصائصها protected .


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


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


أصبحت الفئة الأصل مضطرة الآن للحفاظ على الاستقرار ليس فقط للواجهة العامة ، ولكن أيضًا للواجهة المحمية ، لأن أي تغييرات في ذلك سيؤدي إلى مشاكل في عمل الفصول الفرعية. ومع ذلك ، من المستحيل رفض استخدام أعضاء الفصل protected . إذا كانت الواجهة المحمية سوف تتطابق تمامًا مع الواجهة العامة الخارجية ، أي سوف تستخدم الفئة الأصل فقط الأعضاء من public private ، ثم الميراث عموما لا معنى له.


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


باستخدام فئة من خلال واجهة محمية


انتهاك مبدأ الإخفاء من خلال واجهة محمية


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


على سبيل المثال ، نضيف وظيفة فئة CommentBlock :


 class CommentBlock { /** @var Comment[]   */ protected $comments = []; /**       `$comments` */ public function getComment(string $key): ?Comment { return $this->comments[$key] ?? null; } } 

وسوف نرث منه الفئة CustomCommentBlock ، والتي نستغل فيها كل الاحتمالات لكسر الإخفاء.


 class CustomCommentBlock extends CommentBlock { /** *    * *    (information hiding) *     `CommentBlock::$comments`, *     */ public function setComments(array $comments): void { $this->comments = $comments; } /** *    ,   `Comment::getKey()` * *       */ public function getComment(string $key): ?Comment { foreach ($this->comments as $comment) { if ($comment->getKey() === $key) { return $comment; } } return null; } } 

الحالات الشائعة لانتهاكات الإخفاء هي كما يلي:


  • تكشف طرق الفصل الفرعي عن حالة الفصل الأصل وتوفر الوصول إلى الأعضاء المخفيين للفئة الأصل. ربما لم يكن هذا السيناريو متوقعًا عند تصميم الفئة الأصل ، مما يعني أنه من المحتمل أن يتم انتهاك منطق أساليبها.
    في المثال ، توفر الفئة الفرعية طريقة تعيين CustomCommentBlock::setComments() لتعديل خاصية CommentBlock::$comments المحمية المخفية في الفئة الأصل.
  • تجاوز سلوك طريقة الفئة الأصل في الفصل الفرعي. في بعض الأحيان ينظر المطورون إلى هذه الميزة كوسيلة لحل مشكلات الفئة الأصل عن طريق إنشاء فئات فرعية ذات سلوك معدل.
    في المثال ، تعتمد CommentBlock::getComment() في الفئة الأصل على المفاتيح الموجودة في CommentBlock::$comments . وفي الفئة الفرعية - إلى مفاتيح التعليقات نفسها ، يمكن الوصول إليها من خلال أسلوب Comment::getKey() .

مشكلة قرد الموز


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

جو أرمسترونغ ، خالق إرلانج

التبعيات موجودة دائمًا في بنية النظام. ومع ذلك ، يحمل الميراث عددًا من العوامل المعقدة.


من المحتمل أن تكون قد واجهت موقفًا حيث نما التسلسل الهرمي للفصول بشكل كبير مع تطور منتج البرنامج. على سبيل المثال


 class Block { /* ... */ } class CommentBlock extends Block { /* ... */ } class PopularCommentBlock extends CommentBlock { /* ... */ } class CachedPopularCommentBlock extends PopularCommentBlock { /* ... */ } /* .... */ 

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


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


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


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


كيف الآن لاختبار الفصول الفرعية في مكان ما في شجرة التسلسل الهرمي ، وذلك لأن تطبيقها منتشر في الفصول الرئيسية؟ للاختبار ، ستحتاج إلى جميع الفئات الرئيسية ، ولا يمكنك تجاوزها بأي طريقة ، لأن لديك ارتباط ليس في السلوك ، ولكن في التنفيذ. نظرًا لأنه لا يمكن عزل فصلك واختباره بسهولة ، فسوف ترث الكثير من المشكلات - مع إمكانية الصيانة والتوسعة وإعادة الاستخدام.


العودية مفتوحة الافتراضية


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


توفر اللغات الموجهة للكائنات عودية مفتوحة بشكل افتراضي. في PHP ، يتم تطبيق العودية المفتوحة باستخدام المتغير الزائف $this . تسمى طريقة استدعاء خلال $this استدعاء الذات في الأدب.


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


  • استدعاء لأسفل - مكالمة إلى أسلوب تم إبطال تطبيقه في فصل فرعي ، في تدرج هرمي.
  • up-call - استدعاء إلى طريقة موروث تنفيذها من الفئة الأصل ، أعلى في التسلسل الهرمي. يمكنك إجراء مكالمة هاتفية بشكل صريح في PHP من خلال بنية parent::method() .

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


لنأخذ مثالا. getComments() طريقة getComments() في فئة CommentBlock الأصل ، والتي تُرجع مجموعة من التعليقات.


 class CommentBlock { /* ... */ /** *        `getComment()`. * *        `CustomCommentBlock`, * ..   `CommentBlock::getComment()`  * `CustomCommentBlock::getComment()` . */ public function getComments(): array { $comments = []; foreach ($this->comments as $key => $comment) { $comments[] = $this->getComment($key); } return $comments; } } 

تعتمد هذه الطريقة على منطق CommentBlock::getComment() وتقوم CommentBlock::getComment() التعليقات من خلال مفاتيح صفيف الجمعيات $comments . في سياق فئة CustomCommentBlock من أسلوب CommentBlock::getComments() ، سيتم تنفيذ استدعاء لأسفل للأسلوب CustomCommentBlock::getComment() . ومع ذلك ، أسلوب CustomCommentBlock::getComment() لديه سلوك مختلف عن المتوقع في الفئة الأصل. كمعلمة ، تتوقع هذه الطريقة الخاصية key للتعليق نفسه.


نتيجة لذلك ، CommentBlock::getComments() الموروثة تلقائيًا من الفئة الأصل ، غير متوافق في السلوك مع CustomCommentBlock::getComment() . استدعاء getComments() في سياق CustomCommentBlock سيعود على الأرجح صفيف من القيم null .


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


السيطرة على الآثار الجانبية


في المثال السابق ، getComment() المشكلة بسبب الاختلاف في منطق أساليب getComment() في فصول الأصل والطفل. ومع ذلك ، لا يكفي التحكم في تشابه سلوك الطرق في التسلسل الهرمي للفئة. قد تنتظرك المشكلات إذا كانت هذه الطرق لها آثار جانبية.


تعمل الوظيفة ذات التأثيرات الجانبية (وظيفة ذات تأثيرات جانبية ) على تغيير حالة النظام ، بالإضافة إلى التأثير الرئيسي - إعادة النتيجة إلى نقطة الاتصال. أمثلة على الآثار الجانبية:


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

لذلك فإن هذه الآثار الجانبية هي أيضًا جزء من التنفيذ ، والذي لا يمكن إخفاؤه أيضًا بشكل فعال في عملية الميراث.


تخيل CommentBlock نحتاج إلى تضمين طريقة viewComment() في فئة CommentBlock للحصول على تمثيل نصي لأحد التعليقات.


 class CommentBlock { /** @var Comment[]   */ protected $comments = []; /**         */ public function viewComment(string $key): string { return $this->comments[$key]->view(); } } 

أضف تأثيرًا جانبيًا على الفصل الفرعي وحدد الغرض منه. نحن نطبق فئة CountingCommentBlock ، التي تكمل CommentBlock القدرة على حساب وجهات نظر التعليقات الفردية في ذاكرة التخزين المؤقت. دع الفصل يقبل حقن ذاكرة التخزين المؤقت المتوافقة مع PSR-16 في المنشئ ( حقن المنشئ ) عبر واجهة CounterInterface (والتي تم استبعادها في النهاية من PSR-16). سنستخدم طريقة increment() لزيادة قيمة العداد تلقائيًا في ذاكرة التخزين المؤقت.


 class CountingCommentBlock extends CommentBlock { /** @var CounterInterface  */ private $cache; public function __construct(CounterInterface $cache) { $this->cache = $cache; } /**        */ public function viewComment(string $key): string { $this->cache->increment($key); return parent::viewComment($key); } } 

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


 class CommentBlock { /* ... */ /**           */ public function viewComments(): string { $view = ''; foreach ($this->comments as $key => $comment) { $view .= $comment->view(); } return $view; } } 

ومع ذلك ، لا يعرف الفصل الأصل أي شيء حول ميزات التنفيذ للفئات التابعة. viewComments() (responsibility) CountingCommentBlock – .


:


 $commentBlock = new CountingCommentBlock(new SomeCache()); /* ... */ $commentBlock->viewComments(); 

. , .


« » . , viewComments() ( ).



, . , , , . – « » (" Fragile base class "). «- » – (fragility).


, ? . , CommentBlock , .


 class CommentBlock { /** @var Comment[]   */ protected $comments = []; /**         */ public function viewComment(string $key): string { return $this->comments[$key]->view(); } /**           */ public function viewComments(): string { $view = ''; foreach ($this->comments as $key => $comment) { $view .= $comment->view(); } return $view; } } 

CountingCommentBlock .


 class CountingCommentBlock extends CommentBlock { /** @var CounterInterface  */ private $cache; public function __construct(CounterInterface $cache) { $this->cache = $cache; } /**        */ public function viewComment(string $key): string { $this->cache->increment($key); return parent::viewComment($key); } /**        */ public function viewComments(): string { foreach ($this->comments as $key => $comment) { $this->cache->increment($key); } return parent::viewComments(); } } 

CommentBlock::viewComments() :


 $view .= $comment->view(); 

, viewComment() , – . . viewComment() viewComments() . , CommentBlock::viewComment() CommentBlock::viewComments() :


 class CommentBlock { /* ... */ public function viewComments(): string { $view = ''; foreach ($this->comments as $key => $comment) { $view .= $this->viewComment($key); //  `$comment->view()` } return $view; } } 

CommentBlock , , . CommentBlock – , «». .


, . CountingCommentBlock . :


 $commentBlock = new CountingCommentBlock(new SomeCache()); /* ... */ $commentBlock->viewComments(); 

:


 CountingCommentBlock::viewComments() -> CommentBlock::viewComments() -> (n ) CountingCommentBlock::viewComment() 

: CountingCommentBlock::viewComments() CountingCommentBlock::viewComment() . أي – . CountingCommentBlock , , !


, protected . , . . , .


. , . , . , , « - ».


, . , « ». « » $this , private , .


, . , , . PHP ( public , protected , private ) final .


final


PHP . , , .. , , , . , final .


PHP 5 final , , . , .

#1.
...
#2
...

: , .

PHP, « final »


PHP . . final , .


, final – PHP, . , PHP : typehints , ..


final , , . أي , . .


, , private , . , .


, , , API. , «» , . «» - . «» , .


API « ». , .


, final .


final


« »


– . , public protected .


, , . PHP ( -) . – , , , .


, () . « » ( Template method ).


, :


  • . , () . . final . – .
  • . . , . () .

, abstract , , final , . , , . , .. final .


. , , . , . . .., , - , , , . :


  • abstract ;
  • final , , .

. « » abstract final .


CommentBlock .


 abstract class CommentBlock { /**   */ protected $comments = []; /**         */ abstract public function viewComment(string $key): string; /**           */ final public function viewComments(): string { $view = ''; foreach ($this->comments as $key => $comment) { $view .= $this->viewComment($key); } return $view; } } 

SimpleCommentBlock :


 final class SimpleCommentBlock extends CommentBlock { public function viewComment(string $key): string { return $this->comments[$key]->view(); } } 

, , :


 final class CountingCommentBlock extends CommentBlock { /**  */ private $cache; public function __construct(CounterInterface $cache) { $this->cache = $cache; } /**        */ public function viewComment(string $key): string { $this->cache->increment($key); return $this->comments[$key]->view(); } } 

«», . final final .


. , . , « » down-call , , . .


, , . , .



- , . extends , implements .


- . CommentBlock :


 interface CommentBlock { /**         */ public function viewComment(string $key): string; /**           */ public function viewComments(): string; } 

:


 final class SimpleCommentBlock implements CommentBlock { /**   */ private $comments = []; public function viewComment(string $key): string { return $this->comments[$key]->view(); } public function viewComments(): string { $view = ''; foreach ($this->comments as $key => $comment) { $view .= $this->viewComment($key); } return $view; } } 

, :


 final class CountingCommentBlock implements CommentBlock { /**   */ private $comments = []; /**  */ private $cache; public function __construct(CounterInterface $cache) { $this->cache = $cache; } /**        */ public function viewComment(string $key): string { $this->cache->increment($key); return $this->comments[$key]->view(); } public function viewComments(): string { $view = ''; foreach ($this->comments as $key => $comment) { $view .= $this->viewComment($key); } return $view; } } 

implements - ( association ) .


-, . - . final , : , ..


. . , public . implements , () , , ( ISP ). , .


/ ( OCP )? final implements – PHP .


, , , . , implements , . .


final , . . – , implements final .


« », . implements , final .


, « », , . – SimpleCommentBlock CountingCommentBlock viewComments() .



, viewComments() , , , , . , . ( decorator pattern ). , « », – final , implements .


, CommentBlock , .


 interface CommentBlock { /**      */ public function getCommentKeys(): array; /**         */ public function viewComment(string $key): string; /**           */ public function viewComments(): string; } 

, getCommentKeys() . . , protected , CommentBlock .


SimpleCommentBlock - «» . , , implements final .


 final class SimpleCommentBlock implements CommentBlock { /**   */ private $comments = []; public function getCommentKeys(): array { return array_keys($this->comments); } public function viewComment(string $key): string { return $this->comments[$key]->view(); } public function viewComments(): string { $view = ''; foreach ($this->comments as $key => $comment) { $view .= $this->viewComment($key); } return $view; } } 

CountingCommentBlock - «» – OCP . CountingCommentBlock : CommentBlock .


 final class CountingCommentBlock implements CommentBlock { /**  CommentBlock */ private $commentBlock; /**  */ private $cache; public function __construct(CommentBlock $commentBlock, CounterInterface $cache) { $this->commentBlock = $commentBlock; $this->cache = $cache; } public function getCommentKeys(): array { return $this->commentBlock->getCommentKeys(); } public function viewComment(string $key): string { $this->cache->increment($key); return $this->commentBlock->viewComment($key); } public function viewComments() : string { $commentKeys = $this->getCommentKeys(); foreach ($commentKeys as $commentKey) { $this->cache->increment($commentKey); } return $this->commentBlock->viewComments(); } } 

- CountingCommentBlock . , viewComment() . ( forwarding methods ).


, « ». getCommentKeys() . , «» , , .


, . , - «» . , – ( , SOLID) ( , , ).


. SimpleCommentBlock CountingCommentBlock CommentBlock , . -, .


, , final , CommentBlock .


SimpleCommentBlock CountingCommentBlock - « », . « » , . , « ». – .


, final , . , «--» – . DIP , .


: SimpleCommentBlock – ; CountingCommentBlockSimpleCommentBlock , (). أي , – SRP . , , ( cohesion ) .


« » – (, , ), , . , .


.


viewRandomComment() SimpleCommentBlock - CountingCommentBlock . , – viewRandomComment() . CountingCommentBlock::viewRandomComment() .


, viewComments() SimpleCommentBlock . CountingCommentBlock SimpleCommentBlock , .


 final class SimpleCommentBlock implements CommentBlock { /* ... */ /**         */ public function viewRandomComment(): string { $key = array_rand($this->comments); return $this->comments[$key]->view(); } /**      */ public function viewComments() : string { $view = ''; foreach ($this->comments as $key => $comment) { /*    `$this->viewComment()`      */ $view .= $this->comments[$key]->view(); } return $view; } } 


: , , , , , .. .


PHP , , final , . , , , . , . . – , – .


, . PHPDoc , . : .


, , (.. , public protected ). PSR-19: PHPDoc tags , , . PHPDoc , « -».


JavaDoc @implSpec , . , PHPDoc API , .. public . @implSpec , API . , protected .


 class CommentBlock { /* ... */ /** *         *       *  (,    ) * * @implSpec      `$key` *    `$this->comments`      `view()` * * @param string $key   * @return string    */ public function viewComment(string $key): string { return $this->comments[$key]->view(); } } 

PHPDoc @implSpec . $key . – view() .


, @implSpec :


  • ( , , , ..).
  • parent::method() .

, «» . , self-call ( $this ) , , :


  • $this ;
  • ;
  • .

 class CommentBlock { /* ... */ /** *           * * @implSpec    `$this->comments` *        `$this->viewComment()`. *       . * * @return string     */ final public function viewComments(): string { $view = ''; foreach ($this->comments as $key => $comment) { $view .= $this->viewComment($key); } return $view; } } 

, viewComments() final . viewComment() , . :


  • viewComment() viewComments() ;
  • viewComments() viewComment() .

, . , , . « », CommentBlock CountingCommentBlock , . viewComment() viewComments() , , .


, , . PHPDoc API : , , .


, , , , , , «». – . PHPDoc , .


, , . – final , . . – « ».


:


  • . ;
  • final . .

, , final . final , . , . , ?


final . , , . , , final , .


final


« », , , , , . , protected . , final – , , , public , .


, , . , , , . – final .


, final ? لا. - , Opensource , , . IDE . , .


, , final . final , . .


final , . public ? – , ?


, . , final , , protected .


final , code review ( code review ? ;). , .


  • final ? ? أي منها؟
  • final ? ? ?

. , , code review . .


. final PHP5 . , , ( , ) .


, , . « » , . , C# virtual , – override . PHP final extandable , « final », extend .



, – . . . .


. , . , . final , , -. , , implements .


, , final .


  1. final :


     final class SimpleCommentBlock { /* ... */ public function getCommentKeys(): array { /* ... */ } public function viewComment(string $key): string { /* ... */ } public function viewComments(): string { /* ... */ } } 

  2. . , final . , . .


    , public . , , .


    , .


     interface CommentBlock { public function getCommentKeys(): array; public function viewComment(string $key): string; public function viewComments(): string; } 

  3. .


     final class SimpleCommentBlock implements CommentBlock { /* ... */ } 

  4. -, . - CommentBlock . - .


     final class CountingCommentBlock implements CommentBlock { /* ... */ private $commentBlock; public function __construct(CommentBlock $commentBlock /* ,... */) { $this->commentBlock = $commentBlock; } /* ... */ } 


, . , final , , - . , final .


final , . , «». , . final .


final


final – ( PHPUnit, Mockery ) «» ( test doubles ). , .


, :


 final class SimpleCommentBlockTest extends TestCase { public function testCreatingTestDouble(): void { $mock = $this->createMock(SimpleCommentBlock::class); } } 

:


 Class "SimpleCommentBlock" is declared "final" and cannot be mocked. 

, « » PHPUnit , :


 class Mock_SimpleCommentBlock_591bc3f3 extends SimpleCommentBlock { /* ... */ } 

«» . -, , .. . , -, , PHPUnit . , .


final . : . .



«» . , ( Post , Comment ) - ( value object ) ( stable ) . . classical TDD ( mockist TDD )


, ( volatile ) . , , «» – , . , «» , . ( DIP ), , «» .


, . أي :


 interface CommentBlock { /* ... */ } 

:


 final class SimpleCommentBlock implements CommentBlock { /* ... */ } 

«» :


 final class CommentBlockTest extends TestCase { public function testCreatingTestDouble(): void { $mock = $this->createMock(CommentBlock::class); } } 

«» , . :


  • «» ;
  • ;
  • «» , .. DIP .


, «» – , -. , ( , ). , « » « ».


, final .


– -. -. , -, . Mockery .


 class SimpleCommentBlockTest extends TestCase { public function testCreatingProxyDouble() { /*    */ $simpleCommentBlock = new SimpleCommentBlock(); /*  - */ $proxy = Mockery::mock($simpleCommentBlock); /*    */ $proxy->shouldReceive('viewComment') ->andReturn('text'); /*     */ $this->assertEquals('text', $proxy->viewComment('1')); /* `$proxy`     `SimpleCommentBlock`  */ $this->assertNotInstanceOf(SimpleCommentBlock::class, $proxy); } } 

– - . , instanceof . ( type declarations ), - .


« » – PHP, , . Bypass Finals , final . composer final :


 public function testUsingBypassFinals(): void { /*   `final` */ BypassFinals::enable(); $mock = $this->createMock(SimpleCommentBlock::class); } 

final


, PHP . « » , final . , IDE final .


PHPStorm


PHPStorm . File | Settings | Editor | File and Code Templates Files PHP Class . final .


`PHP Class`


File | New | PHP Class :


 final class SimpleCommentBlock { } 

, . . – .


PHPStorm Refactor | Extract | Interface . , . ( Replace class reference with interface where possible ) PHPDoc ( Move PHPDoc ).


:


 interface CommentBlock { /** PHPDoc */ public function viewComment(string $key): string; } 

:


 final class SimpleCommentBlock implements CommentBlock { public function viewComment(string $key): string { /* ... */ } } 

File | New | PHP Class -, . :


 final class CountingCommentBlock implements CommentBlock { /** @var CommentBlock */ private $commentBlock; } 

Code | Generate | Constructor . .


 final class CountingCommentBlock implements CommentBlock { /* ... */ public function __construct(CommentBlock $commentBlock) { $this->commentBlock = $commentBlock; } } 

– . Code | Generate | Implement Methods . , . PHPStorm «» , IntelliJ IDEA ReSharper .


 final class CountingCommentBlock implements CommentBlock { /* ... */ /** * @inheritDoc */ public function viewComment(string $key): string { // TODO: Implement viewComment() method. } } 

PHPDoc . .


PHPStan


, . , , , final . ( ).


PHPStan . « » . PHPStan . .


FinalRule localheinz/phpstan-rules . PHPStan\Rules\Rule processNode() .


. , FinalRule « » allowAbstractClasses . , , classesNotRequiredToBeAbstractOrFinal .


, composer :


 composer require --dev phpstan/phpstan composer require --dev localheinz/phpstan-rules 

FinalRule phpstan.neon :


 services: - class: Localheinz\PHPStan\Rules\Classes\FinalRule arguments: allowAbstractClasses: true classesNotRequiredToBeAbstractOrFinal: [] tags: - phpstan.rules.rule 

( max ):


 vendor/bin/phpstan -lmax analyse src 

:


  ------ ------------------------------------------------------------------------ Line CommentBlock.php ------ ------------------------------------------------------------------------ 10 Class CommentBlock is neither abstract nor final. ------ ------------------------------------------------------------------------ 

JSON Continuous Integration .


استنتاج


, : final ! IDE, .


, , . – SOLID . , , .


, . :


  • ;
  • final , , , ;
  • .

, - - . . . , , , , . , . ?


, – , . final . ( cognitive load ) – . – .


: final . . – . . . , . , .


final . ( SOLID ) ( fragile ) . , .

Source: https://habr.com/ru/post/ar482154/


All Articles