Projektowanie interfejsu, w którym funkcje muszą być wywoływane w określonej kolejności

24

Zadanie polega na skonfigurowaniu sprzętu w urządzeniu zgodnie z niektórymi specyfikacjami wejściowymi. Należy to osiągnąć w następujący sposób:

1) Zbierz informacje o konfiguracji. Może się to zdarzyć w różnych momentach i miejscach. Na przykład zarówno moduł A, jak i moduł B mogą żądać (w różnym czasie) niektórych zasobów od mojego modułu. Te „zasoby” są w rzeczywistości tym, czym jest konfiguracja.

2) Po tym, jak jasne jest, że nie będzie już realizowanych żądań, do sprzętu należy wysłać polecenie startowe, zawierające podsumowanie żądanych zasobów.

3) Dopiero potem można (i trzeba) dokonać szczegółowej konfiguracji wspomnianych zasobów.

4) Również tylko po 2) można (i trzeba) przekierować wybrane zasoby do zadeklarowanych rozmówców.


Częstą przyczyną błędów, nawet dla mnie, który to napisał, jest pomyłka w tej kolejności. Jakie konwencje nazewnictwa, projekty lub mechanizmy mogę zastosować, aby interfejs mógł być używany przez osobę, która zobaczy kod po raz pierwszy?

Vorac
źródło
Etap 1 lepiej nazwać discoverylub handshake?
rwong
1
Sprzężenie czasowe jest anty-wzorem i należy go unikać.
1
Tytuł pytania sprawia, że ​​myślę, że możesz zainteresować się wzorem konstruktora kroków .
Joshua Taylor

Odpowiedzi:

45

To przeprojektowanie, ale możesz zapobiec niewłaściwemu użyciu wielu interfejsów API, ale nie dysponując żadną metodą, której nie należy wywoływać.

Na przykład zamiast first you init, then you start, then you stop

Twój konstruktor initjest obiektem, który można uruchomić i starttworzy sesję, którą można zatrzymać.

Oczywiście, jeśli masz ograniczenie do jednej sesji na raz, musisz poradzić sobie ze sprawą, w której ktoś próbuje utworzyć taką z już aktywną.

Teraz zastosuj tę technikę do własnego przypadku.

Dojną krową
źródło
zlibi jpeglibsą dwoma przykładami, które są zgodne z tym wzorem inicjalizacji. Mimo to wiele dokumentów jest potrzebnych, aby nauczyć programistów tej koncepcji.
rwong
5
To jest właściwa odpowiedź: jeśli kolejność ma znaczenie, każda funkcja zwraca wynik, który można następnie wywołać w celu wykonania następnego kroku. Sam kompilator jest w stanie wymusić ograniczenia projektowe.
2
Jest to podobne do wzorca konstruktora kroków ; prezentuj tylko interfejs, który ma sens na danym etapie.
Joshua Taylor
@JoshuaTaylor moja odpowiedź to implementacja wzorca konstruktora kroków :)
Silviu Burcea
@SilviuBurcea Twoja odpowiedź nie jest implementacją kreatora kroków, ale skomentuję ją bardziej niż tutaj.
Joshua Taylor
19

Możesz mieć metodę uruchamiania zwracającą obiekt, który jest wymaganym parametrem do konfiguracji:

Zasób * MyModule :: GetResource ();
MySession * MyModule :: Startup ();
void Resource :: Configure (sesja MySession *);

Nawet jeśli twoja MySessionjest tylko pustą strukturą, wymusi to bezpieczeństwo typu, że żadna Configure()metoda nie może zostać wywołana przed uruchomieniem.

jpa
źródło
Co powstrzymuje kogoś przed zrobieniem module->GetResource()->Configure(nullptr)?
svick
@svick: Nic, ale musisz to zrobić jawnie. To podejście mówi ci, czego się spodziewa, a pominięcie tego oczekiwania jest świadomą decyzją. Podobnie jak w przypadku większości języków programowania, nikt nie stoi na przeszkodzie, abyś postrzelił się w stopę. Ale API zawsze dobrze wskazuje, że to robisz;)
Michael Klement
+1 wygląda świetnie i prosto. Widzę jednak problem. Jeśli mam obiekty a, b, c, d, mogę zacząć a, a następnie użyć go, MySessionaby użyć go bjako obiektu już uruchomionego, podczas gdy w rzeczywistości tak nie jest.
Vorac
8

Opierając się na odpowiedzi Cashcova - dlaczego musisz przedstawić dzwoniącemu nowy obiekt, skoro możesz po prostu przedstawić nowy interfejs? Wzór Rebrand:

class IStartable     { public: virtual IRunnable      start()     = 0; };
class IRunnable      { public: virtual ITerminateable run()       = 0; };
class ITerminateable { public: virtual void           terminate() = 0; };

Możesz także pozwolić ITerminated na implementację IRunnable, jeśli sesję można uruchomić wiele razy.

Twój obiekt:

class Service : IStartable, IRunnable, ITerminateable
{
  public:
    IRunnable      start()     { ...; return this; }
    ITerminateable run()       { ...; return this; }
    void           terminate() { ...; }
}

// And use it like this:
IStartable myService = Service();

// Now you can only call start() via the interface
IRunnable configuredService = myService.start();

// Now you can also call run(), because it is wrapped in the new interface...

W ten sposób możesz wywoływać tylko odpowiednie metody, ponieważ na początku masz tylko interfejs IStartable, a metoda run () będzie dostępna tylko po wywołaniu start (); Z zewnątrz wygląda jak wzór z wieloma klasami i obiektami, ale klasa podstawowa pozostaje jedną klasą, do której zawsze się odwołuje.

Falco
źródło
1
Jaka jest zaleta posiadania tylko jednej klasy podstawowej zamiast kilku? Ponieważ jest to jedyna różnica w stosunku do zaproponowanego przeze mnie rozwiązania, byłbym zainteresowany tą szczególną kwestią.
Michael Le Barbier Grünewald
1
@ MichaelGrünewald Nie jest konieczne implementowanie wszystkich interfejsów za pomocą jednej klasy, ale w przypadku obiektu typu konfiguracyjnego może być najprostszą techniką implementacji udostępnianie danych między instancjami interfejsów (tj. Ponieważ jest to wspólne z racji bycia tym samym obiekt).
Joshua Taylor
1
Jest to zasadniczo wzorzec konstruktora kroków .
Joshua Taylor
@JoshuaTaylor Udostępnianie danych między instancjami interfejsu jest dwojakie: chociaż może być łatwiejsze do wdrożenia, musimy uważać, aby nie uzyskać dostępu do „niezdefiniowanego stanu” (np. Dostępu do adresu klienta niepołączonego serwera). Ponieważ OP kładzie nacisk na użyteczność interfejsu, możemy ocenić oba podejścia jako równe. Dziękujemy za zacytowanie BTW „wzorca budowniczego kroków”.
Michael Le Barbier Grünewald
1
@ MichaelGrünewald Jeśli wchodzisz w interakcję z obiektem tylko za pośrednictwem określonego interfejsu określonego w danym punkcie, nie powinno być żadnego sposobu (bez rzutowania itp.) Dostępu do tego stanu.
Joshua Taylor
2

Istnieje wiele prawidłowych sposobów rozwiązania problemu. Basile Starynkevitch zaproponował podejście „zero biurokracji”, które pozostawia prosty interfejs i polega na programiście używającym odpowiednio interfejsu. Chociaż podoba mi się to podejście, przedstawię inne, które ma więcej eingineeringu, ale pozwala kompilatorowi wykryć pewne błędy.

  1. Określ, w jakich stanach może znajdować się Twoje urządzenie, as Uninitialised , Started, Configuredi tak dalej. Lista musi być skończona.¹

  2. Dla każdego stanu określ structgospodarstwo niezbędne dodatkowe informacje istotne dla tego stanu, npDeviceUninitialised , DeviceStartedi tak dalej.

  3. Zapakuj wszystkie zabiegi w jeden przedmiot DeviceStrategy którym metody wykorzystują struktury zdefiniowane w 2. jako dane wejściowe i wyjściowe. W związku z tym możesz mieć DeviceStarted DeviceStrategy::start (DeviceUninitalised dev)metodę (lub dowolną równoważną, zgodną z konwencjami projektu).

Przy takim podejściu prawidłowy program musi wywoływać niektóre metody w sekwencji wymuszonej przez prototypy metody.

Różne stany są niepowiązanymi obiektami, wynika to z zasady substytucji. Jeśli użyteczne jest, aby struktury te miały wspólnego przodka, przypomnij sobie, że wzorzec odwiedzających może być użyty do odzyskania konkretnego typu wystąpienia klasy abstrakcyjnej.

Chociaż opisałem w 3. wyjątkową DeviceStrategyklasę, zdarzają się sytuacje, w których możesz chcieć podzielić jej funkcjonalność na kilka klas.

Podsumowując je, kluczowe punkty opisanego przeze mnie projektu to:

  1. Ze względu na zasadę podstawiania obiekty reprezentujące stany urządzeń powinny być wyraźne i nie mogą mieć specjalnych relacji dziedziczenia.

  2. Pakuj zabiegi urządzeń w obiekty startowe, a nie w obiekty reprezentujące same urządzenia, tak aby każde urządzenie lub stan urządzenia widział tylko siebie, a strategia widziała je wszystkie i wyrażał możliwe przejścia między nimi.

Przysięgałbym, że widziałem kiedyś opis implementacji klienta Telnet zgodnie z tymi wierszami, ale nie byłem w stanie go znaleźć ponownie. Byłoby to bardzo przydatne odniesienie!

¹: W tym celu postępuj zgodnie z intuicją lub znajdź klasy równoważności metod w swojej rzeczywistej implementacji dla relacji „method₁ ~ method₂ iff. ważne jest, aby używać ich na tym samym obiekcie ”- zakładając, że masz duży obiekt otaczający wszystkie zabiegi na twoim urządzeniu. Obie metody wyświetlania stanów dają fantastyczne wyniki.

Michael Le Barbier Grünewald
źródło
1
Zamiast definiować osobne struktury, może być wystarczające zdefiniowanie niezbędnych interfejsów, które powinien posiadać obiekt w każdej fazie. Następnie jest to wzorzec konstruktora kroków .
Joshua Taylor
2

Użyj wzorca konstruktora.

Mieć obiekt, który ma metody dla wszystkich operacji wymienionych powyżej. Jednak nie wykonuje tych operacji od razu. Po prostu zapamiętuje każdą operację na później. Ponieważ operacje nie są wykonywane od razu, kolejność ich przekazywania do konstruktora nie ma znaczenia.

Po zdefiniowaniu wszystkich operacji w executekreatorze wywołujesz metodę . Po wywołaniu tej metody wykonuje ona wszystkie wymienione powyżej czynności we właściwej kolejności z operacjami zapisanymi powyżej. Ta metoda jest również dobrym miejscem do przeprowadzenia kontroli poprawności obejmującej całą operację (takich jak próba skonfigurowania zasobu, który nie został jeszcze skonfigurowany) przed zapisaniem ich na sprzęcie. Może to uchronić Cię przed uszkodzeniem sprzętu przez bezsensowną konfigurację (w przypadku, gdy twój sprzęt jest na to podatny).

Philipp
źródło
1

Musisz tylko udokumentować poprawnie sposób użycia interfejsu i podać przykładowy samouczek.

Możesz również mieć wariant biblioteki debugowania, który sprawdza niektóre środowiska wykonawcze.

Może definiowania i prawidłowo dokumentowania pewnych konwencji nazewnictwa (np preconfigure*, startup*, postconfigure*,run* ....)

BTW, wiele istniejących interfejsów ma podobny wzór (np. Zestawy narzędzi X11).

Basile Starynkevitch
źródło
Schemat przejścia stanu, podobny do cyklu życia aplikacji Android , może być konieczny do przekazania informacji.
rwong
1

Jest to rzeczywiście powszechny i ​​podstępny rodzaj błędu, ponieważ kompilatory mogą tylko wymuszać warunki składniowe, podczas gdy programy klienckie wymagają poprawności gramatycznej.

Niestety konwencje nazewnictwa są prawie całkowicie nieskuteczne w przypadku tego rodzaju błędów. Jeśli naprawdę chcesz zachęcić ludzi, aby nie robili niegramatycznych rzeczy, powinieneś przekazać obiekt polecenia pewnego rodzaju, który musi zostać zainicjowany wartościami warunków wstępnych, aby nie mogli wykonać kroków poza kolejnością.

Kilian Foth
źródło
Czy masz na myśli coś jak to ?
Vorac,
1
public class Executor {

private Executor() {} // helper class

  public void execute(MyStepsRunnable r) {
    r.step1();
    r.step2();
    r.step3();
  }
}

interface MyStepsRunnable {

  void step1();
  void step2();
  void step3();
}

Korzystając z tego wzorca masz pewność, że dowolny implementator wykona się w dokładnie takiej kolejności. Możesz pójść o krok dalej i stworzyć ExecutorFactory, który zbuduje Executory z niestandardowymi ścieżkami wykonania.

Silviu Burcea
źródło
W innym komentarzu nazwałeś to implementacją konstruktora kroków, ale tak nie jest. Jeśli masz instancję MyStepsRunnable, możesz wywołać krok 3 przed krokiem 1. Implementacja konstruktora kroków byłaby bardziej podobna do ideone.com/UDECgY . Chodzi o to, aby uzyskać coś z step2, uruchamiając step1. W związku z tym musisz wywoływać metody w odpowiedniej kolejności. Np. Patrz stackoverflow.com/q/17256627/1281433 .
Joshua Taylor
Możesz przekonwertować go na klasę abstrakcyjną za pomocą metod chronionych (lub nawet domyślnych), aby ograniczyć sposób jego użycia. Będziesz zmuszony użyć executora, ale mam wrażenie, że w obecnej implementacji może występować błąd.
Silviu Burcea
To wciąż nie czyni z niego budowniczego kroku. W twoim kodzie nic nie można zrobić, aby uruchomić kod między różnymi krokami. Chodzi nie tylko o sekwencjonowanie kodu (niezależnie od tego, czy jest to kod publiczny, prywatny, czy w inny sposób zamknięty). Jak pokazuje Twój kod, jest to łatwe do zrobienia po prostu step1(); step2(); step3();. Konstruktorem kroków jest udostępnienie interfejsu API, który ujawnia niektóre kroki, oraz wymuszenie kolejności wywoływania. Nie powinno to uniemożliwiać programistom wykonywania innych czynności między krokami.
Joshua Taylor