Projekt wzorca poleceń

11

Mam tę starą implementację wzorca poleceń. To rodzaj przekazywania kontekstu przez całą implementację DIOperation , ale później uświadomiłem sobie, że proces uczenia się i uczenia się (który nigdy się nie kończy) nie jest optymalny. Myślę też, że „odwiedzanie” tutaj nie pasuje i po prostu myli.

Właściwie myślę o refaktoryzacji mojego kodu, również dlatego, że polecenie nie powinno nic wiedzieć o innych, a obecnie wszystkie mają te same pary klucz-wartość. Naprawdę trudno jest utrzymać, która klasa jest właścicielem której klucz-wartość, co czasami prowadzi do zduplikowania zmiennych.

Przykładem przypadku użycia: powiedzmy CommandB wymaga nazwa_użytkownika , która jest tworzona przez CommandA . Czy CommandA powinien ustawić klucz UserNameForCommandB = John ? A może powinny dzielić wspólną wartość UserName = John klucz-wartość? Co jeśli nazwa użytkownika jest używana przez trzecie polecenie?

Jak mogę ulepszyć ten projekt? Dzięki!

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;
};
Andrea Richiardi
źródło
3
Nigdy nie miałem szczęścia, używając komendy do ustawiania właściwości (takich jak nazwa). Zaczyna być bardzo zależny. Jeśli właściwości ustawień spróbuj użyć architektury zdarzeń lub wzorca obserwatora.
ahenderson
1
1. Po co przekazywać parametry osobnemu gościowi? Co jest złego w przekazywaniu kontekstu jako argumentu wykonania? 2. Kontekst dotyczy „wspólnej” części polecenia (np. Bieżącej sesji / dokumentu). Wszystkie parametry specyficzne dla operacji są lepiej przekazywane przez konstruktora operacji.
Kris Van Bael
@KrisVanBael to myląca część, którą próbuję zmienić. Podaję go jako gościa, podczas gdy w rzeczywistości jest kontekstem ...
Andrea Richiardi
@ ahenderson Czy masz na myśli wydarzenia między moimi poleceniami, prawda? Czy umieściłbyś tam swoje kluczowe wartości (podobne do tego, co Android robi z Parcel)? Czy byłoby tak samo w tym sensie, że CommandA powinien zbudować Zdarzenie z parami klucz-wartość, które CommandB akceptuje?
Andrea Richiardi

Odpowiedzi:

2

Martwię się trochę o zmienność parametrów poleceń. Czy naprawdę konieczne jest utworzenie polecenia z ciągle zmieniającymi się parametrami?

Problemy z twoim podejściem:

Czy chcesz, aby inne wątki / polecenia zmieniały parametry w trakcie performdziałania?

Chcesz visitBeforei visitAftertego samego Commandobiektu na miano z różnymi DIParameterprzedmiotami?

Czy chcesz, aby ktoś podawał parametry do twoich poleceń, o których polecenia nie mają pojęcia?

Nic nie jest zabronione przez twój obecny projekt. Chociaż ogólna koncepcja parametru klucz-wartość ma swoje zalety, nie podoba mi się to w odniesieniu do ogólnej klasy poleceń.

Przykład konsekwencji:

Rozważ konkretną realizację swojej Commandklasy - coś w rodzaju CreateUserCommand. Oczywiście, kiedy poprosisz o utworzenie nowego użytkownika, polecenie będzie wymagało nazwy tego użytkownika. Biorąc pod uwagę, że znam CreateUserCommandi DIParametersklasy, który parametr powinienem ustawić?

Mógłbym ustawić userNameparametr lub username... czy traktujesz wielkość liter w sposób niewrażliwy? Nie wiedziałbym tak naprawdę… och, czekaj… może to po prostu name?

Jak widać, swobodę, którą zyskujesz dzięki ogólnemu mapowaniu klucz-wartość, oznacza, że ​​używanie twoich klas jako kogoś, kto ich nie zaimplementował, jest nieuzasadnione trudne. Musisz przynajmniej podać stałe dla swoich poleceń, aby inni wiedzieli, które klucze są obsługiwane przez to polecenie.

Możliwe różne podejścia do projektu:

  • Niezmienne parametry: zmieniając Parameterinstancje na niezmienne, możesz swobodnie wykorzystywać je między różnymi poleceniami.
  • Specyficzne klasy parametrów: Biorąc pod uwagę UserParameterklasę, która zawiera dokładnie parametry, których potrzebowałbym dla poleceń angażujących użytkownika, znacznie łatwiej byłoby pracować z tym interfejsem API. Nadal możesz mieć dziedziczenie parametrów, ale nie ma sensu, aby klasy poleceń przyjmowały dowolne parametry - po stronie pro oznacza to oczywiście, że użytkownicy interfejsu API wiedzą, które parametry są dokładnie wymagane.
  • Jedna instancja komenda za kontekstu: Jeśli chcesz mieć swoje polecenia do rzeczy, jak visitBeforei visitAfter, jednocześnie ponowne ich z różnymi parametrami, będzie otwarty na problem coraz wywołana z różnych parametrów. Jeśli parametry powinny być takie same w przypadku wielu wywołań metod, należy umieścić je w komendzie, aby nie można było ich zmienić na inne parametry między wywołaniami.
Szczery
źródło
Tak, pozbyłem się wizyta przed i wizyta później. Zasadniczo przekazuję mój interfejs DIParameter w metodzie perform. Problem z niepożądanymi instancjami DIParamters zawsze będzie istniał, ponieważ zdecydowałem się na elastyczność w przekazywaniu interfejsu. Bardzo podoba mi się pomysł, aby móc podzielić dzieci na klasy i uczynić dzieci DIParameters niezmiennymi po ich wypełnieniu. Jednak „organ centralny” nadal musi przekazać poprawny parametr DIP do polecenia. Prawdopodobnie dlatego zacząłem wdrażać wzorzec Odwiedzającego. Chciałem w jakiś sposób mieć odwrócenie kontroli ...
Andrea Richiardi
0

Zaletą zasad projektowania jest to, że prędzej czy później powodują one konflikt między sobą.

W opisanej sytuacji myślę, że wolałbym zastosować pewien kontekst, w którym każde polecenie może pobierać informacje i umieszczać informacje (szczególnie jeśli są to pary klucz-wartość). Jest to oparte na kompromisie: nie chcę, aby osobne polecenia były łączone tylko dlatego, że są one pewnego rodzaju danymi wejściowymi do siebie. W CommandB nie obchodzi mnie, jak ustawiono UserName - wystarczy, że mogę z niego korzystać. To samo w CommandA: wprowadzam informacje, nie chcę wiedzieć, co inni z tym robią - ani kim są.

Oznacza to rodzaj przejściowego kontekstu, który można znaleźć źle. Dla mnie alternatywa jest gorsza, zwłaszcza jeśli ten prosty kontekst klucz-wartość (może być zwykłą fasolą z modułami pobierającymi i ustawiającymi, aby nieco ograniczyć współczynnik „swobodnej postaci”), może pozwolić, aby rozwiązanie było proste i testowalne, z dobrze oddzielne polecenia, każde z własną logiką biznesową.

Jaskółka oknówka
źródło
1
Które zasady są tutaj sprzeczne?
Jimmy Hoffa
Żeby wyjaśnić, moim problemem nie jest wybór pomiędzy Kontekstem a Wzorem Odwiedzającego. Używam w zasadzie wzorca kontekstowego o nazwie Visitor :)
Andrea Richiardi
Ok, prawdopodobnie źle zrozumiałem twoje pytanie / problem.
Martin
0

Załóżmy, że masz interfejs poleceń:

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

I temat:

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

Potrzebujesz:

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();
    }
};

Ustaw NameObserver& ojako odniesienie do CommandB. Teraz za każdym razem, gdy CommandA zmienia nazwę podmiotów CommandB może wykonać z poprawnymi informacjami. Jeśli nazwa jest używana przez więcej poleceń, użyj astd::list<NameObserver>

ahenderson
źródło
Dziękuję za odpowiedź. Problem z tym imho projektu polega na tym, że potrzebujemy Setter + NameObserver dla każdego parametru. Mógłbym przekazać instancję DIParameters (kontekst) i powiadomić, ale ponownie prawdopodobnie nie rozwiążę faktu, że wciąż łączę CommandA z CommandB, co oznacza, że ​​CommandA musi podać wartość klucza, o której tylko CommandB powinien wiedzieć ... to, co próbowałem, to także mieć zewnętrzny byt (ParameterHandler), który jako jedyny wie, które polecenie potrzebuje którego parametru i odpowiednio ustawia / pobiera w instancji DIParameters.
Andrea Richiardi
@Kap „Problem z tym imho projektu polega na tym, że potrzebujemy Setter + NameObserver na każdy parametr” - parametr w tym kontekście jest dla mnie trochę mylący, myślę, że miałeś na myśli pole. W takim przypadku powinieneś już mieć program ustawiający dla każdego zmieniającego się pola. Z twojego przykładu wydaje się, że ComamndA zmienia nazwę podmiotu. Powinien zmienić pole za pomocą setera. Uwaga: nie potrzebujesz obserwatora na pole, po prostu miej getter i przekaż obiekt wszystkim obserwatorom.
ahenderson
0

Nie wiem, czy jest to właściwy sposób, aby poradzić sobie z tym na programistach (w takim przypadku przepraszam), ale po sprawdzeniu wszystkich odpowiedzi tutaj (w szczególności @ Frank). Kod zreorganizowałem w ten sposób:

  • Upuszczono DIParameters. Będę miał pojedyncze (ogólne) obiekty jako dane wejściowe DIOperation (niezmienne). Przykład:
klasa RelatedObjectTriplet {
prywatny:
    std :: string const m_sPrimaryObjectId;
    std :: string const m_sSecondaryObjectId;
    std :: string const m_sRelationObjectId;

    RelatedObjectTriplet & operator = (RelatedObjectTriplet inne);

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

    RelatedObjectTriplet (RelatedObjectTriplet const i inne);


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

    ~ RelatedObjectTriplet ();
};
  • Nowa klasa DIOperation (z przykładem) zdefiniowana jako:
szablon <klasa T = nieważne> 
klasa DIOperation {
publiczny:
    virtual int perform () = 0;

    virtual T getResult () = 0;

    virtual ~ DIOperation () = 0;
};

klasa CreateRelation: public DIOperation <RelatedObjectTriplet> {
prywatny:
    static std :: string const TYPE;

    // Params (niezmienny)
    RelatedObjectTriplet const m_sParams;

    // Ukryty
    CreateRelation & operator = (CreateRelation const & source);
    CreateRelation (CreateRelation const i source);

    // Wewnętrzne
    std :: string m_sNewRelationId;

publiczny:
    CreateRelation (RelatedObjectTriplet const i params);

    int perform ();

    RelatedObjectTriplet getResult ();

    ~ CreateRelation ();
};
  • Można go użyć w następujący sposób:
Pokrewny tryplet tripletu („33333”, „55555”, „77777”);
CreateRelation createRel (triplet);
createRel.perform ();
const RelatedObjectTriplet res = createRel.getResult ();

Dzięki za pomoc i mam nadzieję, że nie popełniłem tutaj błędów :)

Andrea Richiardi
źródło