سؤال

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

حالة 1: أريد إنشاء فئات تمثل أنواعًا مختلفة من "الإجراءات" في نظامي.يتم تصنيف الإجراءات حسب عدة معلمات:

  • يمكن أن يكون الإجراء "قراءة" أو "كتابة".
  • يمكن أن يكون الإجراء مع تأخير أو بدون تأخير (لا يقتصر الأمر على معلمة واحدة فقط.يغير السلوك بشكل ملحوظ).
  • يمكن أن يكون "نوع التدفق" الخاص بالإجراء هو FlowA أو FlowB.

أريد التصميم التالي :

// abstract classes
class Action  
{
    // methods relevant for all actions
};
class ActionRead      : public virtual Action  
{
    // methods related to reading
};
class ActionWrite     : public virtual Action  
{
    // methods related to writing
};
class ActionWithDelay : public virtual Action  
{
    // methods related to delay definition and handling
};
class ActionNoDelay   : public virtual Action  {/*...*/};
class ActionFlowA     : public virtual Action  {/*...*/};
class ActionFlowB     : public virtual Action  {/*...*/};

// concrete classes
class ActionFlowAReadWithDelay  : public ActionFlowA, public ActionRead, public ActionWithDelay  
{
    // implementation of the full flow of a read command with delay that does Flow A.
};
class ActionFlowBReadWithDelay  : public ActionFlowB, public ActionRead, public ActionWithDelay  {/*...*/};
//...

بالطبع، سأطيع أنه لا يوجد إجراءان (وراثان من فئة العمل) سينفذان نفس الطريقة.

الحالة 2: أقوم بتنفيذ نمط التصميم المركب لـ "الأمر" في نظامي.يمكن قراءة الأمر وكتابته وحذفه وما إلى ذلك.أريد أيضًا الحصول على سلسلة من الأوامر، والتي يمكن أيضًا قراءتها وكتابتها وحذفها وما إلى ذلك.يمكن أن يحتوي تسلسل الأوامر على تسلسلات أخرى من الأوامر.

لذلك لدي التصميم التالي:

class CommandAbstraction
{
    CommandAbstraction(){};
    ~CommandAbstraction()=0;
    void Read()=0;
    void Write()=0;
    void Restore()=0;
    bool IsWritten() {/*implemented*/};
    // and other implemented functions
};

class OneCommand : public virtual CommandAbstraction
{
    // implement Read, Write, Restore
};

class CompositeCommand : public virtual CommandAbstraction
{
    // implement Read, Write, Restore
};

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

class ModernCommand : public virtual CommandAbstraction
{
    ~ModernCommand()=0;
    void SetModernPropertyA(){/*...*/}
    void ExecModernSomething(){/*...*/}
    void ModernSomethingElse()=0;

};
class OneModernCommand : public OneCommand, public ModernCommand
{
    void ModernSomethingElse() {/*...*/};
    // ... few methods specific for OneModernCommand
};
class CompositeModernCommand : public CompositeCommand, public ModernCommand
{
    void ModernSomethingElse() {/*...*/};
    // ... few methods specific for CompositeModernCommand
};

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

شكرًا لك.

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

المحلول

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

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

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

class ActionDelayPolicy_NoWait;

class ActionBase // Only needed if you want to use polymorphically different actions
{
public:
    virtual ~Action() {}
    virtual void run() = 0;
};

template < typename Command, typename DelayPolicy = ActionDelayPolicy_NoWait >
class Action : public DelayPolicy, public Command
{
public:
   virtual run() {
      DelayPolicy::wait(); // inherit wait from DelayPolicy
      Command::execute();  // inherit command to execute
   }
};

// Real executed code can be written once (for each action to execute)
class CommandSalute
{
public:
   void execute() { std::cout << "Hi!" << std::endl; }
};

class CommandSmile
{
public:
   void execute() { std::cout << ":)" << std::endl; }
};

// And waiting behaviors can be defined separatedly:
class ActionDelayPolicy_NoWait
{
public:
   void wait() const {}
};

// Note that as Action inherits from the policy, the public methods (if required)
// will be publicly available at the place of instantiation
class ActionDelayPolicy_WaitSeconds
{
public:
   ActionDelayPolicy_WaitSeconds() : seconds_( 0 ) {}
   void wait() const { sleep( seconds_ ); }
   void wait_period( int seconds ) { seconds_ = seconds; }
   int wait_period() const { return seconds_; }
private:
   int seconds_;
};

// Polimorphically execute the action
void execute_action( Action& action )
{
   action.run();
}

// Now the usage:
int main()
{
   Action< CommandSalute > salute_now;
   execute_action( salute_now );

   Action< CommandSmile, ActionDelayPolicy_WaitSeconds > smile_later;
   smile_later.wait_period( 100 ); // Accessible from the wait policy through inheritance
   execute_action( smile_later );
}

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

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

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

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

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

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

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

نصائح أخرى

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

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

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

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

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

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

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

"الميراث المتعدد يعتبر مفيدًا"

يعد "الماس" في التسلسل الهرمي لوراثة الواجهات آمنًا تمامًا - فهو وراثة التعليمات البرمجية التي تضعك في موقف صعب.

لإعادة استخدام التعليمات البرمجية، أنصحك بالتفكير في mixins (google for C++ Mixins إذا لم تكن على دراية بالتقنية).عند استخدام mixins، تشعر أنه يمكنك "التسوق" للحصول على مقتطفات التعليمات البرمجية التي تحتاجها لتنفيذ صفك دون استخدام وراثة متعددة للفئات ذات الحالة.

لذا، فإن النمط هو - وراثة متعددة للواجهات وسلسلة واحدة من الخلطات (مما يتيح لك إعادة استخدام التعليمات البرمجية) للمساعدة في تنفيذ الفصل الملموس.

امل ان يساعد!

مع المثال الأول .....

ما إذا كان ActionRead ActionWrite بحاجة إلى أن يكون فئات فرعية من الإجراء على الإطلاق.

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

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

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

شيء مثل ما يلي (الذي ليس له وراثة الماس):

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

class Action // abstract
{
   // Reader and writer would be abstract classes (if not interfaces)
   // from which you would derive to implement the specific
   // read and write protocols.

   class Reader // abstract
   {
      Class Delay {...};
      Delay *optional_delay; // NULL when no delay
      Reader (bool with_delay)
      : optional_delay(with_delay ? new Delay() : NULL)
      {};
      ....
   };

   class Writer {... }; // abstract

   Reader  *reader; // may be NULL if not a reader
   Writer  *writer; // may be NULL if not a writer

   Action (Reader *_reader, Writer *_writer)
   : reader(_reader)
   , writer(_writer)
   {};

   void read()
   { if (reader) reader->read(); }
   void write()
   { if (writer)  writer->write(); }
};


Class Flow : public Action
{
   // Here you would likely have enhanced version
   // of read and write specific that implements Flow behaviour
   // That would be comment to FlowA and FlowB
   class Reader : public Action::Reader {...}
   class Writer : public Action::Writer {...}
   // for Reader and W
   Flow (Reader *_reader, Writer *_writer)
   : Action(_reader,_writer)
   , writer(_writer)
   {};
};

class FlowA :public Flow  // concrete
{
    class Reader : public Flow::Reader {...} // concrete
    // The full implementation for reading A flows
    // Apparently flow A has no write ability
    FlowA(bool with_delay)
    : Flow (new FlowA::Reader(with_delay),NULL) // NULL indicates is not a writer
    {};
};

class FlowB : public Flow // concrete
{
    class Reader : public Flow::Reader {...} // concrete
    // The full implementation for reading B flows
    // Apparently flow B has no write ability
    FlowB(bool with_delay)
    : Flow (new FlowB::Reader(with_delay),NULL) // NULL indicates is not a writer
    {};
};

بالنسبة للحالة 2، ليست أ OneCommand مجرد حالة خاصة من CompositeCommand؟إذا قمت بالقضاء على OneCommand والسماح CompositeCommandنظرًا لأنه يحتوي على عنصر واحد فقط، أعتقد أن تصميمك يصبح أكثر بساطة:

              CommandAbstraction
                 /          \
                /            \
               /              \
        ModernCommand      CompositeCommand
               \               /
                \             /
                 \           /
             ModernCompositeCommand

لا تزال لديك الماسة المخيفة، ولكن أعتقد أن هذه قد تكون حالة مقبولة لها.

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