سؤال

لقد تعثرت مؤخرًا عبر هذا الإدخال في مدونة اختبار Google حول الإرشادات لكتابة رمز أكثر قابلية للاختبار. كنت متفقًا مع المؤلف حتى هذه النقطة:

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

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

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

هل كانت مفيدة؟

المحلول

في الواقع هذا يجعل الاختبار والرمز أسهل في الكتابة.

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

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

class Animal
{
    public:
       Noise warningNoise();
       Noise pleasureNoise();
    private:
       AnimalType type;
};

Noise Animal::warningNoise()
{
    switch(type)
    {
        case Cat: return Hiss;
        case Dog: return Bark;
    }
}
Noise Animal::pleasureNoise()
{
    switch(type)
    {
        case Cat: return Purr;
        case Dog: return Bark;
    }
}

في هذه الحالة البسيطة ، يتطلب كل أسباب حيوانية جديدة تحديث كل من عبارات التبديل.
نسيت واحدة؟ ما هو الافتراضي؟ حية!!

باستخدام تعدد الأشكال

class Animal
{
    public:
       virtual Noise warningNoise() = 0;
       virtual Noise pleasureNoise() = 0;
};

class Cat: public Animal
{
   // Compiler forces you to define both method.
   // Otherwise you can't have a Cat object

   // All code local to the cat belongs to the cat.

};

باستخدام تعدد الأشكال ، يمكنك اختبار فئة الحيوان.
ثم اختبار كل من الفئات المشتقة بشكل منفصل.

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

نصائح أخرى

لا تخف...

أعتقد أن مشكلتك تكمن في الألفة وليس التكنولوجيا. تعرف على C ++ OOP.

C ++ هي لغة OOP

من بين نماذجها المتعددة ، لديها ميزات OOP وهي أكثر من قادرة على دعم المقارنة مع معظم لغة OO النقية.

لا تدع "جزء C داخل C ++" يجعلك تعتقد أن C ++ لا يمكنه التعامل مع النماذج الأخرى. يمكن C ++ التعامل مع الكثير من نماذج البرمجة بلطف. ومن بينها ، OOP C ++ هو أكثر نماذج C ++ نضجًا بعد النموذج الإجرائي (أي الجزء "C" المذكور أعلاه.

تعدد الأشكال على ما يرام للإنتاج

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

التبديل وتعدد الأشكال متشابهة [تقريبًا] ...

... لكن تعدد الأشكال أزال معظم الأخطاء.

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

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

ولكن في النهاية ، إذا نسيت التعامل مع حالة في Switch ، فلن يخبرك المترجم ، بينما يتم إخبارك إذا كنت تستمد من فصل دون تجاوز أساليبها الافتراضية الخالصة. وبالتالي يتم تجنب معظم أخطاء التبديل.

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

تجنب استخدام RTTI للعثور على نوع كائن

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

إذا كنت تستخدم RTTI كمفتاح مجيد ، فأنت تفتقد هذه النقطة.

(إخلاء المسئولية: أنا معجب كبير بمفهوم RTTI و Dynamic_casts. ولكن يجب على المرء استخدام الأداة الصحيحة للمهمة في متناول اليد ، ومعظم الوقت يتم استخدام RTTI كمفتاح مجيد ، وهو خطأ)

قارن التعدد الديناميكي مقابل تعدد الأشكال الثابت

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

إذا كان الكود الخاص بك يعرف النوع في وقت الترجمة ، فربما يمكنك استخدام تعدد الأشكال الثابت ، أي نمط CRTP http://en.wikipedia.org/wiki/curially_recurring_template_pattern

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

مثال رمز الإنتاج

يتم استخدام رمز مشابه لهذا (من الذاكرة) في الإنتاج.

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

void MyProcedure(int p_iCommand, void *p_vParam)
{
   // A LOT OF CODE ???
   // each case has a lot of code, with both similarities
   // and differences, and of course, casting p_vParam
   // into something, depending on hoping no one
   // did a mistake, associating the wrong command with
   // the wrong data type in p_vParam

   switch(p_iCommand)
   {
      case COMMAND_AAA: { /* A LOT OF CODE (see above) */ } break ;
      case COMMAND_BBB: { /* A LOT OF CODE (see above) */ } break ;
      // etc.
      case COMMAND_XXX: { /* A LOT OF CODE (see above) */ } break ;
      case COMMAND_ZZZ: { /* A LOT OF CODE (see above) */ } break ;
      default: { /* call default procedure */} break ;
   }
}

أضافت كل إضافة من الأمر حالة.

المشكلة هي أن بعض الأوامر التي تشابهها ، وشاركها جزئيا تنفيذها.

لذلك كان خلط الحالات خطرًا على التطور.

لقد قمت بحل المشكلة باستخدام نمط الأوامر ، أي إنشاء كائن أمر أساسي ، باستخدام طريقة واحدة ().

لذلك أقوم بإعادة كتابة إجراء الرسالة ، وتقليل الكود الخطير (أي اللعب مع void *، وما إلى ذلك) إلى الحد الأدنى ، وكتبته للتأكد من أنني لن أحتاج أبدًا إلى لمسه مرة أخرى:

void MyProcedure(int p_iCommand, void *p_vParam)
{
   switch(p_iCommand)
   {
      // Only one case. Isn't it cool?
      case COMMAND:
         {
           Command * c = static_cast<Command *>(p_vParam) ;
           c->process() ;
         }
         break ;
      default: { /* call default procedure */} break ;
   }
}

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

أدى ذلك إلى التسلسل الهرمي (ممثل كشجرة):

[+] Command
 |
 +--[+] CommandServer
 |   |
 |   +--[+] CommandServerInitialize
 |   |
 |   +--[+] CommandServerInsert
 |   |
 |   +--[+] CommandServerUpdate
 |   |
 |   +--[+] CommandServerDelete
 |
 +--[+] CommandAction
 |   |
 |   +--[+] CommandActionStart
 |   |
 |   +--[+] CommandActionPause
 |   |
 |   +--[+] CommandActionEnd
 |
 +--[+] CommandMessage

الآن ، كل ما كنت بحاجة إلى فعله هو تجاوز العملية لكل كائن.

بسيطة ، وسهلة تمديدها.

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

class CommandAction : public Command
{
   // etc.
   virtual void process() // overriding Command::process pure virtual method
   {
      this->processBefore() ;
      this->processWhile() ;
      this->processAfter() ;
   }

   virtual void processBefore() = 0 ; // To be overriden

   virtual void processWhile()
   {
      // Do something common for all CommandAction objects
   }

   virtual void processAfter()  = 0 ; // To be overriden

} ;

وعلى سبيل المثال ، يمكن ترميز CommandActionStart على النحو التالي:

class CommandActionStart : public CommandAction
{
   // etc.
   virtual void processBefore()
   {
      // Do something common for all CommandActionStart objects
   }

   virtual void processAfter()
   {
      // Do something common for all CommandActionStart objects
   }
} ;

كما قلت: من السهل الفهم (إذا تم التعليق بشكل صحيح) ، ومن السهل جدًا تمديده.

يتم تقليل المفتاح إلى الحد الأدنى الخاص به (أي IF-like ، لأننا ما زلنا بحاجة إلى تفويض أوامر Windows إلى الإجراء الافتراضي Windows) ، ولا حاجة إلى RTTI (أو أسوأ ، في المنزل RTTI).

أعتقد أن نفس الرمز داخل المفتاح سيكون مسلياً للغاية ، على ما أعتقد (إذا حكم فقط بمبلغ الكود "التاريخي" الذي رأيته في تطبيقنا في العمل).

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

تعدد الأشكال يجعل هذا ممكنا من خلال القضاء على تلك البيانات الشرطية. النظر في هذا المثال:

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

If (weapon is a rifle) then //Code to attack with rifle else
If (weapon is a plasma gun) //Then code to attack with plasma gun

إلخ.

مع تعدد الأشكال ، لا يتعين على الشخصية "معرفة" نوع السلاح ، ببساطة

weapon.attack()

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

أنا متشكك قليلاً: أعتقد أن الميراث غالبًا ما يضيف تعقيدًا أكثر مما يزيل.

أعتقد أنك تطرح سؤالًا جيدًا ، وشيء واحد أعتبره هو:

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

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

أعتقد أن الحل الافتراضي هو الحل المفرد ، والعبء في المبرمج يقترح الميراث لإثبات قضيتهم.

ليس خبيرًا في الآثار المترتبة على حالات الاختبار ، ولكن من منظور تطوير البرمجيات:

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

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

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

يمكنك أيضًا تبسيط خوارزمية العميل عن طريق التعامل مع نوع واحد فقط: الواجهة.

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

تعدد الأشكال هو الأساس الذي تم بناء قاعدة شركتنا من خلالها ، لذلك أعتبرها عملية للغاية.

المفاتيح وتعدد الأشكال يفعل نفس الشيء.

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

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

في بعض الحالات ، قد يكون لديك كمية ثابتة من الأنواع ، ويمكن أن تأتي الوظائف الجديدة ، ثم تكون المفاتيح أفضل. لكن إضافة أنواع جديدة تجعلك تحديث كل مفتاح.

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

نظرًا لأن نقاط الأشكال المؤيدة للخلط تتم مناقشتها جيدًا هنا ، اسمحوا لي أن أقدم نقطة تبديل.

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

يمكن إجراء المفاتيح بشكل صحيح:

switch (type)
{
    case T_FOO: doFoo(); break;
    case T_BAR: doBar(); break;
    default:
        fprintf(stderr, "You, who are reading this, add a new case for %d to the FooBar function ASAP!\n", type);
        assert(0);
}

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

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

إذا كنت ترغب في تمديد هذه المفاتيح فقط قم بعمل grep 'case[ ]*T_BAR' rn . (على Linux) وسوف يبصق المواقع التي تستحق النظر إليها. نظرًا لأنك تحتاج إلى إلقاء نظرة على الكود ، سترى بعض السياق الذي يساعدك على كيفية إضافة الحالة الجديدة بشكل صحيح. عند استخدام تعدد الأشكال ، يتم إخفاء مواقع الاتصال داخل النظام ، وتعتمد على صحة الوثائق ، إذا كانت موجودة على الإطلاق.

لا يؤدي تمديد المفاتيح إلى كسر OCP أيضًا ، نظرًا لأنك لا تغير الحالات الحالية ، فقط أضف حالة جديدة.

تساعد المفاتيح أيضًا الرجل التالي الذي يحاول التعود على الكود وفهمه:

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

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

يمكن العثور على مزيد من النقاش هنا: http://c2.com/cgi/wiki؟switchstatementssmell

أخشى أن "متلازمة C-hacker" وستحرق معاداة الصبغة كل سمعتي هنا. ولكن كلما احتجت أو اضطررت إلى اختراق شيء أو صاخبة في نظام C الإجرائي ، وجدت أنه من السهل جدًا ، وعدم وجود قيود ، والتغليف القسري ، وطبقات التجريد الأقل ، يجعلني "أفعل ذلك". ولكن في نظام C ++/C#/java حيث عشرات طبقات التجريد مكدسة في الجزء العلوي من بعضها البعض في عمر البرنامج ، أحتاج إلى قضاء عدة ساعات في بعض الأحيان في أيام لمعرفة كيفية العمل بشكل صحيح حول جميع القيود والقيود التي يقوم بها المبرمجين الآخرون مدمجة في نظامهم لتجنب الآخرين "العبث مع صفهم".

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

switch(obj.type): {
case 1: cout << "Type 1" << obj.foo <<...; break;   
case 2: cout << "Type 2" << ...

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

cout << object.toString();

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

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

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

أنه يعمل بشكل جيد جدا إذا فهمت ذلك.

هناك أيضا 2 نكهات من تعدد الأشكال. الأول سهل للغاية في جافا:

interface A{

   int foo();

}

final class B implements A{

   int foo(){ print("B"); }

}

final class C implements A{

   int foo(){ print("C"); }

}

مشاركة B و C واجهة مشتركة. لا يمكن تمديد B و C في هذه الحالة ، لذلك أنت متأكد دائمًا من FOO () الذي تتصل به. الشيء نفسه ينطبق على C ++ ، فقط اجعل :: Foo Pure Virtual.

ثانياً ، وأكثر صعوبة هو تعدد الأشكال وقت التشغيل. لا يبدو سيئًا للغاية في الرمز الزائف.

class A{

   int foo(){print("A");}

}

class B extends A{

   int foo(){print("B");}

}

class C extends B{

  int foo(){print("C");}

}

...

class Z extends Y{

   int foo(){print("Z");

}

main(){

   F* f = new Z();
   A* a = f;
   a->foo();
   f->foo();

}

لكنها أكثر صعوبة. خاصة إذا كنت تعمل في C ++ حيث قد تكون بعض إعلانات FOO افتراضية ، وقد تكون بعض الميراث افتراضيًا. أيضا الجواب على هذا:

A* a  = new Z;
A  a2 = *a;
a->foo();
a2.foo();

قد لا يكون ما تتوقعه.

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

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

تحقق أيضًا من كتاب "Martin Fowlers" على "إعادة البناء"
باستخدام مفتاح بدلاً من تعدد الأشكال هو رائحة رمز.

يعتمد الأمر حقًا على أسلوبك في البرمجة. على الرغم من أن هذا قد يكون صحيحًا في Java أو C#، إلا أنني لا أوافق على أن قرار تعدد الأشكال تلقائيًا هو الصحيح. يمكنك تقسيم التعليمات البرمجية إلى الكثير من الوظائف الصغيرة وإجراء بحث صفيف مع مؤشرات الوظائف (التي تمت تهيئتها في وقت الترجمة) ، على سبيل المثال. في C ++ ، غالبًا ما يتم الإفراط في تعدد الأشكال والفصول الدراسية - ربما يكون أكبر خطأ في التصميم من قبل الأشخاص القادمين من لغات OOP القوية إلى C ++ هو أن كل شيء يذهب إلى الفصل - هذا غير صحيح. يجب أن يحتوي الفصل على الحد الأدنى من الأشياء التي تجعلها تعمل ككل. إذا كانت الفئة الفرعية أو الصديق ضرورية ، فليكن ذلك ، لكن لا ينبغي أن تكون هي القاعدة. يجب أن تكون أي عمليات أخرى في الفصل وظائف مجانية في نفس مساحة الاسم ؛ سوف ADL تسمح باستخدام هذه الوظائف دون البحث.

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

مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top