مبدأ مفتوح مغلق

مرحبا يا هبر! إليكم ترجمة لمقال كتبه روبرت مارتن من المبدأ المفتوح ، والذي نشره في يناير 1996. المقالة ، بعبارة ملطفة ، ليست هي الأحدث. لكن في RuNet ، تتم إعادة سرد مقالات العم بوب حول SOLID فقط في شكل مقطوع ، لذلك اعتقدت أن الترجمة الكاملة لن تكون ضرورية.



قررت أن أبدأ بالحرف O ، لأن مبدأ الانفتاح ، في الواقع ، أساسي. من بين أشياء أخرى ، هناك العديد من التفاصيل الدقيقة التي تستحق الاهتمام بما يلي:


  • لا يمكن "إغلاق" البرنامج بنسبة 100٪.
  • البرمجة الموجهة للكائنات (OOP) لا تعمل مع الأشياء المادية للعالم الحقيقي ، ولكن مع المفاهيم - على سبيل المثال ، مفهوم "الترتيب".

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


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


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


إذا كان أحد التغييرات في البرنامج يستتبع سلسلة من التغييرات في الوحدات التابعة ، فإن البرنامج يعرض علامات غير مرغوب فيها لتصميم "سيء".


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


وصف


تتميز الوحدات التي تتوافق مع مبدأ الانفتاح والالتزام بخاصيتين رئيسيتين:


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

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


مفتاح الحل هو التجريد.


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


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


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


صورة
عميل مغلق


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


صورة
افتح العميل


Shape مجردة


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


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


 // 1 //  /    enum ShapeType {circle, square} struct Shape { ShapeType itsType; }; struct Circle { ShapeType itsType; double itsRadius; Point itsCenter; }; struct Square { ShapeType itsType; double itsSide; Point itsTopLeft; }; // //     // void DrawSquare(struct Square*) void DrawCircle(struct Circle*); typedef struct Shape *ShapePointer; void DrawAllShapes(ShapePointer list[], int n) { int i; for (i=0; i<n; i++) { struct Shape* s = list[i]; switch (s->itsType) { case square: DrawSquare((struct Square*)s); break; case circle: DrawCircle((struct Circle*)s); break; } } } 

DrawAllShapes وظيفة DrawAllShapes مع مبدأ الانفتاح DrawAllShapes ، لأنه لا يمكن "إغلاقه" من أنواع جديدة من الأشكال. إذا أردت توسيع هذه الوظيفة مع إمكانية رسم أشكال من قائمة تتضمن مثلثات ، فسوف أحتاج إلى تغيير الوظيفة. في الحقيقة ، عليّ تغيير الوظيفة لكل نوع جديد من الأشكال التي أحتاج إلى رسمها.


بالطبع ، هذا البرنامج هو مجرد مثال. في الحياة الواقعية ، سوف يتكرر مشغل switch من وظيفة DrawAllShapes مرارًا وتكرارًا في وظائف مختلفة في جميع أنحاء التطبيق ، وسيعمل كل منهم بشيء مختلف. تعني إضافة أشكال جديدة لمثل هذا التطبيق إيجاد جميع الأماكن التي تستخدم فيها switch (أو سلاسل أخرى) ، وإضافة شكل جديد لكل منها. علاوة على ذلك ، من غير المحتمل أن تكون جميع switch وما if/else سلاسل أخرى منظمة بشكل جيد كما هو الحال في DrawAllShapes . من الأرجح أن تتنبأ بما if سيتم دمج البيانات مع عوامل تشغيل منطقية ، أو سيتم دمج كتل switch بطريقة "تبسيط" مكان معين في الكود. لذلك ، يمكن أن تكون مشكلة إيجاد وفهم جميع الأماكن التي تحتاج إلى إضافة رقم جديد فيها أمرًا غير تافه.


في القائمة 2 ، سأعرض رمزًا يوضح حل مربع / دائرة يفي بمبدأ الإغلاق المفتوح. يتم تقديم فئة مجردة Shape . تحتوي هذه الفئة التجريدية على دالة Draw ظاهرية واحدة خالصة. فصول Circle Square هي أحفاد فئة Shape .


 // 2 //  /  - class Shape { public: virtual void Draw() const = 0; }; class Square : public Shape { public: virtual void Draw() const; }; class Circle : public Shape { public: virtual void Draw() const; }; void DrawAllShapes(Set<Shape*>& list) { for (Iterator<Shape*>i(list); i; i++) (*i)->Draw(); } 

لاحظ أنه إذا كنا نريد توسيع سلوك دالة DrawAllShapes في القائمة 2 لرسم نوع جديد من DrawAllShapes ، كل ما نحتاج إلى القيام به هو إضافة سليل جديد من فئة Shape . لا حاجة لتغيير وظيفة DrawAllShapes . لذلك ، DrawAllShapes يلبي مبدأ الانفتاح التقارب. يمكن توسيع سلوكها دون تغيير الوظيفة نفسها.


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


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


إستراتيجية الدخول المغلقة


من الواضح ، لا يمكن إغلاق أي برنامج بنسبة 100٪. على سبيل المثال ، ماذا يحدث لوظيفة DrawAllShapes في القائمة 2 إذا قررنا أن الدوائر ثم المربعات يجب رسمها أولاً؟ لا DrawAllShapes إغلاق دالة DrawAllShapes من هذا النوع من التغيير. بشكل عام ، لا يهم مدى "إغلاق" الوحدة النمطية ، فهناك دائمًا نوع من التغيير لا يتم إغلاقه.


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


استخدام التجريد لتحقيق تقارب إضافي


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


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


في C ++ ، يمكن تمثيل هذه الوظيفة كحمل زائد للمشغل "<". يوضح Shape 3 فئة Shape مع أساليب الفرز.


الآن بعد أن أصبح لدينا طريقة لتحديد ترتيب كائنات فئة Shape ، يمكننا فرزها ثم رسمها. سرد 4 يوضح رمز C ++ المطابق. يستخدم OrderedSet Set و OrderedSet و Iterator من فئة Components تطويرها في كتابي (تصميم تطبيقات كائن C ++ الموجه باستخدام أسلوب Booch ، Robert C. Martin ، Prentice Hall ، 1995).


لذلك ، قمنا بتطبيق ترتيب كائنات فئة Shape ورسمها بالترتيب المناسب. لكننا ما زلنا لا ننفذ تنفيذ تجريد الطلب. من الواضح ، أن كل كائن Shape يجب أن يتجاوز الأسلوب Precedes لتحديد الترتيب. كيف يمكن لهذا العمل؟ ما الرمز الذي يجب كتابته في Circle::Precedes بحيث يتم رسم الدوائر على المربعات؟ إيلاء الاهتمام لإدراج 5.


 // 3 //  Shape    . class Shape { public: virtual void Draw() const = 0; virtual bool Precedes(const Shape&) const = 0; bool operator<(const Shape& s) {return Precedes(s);} }; 

 // 4 // DrawAllShapes   void DrawAllShapes(Set<Shape*>& list) { //    OrderedSet  . OrderedSet<Shape*> orderedList = list; orderedList.Sort(); for (Iterator<Shape*> i(orderedList); i; i++) (*i)->Draw(); } 

 // 5 //    bool Circle::Precedes(const Shape& s) const { if (dynamic_cast<Square*>(s)) return true; else return false; } 

من الواضح أن هذه الوظيفة لا تفي بمبدأ الانفتاح. لا توجد طريقة لإغلاقه من أحفاد جديدة من فئة Shape . في كل مرة يظهر سليل جديد من فئة Shape ، يجب تغيير هذه الوظيفة.


باستخدام نهج يحركها البيانات لتحقيق الإغلاق


يمكن تحقيق تقارب ورثة فئة Shape باستخدام نهج جدولي لا يؤدي إلى حدوث تغييرات في كل فئة موروثة. ويرد مثال على هذا النهج في القائمة 6.


باستخدام هذا النهج ، قمنا بنجاح بإغلاق وظيفة DrawAllShapes من التغييرات المتعلقة DrawAllShapes ، وكل من أحفاد فئة الفئة - من تقديم سليل جديد أو من تغيير في سياسة ترتيب كائنات فئة Shape وفقًا لنوعها (على سبيل المثال ، يجب أن الكائنات من فئة Squares أن يوجه أولا).


 // 6 //     #include <typeinfo.h> #include <string.h> enum {false, true}; typedef int bool; class Shape { public: virtual void Draw() const = 0; virtual bool Precedes(const Shape&) const; bool operator<(const Shape& s) const {return Precedes(s);} private: static char* typeOrderTable[]; }; char* Shape::typeOrderTable[] = { "Circle", "Square", 0 }; //      . //   ,    //  . ,    , //      bool Shape::Precedes(const Shape& s) const { const char* thisType = typeid(*this).name(); const char* argType = typeid(s).name(); bool done = false; int thisOrd = -1; int argOrd = -1; for (int i=0; !done; i++) { const char* tableEntry = typeOrderTable[i]; if (tableEntry != 0) { if (strcmp(tableEntry, thisType) == 0) thisOrd = i; if (strcmp(tableEntry, argType) == 0) argOrd = i; if ((argOrd > 0) && (thisOrd > 0)) done = true; } else // table entry == 0 done = true; } return thisOrd < argOrd; } 

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


مزيد من الإغلاق


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


الاستدلال والاتفاقيات


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


اجعل جميع متغيرات الأعضاء خاصة


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


في OOP ، نتوقع أن طرق الفصل ليست مغلقة للتغييرات في المتغيرات التي هي أعضاء في هذه الفئة. ومع ذلك ، نتوقع أن يتم إغلاق أي فئة أخرى ، بما في ذلك الفئات الفرعية ، من التغييرات على هذه المتغيرات. وهذا ما يسمى التغليف.


ولكن ماذا لو كان لديك متغير حوله أنت متأكد من أنه لن يتغير أبداً؟ هل يعقل أن يجعلها private ؟ على سبيل المثال ، يُظهر " Device 7" فئة Device الذي يحتوي على bool status العضو المتغير. يخزن حالة العملية الأخيرة. إذا نجحت العملية ، فستكون قيمة متغير status true أو غير false .


 // 7 //   class Device { public: bool status; }; 

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


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


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


 // 8 class Time { public: int hours, minutes, seconds; Time& operator-=(int seconds); Time& operator+=(int seconds); bool operator< (const Time&); bool operator> (const Time&); bool operator==(const Time&); bool operator!=(const Time&); }; 

الشكوى الوحيدة التي يمكنني تقديمها إلى الكود في القائمة 8 هي أن تغيير الوقت ليس ذريًا. بمعنى ، يمكن للعميل تغيير قيمة متغير minutes دون تغيير قيمة متغير hours . يمكن أن يؤدي هذا إلى كائن من فئة Time يحتوي على بيانات غير متناسقة. أفضّل تقديم وظيفة واحدة لتحديد الوقت ، الأمر الذي سيستغرق ثلاث حجج ، مما يجعل ضبط الوقت عملية ذرية. لكن هذه حجة ضعيفة.


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


لذلك ، في مثل هذه الحالات النادرة ، عندما لا ينتهك مبدأ الانفتاح ، فإن حظر المتغيرات public protected يعتمد أكثر على الأسلوب وليس على المحتوى.


لا متغيرات عالمية ... على الإطلاق!


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


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


RTTI أمر خطير


حظر آخر شائع هو استخدام dynamic_cast . في كثير من الأحيان ، يتم اتهام dynamic_cast أو شكل آخر من أشكال تحديد نوع وقت التشغيل (RTTI) بأنه أسلوب خطير للغاية وبالتالي يجب تجنبه. في الوقت نفسه ، غالبًا ما يقدمون مثالًا من القائمة 9 ، والذي ينتهك بوضوح مبدأ الانفتاح. ومع ذلك ، تعرض القائمة 10 مثالًا لبرنامج مشابه يستخدم dynamic_cast دون انتهاك مبدأ الفتح المفتوح.


الفرق بينهما هو أنه في الحالة الأولى ، كما هو موضح في القائمة رقم 9 ، يجب تغيير الكود في كل مرة يظهر فيها سليل جديد من فئة Shape (ناهيك عن أن هذا حل سخيف للغاية). ومع ذلك ، في القائمة 10 ، لا توجد تغييرات مطلوبة في هذه الحالة. لذلك ، لا ينتهك الكود الموجود في القائمة 10 مبدأ الإغلاق المفتوح.
في هذه الحالة ، تتمثل قاعدة الإبهام في أنه يمكن استخدام RTTI إذا لم يتم انتهاك مبدأ الإغلاق المفتوح.


 // 9 //RTTI,   -. class Shape {}; class Square : public Shape { private: Point itsTopLeft; double itsSide; friend DrawSquare(Square*); }; class Circle : public Shape { private: Point itsCenter; double itsRadius; friend DrawCircle(Circle*); }; void DrawAllShapes(Set<Shape*>& ss) { for (Iterator<Shape*>i(ss); i; i++) { Circle* c = dynamic_cast<Circle*>(*i); Square* s = dynamic_cast<Square*>(*i); if (c) DrawCircle(c); else if (s) DrawSquare(s); } } 

 // 10 //RTTI,    -. class Shape { public: virtual void Draw() cont = 0; }; class Square : public Shape { // . }; void DrawSquaresOnly(Set<Shape*>& ss) { for (Iterator<Shape*>i(ss); i; i++) { Square* s = dynamic_cast<Square*>(*i); if (s) s->Draw(); } } 

استنتاج


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


, - -. , , , , , .

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


All Articles