سؤال

أحاول تنفيذ الأمر تصميم نمط, لكن أنا عثرة تزوجنا مشكلة مفاهيمية.دعونا نقول لديك قاعدة فئة قليلة فرعية كما في المثال أدناه:

class Command : public boost::noncopyable {
    virtual ResultType operator()()=0;

    //Restores the model state as it was before command's execution.
    virtual void undo()=0;

    //Registers this command on the command stack.
    void register();
};


class SomeCommand : public Command {
    virtual ResultType operator()(); // Implementation doesn't really matter here
    virtual void undo(); // Same
};

الشيء هو, في كل مرة المشغل () ويطلق على SomeCommand سبيل المثال ، أود أن أضيف *هذا إلى كومة (في الغالب من أجل التراجع عن الأغراض) عن طريق استدعاء القيادة طريقة التسجيل.أود أن تجنب الاتصال "تسجيل" من SomeCommand::المشغل () () ، ولكن أن يكون ذلك دعا أوتوماتيكيا (نوعا ;-) )

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

كيف يمكن أن أفعل هذا ؟ اعتقد ان التصميم هو معيبة إلى حد ما, ولكن أنا لا أعرف حقا كيفية جعل هذا العمل.

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

المحلول

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

class command {
public:
   void operator()() {
      do_command();
      add_to_undo_stack(this);
   }
   void undo();
private:
   virtual void do_command();
   virtual void do_undo();
};

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

إضافة: الأصل مقالة - سلعة بواسطة Herb Sutter حيث يقدم هذا المفهوم (لم يكشف عن اسمه)

نصائح أخرى

قم بتقسيم المشغل بطريقتين مختلفتين ، مثل التنفيذ والتنفيذ (لأكون صادقًا ، لا أحب المشغل () حقًا). Make Command :: execute non virtual ، و command :: executeimpl pure Virtual ، ثم دع الأمر :: تنفيذ تنفيذ التسجيل ، ثم اتصل به executeimpl ، مثل هذا:

class Command
   {
   public:
      ResultType execute()
         {
         ... // do registration
         return executeImpl();
         }
   protected:
      virtual ResultType executeImpl() = 0;
   };

class SomeCommand
   {
   protected:
      virtual ResultType executeImpl();
   };

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

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

بدلا من ذلك, أود إنشاء UndoRedoStack الفئة التي لديها execute_command(القيادة*الأمر) وظيفة ، وترك الأوامر بسيطة بقدر الإمكان.

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

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

class Command 
{
public:
   void operator()() 
   {
      do_command();
      add_to_undo_stack(this);
   }

   void undo()
   {
      // This might seem pointless now to just call do_undo but 
      // it could become beneficial later if you want to do some
      // error-checking, for instance, without having to do it
      // in every single command subclass's undo implementation.
      do_undo();
   }

private:
   virtual void do_command() = 0;
   virtual void do_undo() = 0;
};

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

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

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

class OperationState
{
protected:
    Operation& mParent;
    OperationState(Operation& parent);
public:
    virtual ~OperationState();
    Operation& getParent();
};

class Operation
{
private:
    const std::string mName;
public:
    Operation(const std::string& name);
    virtual ~Operation();

    const std::string& getName() const{return mName;}

    virtual OperationState* operator ()() = 0;

    virtual bool undo(OperationState* state) = 0;
    virtual bool redo(OperationState* state) = 0;
};

إنشاء وظيفة وستكون حالتها مثل:

class MoveState : public OperationState
{
public:
    struct ObjectPos
    {
        Object* object;
        Vector3 prevPosition;
    };
    MoveState(MoveOperation& parent):OperationState(parent){}
    typedef std::list<ObjectPos> PrevPositions;
    PrevPositions prevPositions;
};

class MoveOperation : public Operation
{
public:
    MoveOperation():Operation("Move"){}
    ~MoveOperation();

    // Implement the function and return the previous
    // previous states of the objects this function
    // changed.
    virtual OperationState* operator ()();

    // Implement the undo function
    virtual bool undo(OperationState* state);
    // Implement the redo function
    virtual bool redo(OperationState* state);
};

اعتاد أن يكون هناك فصل يسمى OperationManager. سجلت هذه الوظائف المختلفة وإنشاء مثيلات منها مثل:

OperationManager& opMgr = OperationManager::GetInstance();
opMgr.register<MoveOperation>();

كانت وظيفة التسجيل مثل:

template <typename T>
void OperationManager::register()
{
    T* op = new T();
    const std::string& op_name = op->getName();
    if(mOperations.count(op_name))
    {
        delete op;
    }else{
        mOperations[op_name] = op;
    }
}

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

void OperationManager::execute(const std::string& operation_name)
{
    if(mOperations.count(operation_name))
    {
        Operation& op = *mOperations[operation_name];
        OperationState* opState = op();
        if(opState)
        {
            mUndoStack.push(opState);
        }
    }
}

عندما يكون هناك ضرورة للتراجع ، فإنك تفعل ذلك من برنامج التشغيل مثل:
OperationManager::GetInstance().undo();
وتبدو وظيفة التراجع عن OperationManager مثل هذا:

void OperationManager::undo()
{
    if(!mUndoStack.empty())
    {
        OperationState* state = mUndoStack.pop();
        if(state->getParent().undo(state))
        {
            mRedoStack.push(state);
        }else{
            // Throw an exception or warn the user.
        }
    }
}

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

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