Командный дизайн

11

У меня есть эта старая реализация шаблона Command. Это своего рода передача контекста через всю реализацию DIOperation , но позже я понял, что в процессе обучения и обучения (что никогда не останавливается) это не оптимально. Я также думаю, что «посещение» здесь не совсем подходит и просто сбивает с толку.

Я на самом деле думаю о рефакторинге своего кода, в том числе потому, что команда не должна ничего знать о других, и в данный момент все они используют одни и те же пары ключ-значение. Действительно сложно определить, какому классу принадлежит какое значение ключа, что иногда приводит к дублированию переменных.

Пример варианта использования: допустим, для CommandB требуется UserName, который устанавливается CommandA . Должна ли 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 - это запутанная часть, которую я пытаюсь изменить. Я передаю его как посетитель, хотя на самом деле это контекст ...
Андреа Ричиарди
@ahenderson Вы имеете в виду события между моими командами, верно? Вы бы там поместили свои значения ключей (аналогично тому, что Android делает с Parcel)? Будет ли то же самое в том смысле, что CommandA должен создать событие с парами ключ-значение, которые принимает CommandB?
Андреа Ричиарди

Ответы:

2

Я немного обеспокоен изменчивостью параметров вашей команды. Действительно ли необходимо создать команду с постоянно меняющимися параметрами?

Проблемы с вашим подходом:

Хотите ли вы, чтобы другие потоки / команды меняли ваши параметры в процессе perform?

Вы хотите, чтобы один visitBeforeи visitAfterтот же Commandобъект вызывался с разными DIParameterобъектами?

Вы хотите, чтобы кто-то передавал параметры вашим командам, о которых команды не имеют представления?

Ничто из этого не запрещено вашим текущим дизайном. Хотя общая концепция параметра «ключ-значение» иногда имеет свои достоинства, она мне не нравится в отношении универсального класса команд.

Пример последствий:

Рассмотрим конкретную реализацию вашего Commandкласса - что-то вроде CreateUserCommand. Теперь, очевидно, когда вы запрашиваете создание нового пользователя, команде понадобится имя для этого пользователя. Учитывая, что я знаю классы CreateUserCommandи DIParametersклассы, какой параметр я должен установить?

Я мог бы установить userNameпараметр, или username.. вы относитесь к параметрам без учета регистра? Я не знаю, на самом деле .. о, подождите .. может быть, это просто name?

Как вы видите, свобода, которую вы получаете от общего сопоставления ключ-значение, подразумевает, что использование ваших классов в качестве того, кто их не реализовал, неоправданно сложно. По крайней мере, вам нужно предоставить некоторые константы для ваших команд, чтобы другие знали, какие ключи поддерживаются этой командой.

Возможны разные конструкции подходов:

  • Неизменяемые параметры: превращая свои Parameterэкземпляры в неизменяемые, вы можете свободно использовать их в различных командах.
  • Определенные классы параметров. Учитывая UserParameterкласс, который содержит именно те параметры, которые мне понадобятся для команд, в которых участвует пользователь, было бы намного проще работать с этим API. Вы по-прежнему можете наследовать параметры, но для командных классов больше не будет смысла принимать произвольные параметры - конечно, для профессионалов это означает, что пользователи API точно знают, какие параметры необходимы.
  • Один экземпляр команды для каждого контекста: если вам нужно, чтобы у ваших команд были такие вещи visitBeforeи visitAfter, хотя они также использовались с разными параметрами, вы столкнетесь с проблемой вызова с разными параметрами. Если параметры должны быть одинаковыми при нескольких вызовах методов, вам необходимо включить их в команду, чтобы они не могли быть переключены для других параметров между вызовами.
Фрэнк
источник
Да, я избавился от визита До и после. Я в основном передаю свой интерфейс DIParameter в методе execute. Проблема с нежелательными экземплярами DIParamters всегда будет существовать, потому что я выбрал гибкость передачи интерфейса. Мне очень нравится идея иметь возможность создавать подклассы и делать детей DIParameters неизменяемыми после заполнения. Однако «центральный орган» все еще должен передать правильный параметр DIP в Команду. Вероятно, поэтому я начал реализовывать шаблон Visitor .. Я хотел каким-то образом
изменить
0

Что хорошо в принципах дизайна, так это то, что рано или поздно они конфликтуют друг с другом.

В описанной ситуации, я думаю, я бы предпочел использовать некоторый контекст, из которого каждая команда может получать информацию и помещать информацию (особенно, если это пары ключ-значение). Это основано на компромиссе: я не хочу, чтобы отдельные команды были связаны просто потому, что они являются своего рода вводом друг для друга. Внутри CommandB мне все равно, как было задано имя пользователя - только то, что оно мне нужно использовать. То же самое в CommandA: я установил информацию, я не хочу знать, что с ней делают другие - ни кто они.

Это означает некий проходной контекст, который вы можете найти плохим. Для меня альтернатива хуже, особенно если этот простой контекст значения ключа (может быть простым компонентом с геттерами и сеттерами, чтобы немного ограничить коэффициент «свободной формы») может позволить решению быть простым и тестируемым, с хорошим отдельные команды, каждая со своей бизнес-логикой.

Мартин
источник
1
Какие принципы вы находите здесь противоречивыми?
Джимми Хоффа
Просто чтобы прояснить, моя проблема не в выборе между контекстом или шаблоном посетителя. Я в основном использую шаблон контекста под названием Visitor :)
Andrea Richiardi
Хорошо, я, вероятно, неправильно понял ваш точный вопрос / проблему.
Мартин
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>

ahenderson
источник
Спасибо за ответ. Проблема с этим дизайном imho заключается в том, что нам нужен Setter + NameObserver для каждого параметра. Я мог бы передать экземпляр DIParameters (context) и уведомить, но, опять же, я, вероятно, не решу тот факт, что я все еще связываю CommandA с CommandB, то есть CommandA должен ввести значение ключа, о котором должна знать только CommandB ... я также попытался создать внешнюю сущность (ParameterHandler), которая будет единственной, кто знает, какой команде нужен какой параметр и соответственно устанавливает / получает в экземпляре DIParameters.
Андреа Ричиарди
@Kap «Проблема с этим дизайном imho в том, что нам нужен Setter + NameObserver для каждого параметра» - параметр в этом контексте немного сбивает меня с толку, я думаю, вы имели в виду поле. В этом случае у вас уже должен быть сеттер для каждого изменяемого поля. Из вашего примера кажется, что ComamndA действительно меняет имя субъекта. Это должно изменить поле через сеттер. Примечание: вам не нужен наблюдатель на поле, просто получите геттер и передайте объект всем наблюдателям.
ahenderson
0

Я не знаю, является ли это правильным способом справиться с этим на программистах (в этом случае я прошу прощения), но, проверив все ответы здесь (в частности, @ Frank's). Я реорганизовал свой код следующим образом:

  • Сбросил DIPпараметры. У меня будут отдельные (общие) объекты в качестве входных данных DIOperation (неизменяемые). Пример:
class 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> 
class DIOperation {
общественности:
    virtual int execute () = 0;

    виртуальный T getResult () = 0;

    виртуальный ~ DIOperation () = 0;
};

класс CreateRelation: public DIOperation <RelatedObjectTriplet> {
частный:
    статический std :: string const TYPE;

    // Params (неизменяемый)
    RelatedObjectTriplet const m_sParams;

    // скрыто
    CreateRelation & operator = (CreateRelation const & source);
    CreateRelation (CreateRelation const & source);

    // Внутренний
    std :: string m_sNewRelationId;

общественности:
    CreateRelation (RelatedObjectTriplet const & params);

    int execute ();

    RelatedObjectTriplet getResult ();

    ~ CreateRelation ();
};
  • Это можно использовать так:
RelatedObjectTriplet triplet («33333», «55555», «77777»);
CreateRelation createRel (триплет);
createRel.perform ();
const RelatedObjectTriplet res = createRel.getResult ();

Спасибо за помощь, и я надеюсь, что я не ошибся здесь :)

Андреа Ричиарди
источник