命令模式设计


11

我有Command模式的旧实现。这是通过所有DIOperation实现传递一个Context的方法,但是后来我意识到,在学习过程中(永远不会停止),这并不是最佳的。我也认为这里的“访问”并不适合,只会造成混乱。

我实际上是在考虑重构我的代码,这也是因为一个Command对其他命令一无所知,并且现在它们都共享相同的键值对。很难维护哪个类拥有哪个键值,有时会导致变量重复。

用例示例:假设CommandB需要由CommandA设置的UserName。CommandA是否应该设置键UserNameForCommandB = John?还是应该共享一个公共的UserName = John键值?如果用户名被第三个命令使用怎么办?

如何改善设计?谢谢!

class DIParameters {
public:
   /**
    * Parameter setter.
    */
    virtual void setParameter(std::string key, std::string value) = 0;
    /**
    * Parameter getter.
    */
    virtual std::string getParameter(std::string key) const = 0;

    virtual ~DIParameters() = 0;
};

class DIOperation {
public:
    /**
     * Visit before performing execution.
     */
    virtual void visitBefore(DIParameters& visitee) = 0;
    /**
     * Perform.
     */
    virtual int perform() = 0;
    /**
     * Visit after performing execution.
     */
    virtual void visitAfter(DIParameters& visitee) = 0;

    virtual ~DIOperation() = 0;
};

3
我从未有过使用命令设置属性(如名称)的运气。它开始变得非常依赖。如果您的设置属性尝试使用事件体系结构或观察者模式。
ahenderson

1
1.为什么要通过单独的访问者传递参数?将上下文作为执行参数传递有什么问题?2.上下文用于命令的“公共”部分(例如,当前会话/文档)。所有特定于操作的参数最好通过操作的构造函数传递。
克里斯·范·贝尔

@KrisVanBael是我要更改的令人困惑的部分。我将其作为访问者传递给了它,而实际上却是一个上下文...
Andrea Richiardi

@ahenderson您的意思是我的命令之间的事件对吗?您是否会在其中放置键值(类似于Android对Parcel所做的操作)?从CommandA应该使用CommandB接受的键值对构建Event的意义上说是一样的吗?
Andrea Richiardi

Answers:


2

我有点担心您的命令参数的可变性。创建参数不断变化的命令真的有必要吗?

您的方法存在问题:

您是否希望其他线程/命令在执行过程中更改参数perform

您是否希望visitBeforevisitAfter同一Command对象一起被不同的DIParameter对象调用?

您是否想要某人向您的命令提供参数,而这些命令却一无所知?

您当前的设计均不禁止这些操作。尽管通用键值参数概念有时会有其优点,但对于通用命令类,我并不喜欢它。

后果示例:

考虑一下您的Command班级的具体实现-例如CreateUserCommand。现在显然,当您请求创建一个新用户时,该命令将需要该用户的名称。鉴于我知道CreateUserCommandDIParameters类,应该设置哪个参数?

我可以设置userName参数,或者username..您是否不区分大小写地对待参数?我真的不知道..哦,等等..也许只是name

如您所见,您从通用键值映射中获得的自由度意味着将类用作未实现类的人是不必要的困难。您至少需要为命令提供一些常量,以使其他人知道该命令支持哪些键。

可能的不同设计方法:

  • 不可变的参数:通过将Parameter实例变为不可变,您可以在不同的命令之间自由地重用它们。
  • 特定的参数类:给定一个UserParameter类,其中包含的参数恰好包含涉及用户的命令所需要的参数,因此使用此API会容易得多。您仍然可以继承参数,但是命令类采用任意参数不再有意义-当然,从专业方面来说,这意味着API用户知道确切需要哪些参数。
  • 每个上下文有一个命令实例:如果您需要使命令具有visitBefore和之类的东西visitAfter,同时又要用不同的参数来重用它们,那么您将面临用不同的参数调用的问题。如果在多个方法调用中参数应该相同,则需要将它们封装到命令中,以使在调用之间不能将它们用于其他参数。

是的,我摆脱了visitBefore和VisitAfter。我基本上是在perform方法中传递我的DIParameter接口。不需要的DIParamters实例的问题总是存在,因为我选择了灵活地传递接口。我真的很喜欢这样的想法:一旦DIParameters子级被填充,它们就可以成为子类并使其不可变。但是,“中央机构”仍然需要将正确的DIParameter传递给命令。这可能就是为什么我开始实现“访客”模式的原因
。.

0

设计原则的优点在于,它们迟早会相互冲突。

在描述的情况下,我认为我更喜欢使用某种上下文,每个命令都可以从中获取信息并将信息放入(特别是如果这些是键值对)。这是基于权衡取舍的:我不希望仅因为它们是彼此的某种输入而耦合单独的命令。在CommandB内部,我不在乎如何设置用户名-只是我可以使用它。CommandA中的同一件事:我将信息放入其中,我不想知道其他人正在使用它-他们都不是谁。

这意味着某种传递的上下文,您可能会发现它很糟糕。对我来说,替代方案更糟,尤其是如果这种简单的键值上下文(可以是带有getter和setter的简单bean,以限制“自由格式”因素)可以使解决方案变得简单且可测试的话,分开的命令,每个命令都有自己的业务逻辑。


1
您在这里发现哪些原则冲突?
Jimmy Hoffa 2013年

只是为了澄清,我的问题不是在上下文模式或访客模式之间进行选择。我基本上使用的是称为“访客:)的上下文模式
Andrea Richiardi

好的,我可能误解了您的确切问题。
2013年

0

假设您有一个命令界面:

class Command {
public:
    void execute() = 0;
};

和一个主题:

class Subject {
    std::string name;
public:
    void setName(const std::string& name) {this->name = name;}
}

您需要的是:

class NameObserver {
public:
    void update(const std::string& name) = 0;
};

class Subject {
    NameObserver& o;
    std::string name;
private:
    void setName(const std::string& name) {
        this->name = name;
        o.update(name);
    }
};

class CommandB: public Command, public NameObserver {
    std::string name;
public:
    void execute();
    void update(const std::string& name) {
        this->name = name;
        execute();
    }
};

设置NameObserver& o为对CommandB的引用。现在,只要CommandA更改了主体名称,CommandB都可以使用正确的信息执行。如果更多命令使用名称,请使用std::list<NameObserver>


感谢你的回答。这种设计恕我直言的问题在于,每个参数都需要一个Setter + NameObserver。我可以传递一个DIParameters(上下文)实例并进行通知,但是,同样,我可能无法解决我仍然将CommandA与CommandB耦合的事实,这意味着CommandA必须放置一个只有CommandB应该知道的键值。我尝试过的还有一个外部实体(ParameterHandler),它是唯一一个知道哪个Command需要哪个参数并在DIParameters实例上相应地设置/获取的实体。
安德烈Richiardi

@Kap“此设计恕我直言的问题是,每个参数我们都需要一个Setter + NameObserver”-在这种情况下,参数对我来说有点令人困惑,我认为您的意思是字段。在这种情况下,您应该已经为每个更改的字段设置了一个setter。从您的示例看来,ComamndA确实更改了主题的名称。它应该通过设置器更改字段。注意:您不需要每个字段都有观察者,只需拥有一个吸气剂并将对象传递给所有观察者即可。
ahenderson

0

我不知道这是否是对程序员进行处理的正确方法(在这种情况下,我表示歉意),但是在检查了所有答案之后(尤其是@Frank)。我以这种方式重构了代码:

  • 删除的DIParameter。我将使用单个(通用)对象作为DIOperation的输入(不可变)。例:
类RelatedObjectTriplet {
私人的:
    std :: string const m_sPrimaryObjectId;
    std :: string const m_sSecondaryObjectId;
    std :: string const m_sRelationObjectId;

    RelatedObjectTriplet&operator =(RelatedObjectTriplet other);

上市:
    RelatedObjectTriplet(std :: string const&sPrimaryObjectId,
                         std :: string const&sSecondaryObjectId,
                         std :: string const&sRelationObjectId);

    RelatedObjectTriplet(RelatedObjectTriplet const&other);


    std :: string const&getPrimaryObjectId()const;
    std :: string const&getSecondaryObjectId()const;
    std :: string const&getRelationObjectId()const;

    〜RelatedObjectTriplet();
};
  • 新的DIOperation类(带有示例)定义为:
模板<class T = void> 
DIOperation类{
上市:
    virtual int perform()= 0;

    虚拟T getResult()= 0;

    虚拟〜DIOperation()= 0;
};

类CreateRelation:public DIOperation <RelatedObjectTriplet> {
私人的:
    静态std :: string const TYPE;

    //参数(不可变)
    RelatedObjectTriplet const m_sParams;

    //隐藏
    CreateRelation&operator =(CreateRelation const&源);
    CreateRelation(CreateRelation const&来源);

    //内部
    std :: string m_sNewRelationId;

上市:
    CreateRelation(RelatedObjectTriplet const&params);

    int perform();

    RelatedObjectTriplet getResult();

    〜CreateRelation();
};
  • 可以这样使用:
RelatedObjectTriplet三元组(“ 33333”,“ 55555”,“ 77777”);
CreateRelation createRel(triplet);
createRel.perform();
const RelatedObjectTriplet res = createRel.getResult();

感谢您的帮助,我希望我在这里没有犯错:)

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.