Interface-Design, bei dem Funktionen in einer bestimmten Reihenfolge aufgerufen werden müssen

24

Die Aufgabe besteht darin, eine Hardware innerhalb des Geräts gemäß einer Eingabespezifikation zu konfigurieren. Dies sollte wie folgt erreicht werden:

1) Sammeln Sie die Konfigurationsinformationen. Dies kann zu verschiedenen Zeiten und an verschiedenen Orten geschehen. Beispielsweise können Modul A und Modul B (zu unterschiedlichen Zeiten) einige Ressourcen von meinem Modul anfordern. Diese "Ressourcen" sind eigentlich die Konfiguration.

2) Nachdem klar ist, dass keine weiteren Anforderungen mehr realisiert werden, muss ein Startbefehl, der eine Zusammenfassung der angeforderten Ressourcen enthält, an die Hardware gesendet werden.

3) Erst danach kann (und muss) eine detaillierte Konfiguration der Ressourcen erfolgen.

4) Auch kann (und muss) erst nach 2) das Routing ausgewählter Ressourcen an die deklarierten Anrufer erfolgen.


Eine häufige Ursache für Fehler, auch für mich, der das Ding geschrieben hat, ist das Verwechseln dieser Reihenfolge. Welche Namenskonventionen, Designs oder Mechanismen kann ich verwenden, um die Benutzeroberfläche für jemanden nutzbar zu machen, der den Code zum ersten Mal sieht?

Vorac
quelle
Stage 1 heißt besser discoveryoder handshake?
Rwong
1
Die zeitliche Kopplung ist ein Anti-Muster und sollte vermieden werden.
1
Der Titel der Frage lässt mich vermuten, dass Sie sich für das Step-Builder-Muster interessieren .
Joshua Taylor

Antworten:

45

Es ist eine Neugestaltung, aber Sie können den Missbrauch vieler APIs verhindern, wenn Sie keine Methode zur Verfügung haben, die nicht aufgerufen werden sollte.

Zum Beispiel anstelle von first you init, then you start, then you stop

Ihr Konstruktor ist initein Objekt, das gestartet werden kann, und starterstellt eine Sitzung, die gestoppt werden kann.

Wenn Sie sich auf eine Sitzung gleichzeitig beschränken, müssen Sie natürlich den Fall behandeln, in dem jemand versucht, eine Sitzung mit einer bereits aktiven Sitzung zu erstellen.

Wenden Sie diese Technik nun auf Ihren eigenen Fall an.

Goldesel
quelle
zlibund jpeglibsind zwei Beispiele, die diesem Initialisierungsmuster folgen. Dennoch sind zahlreiche Dokumentationen erforderlich, um Entwicklern das Konzept beizubringen.
Rwong
5
Dies ist genau die richtige Antwort: Wenn die Reihenfolge wichtig ist, gibt jede Funktion ein Ergebnis zurück, das dann aufgerufen werden kann, um den nächsten Schritt auszuführen. Der Compiler selbst ist in der Lage, die Entwurfsbeschränkungen durchzusetzen.
2
Dies ähnelt dem Step Builder-Muster . Präsentieren Sie nur die Schnittstelle, die in einer bestimmten Phase sinnvoll ist.
Joshua Taylor
@ JoshuaTaylor meine Antwort ist ein Schritt Builder Muster Implementierung :)
Silviu Burcea
@SilviuBurcea Ihre Antwort ist keine schrittweise Implementierung, aber ich werde sie eher kommentieren als hier.
Joshua Taylor
19

Sie können die Startmethode veranlassen, ein Objekt, das ein erforderlicher Parameter ist, an die Konfiguration zurückzugeben:

Ressource * MyModule :: GetResource ();
MySession * MyModule :: Startup ();
void Resource :: Configure (MySession * -Sitzung);

Selbst wenn es sich bei Ihrer MySessionStruktur nur um eine leere Struktur handelt, wird durch die Typensicherheit sichergestellt, dass Configure()vor dem Start keine Methode aufgerufen werden kann.

jpa
quelle
Was hindert jemanden daran module->GetResource()->Configure(nullptr)?
Svick
@svick: Nichts, aber das musst du explizit machen. Dieser Ansatz sagt Ihnen, was es erwartet, und die Umgehung dieser Erwartung ist eine bewusste Entscheidung. Wie bei den meisten Programmiersprachen hindert Sie niemand daran, sich in den Fuß zu schießen. Aber es ist immer gut, durch eine API klar anzuzeigen, dass Sie dies tun;)
Michael Klement
+1 sieht gut und einfach aus. Ich sehe jedoch ein Problem. Wenn ich Objekte habe a, b, c, d, kann ich damit anfangen aund MySessionversuchen, es bals bereits gestartetes Objekt zu verwenden, während dies in Wirklichkeit nicht der Fall ist.
Vorac
8

Aufbauend auf der Antwort von Cashcow - warum müssen Sie dem Aufrufer ein neues Objekt präsentieren, wenn Sie nur ein neues Interface präsentieren können? Rebrand-Pattern:

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

Sie können ITerminateable auch IRunnable implementieren lassen, wenn eine Sitzung mehrmals ausgeführt werden kann.

Ihr Objekt:

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...

Auf diese Weise können Sie nur die richtigen Methoden aufrufen, da Sie am Anfang nur das IStartable-Interface haben und die run () -Methode nur dann aufrufen, wenn Sie start () aufgerufen haben. Von außen sieht es aus wie ein Muster mit mehreren Klassen und Objekten, aber die zugrunde liegende Klasse bleibt eine Klasse, auf die immer verwiesen wird.

Falco
quelle
1
Was ist der Vorteil, wenn nur eine Klasse anstelle mehrerer Klassen zugrunde liegt? Da dies der einzige Unterschied zu der von mir vorgeschlagenen Lösung ist, würde mich dieser spezielle Punkt interessieren.
Michael Le Barbier Grünewald
1
@ MichaelGrünewald Es ist nicht erforderlich, alle Schnittstellen mit einer Klasse zu implementieren, aber für ein Objekt vom Typ Konfiguration ist es möglicherweise die einfachste Implementierungstechnik, die Daten zwischen Instanzen der Schnittstellen gemeinsam zu nutzen (z. B. weil sie identisch sind) Objekt).
Joshua Taylor
1
Dies ist im Wesentlichen das Step-Builder-Muster .
Joshua Taylor
@JoshuaTaylor Es gibt zwei Möglichkeiten, Daten zwischen Instanzen der Schnittstelle auszutauschen: Obwohl die Implementierung möglicherweise einfacher ist, müssen wir darauf achten, nicht auf den Status "undefiniert" zuzugreifen (z. B. auf die Client-Adresse eines nicht verbundenen Servers). Da das OP den Schwerpunkt auf die Benutzerfreundlichkeit der Benutzeroberfläche legt, können wir beide Ansätze gleich bewerten. Vielen Dank, dass Sie das "Step Builder Pattern" zitiert haben.
Michael Le Barbier Grünewald
1
@ MichaelGrünewald Wenn Sie mit dem Objekt nur über die bestimmte Schnittstelle interagieren, die an einem bestimmten Punkt angegeben ist, sollte es keine Möglichkeit geben (ohne Casting usw.), auf diesen Status zuzugreifen.
Joshua Taylor
2

Es gibt viele gültige Ansätze, um Ihr Problem zu lösen. Basile Starynkevitch schlug einen bürokratiefreien Ansatz vor, bei dem Sie eine einfache Schnittstelle haben und sich darauf verlassen können, dass der Programmierer die Schnittstelle entsprechend verwendet. Während ich diesen Ansatz mag, werde ich einen anderen vorstellen, der mehr Ingenieurskunst hat, aber dem Compiler erlaubt, einige Fehler abzufangen.

  1. Identifizieren Sie die verschiedenen Zustände Ihr Gerät in kann, wie Uninitialised, Started, Configuredund so weiter. Die Liste muss endlich sein

  2. Für jeden Zustand, definiert ein structzu diesem Zustand hält die erforderlichen zusätzliche relevante Informationen, zum Beispiel DeviceUninitialised, DeviceStartedund so weiter.

  3. Packen Sie alle Behandlungen in ein Objekt, DeviceStrategywobei die Methoden die in 2. definierten Strukturen als Ein- und Ausgänge verwenden. So haben Sie möglicherweise eine DeviceStarted DeviceStrategy::start (DeviceUninitalised dev)Methode (oder was auch immer die Entsprechung gemäß Ihren Projektkonventionen sein könnte).

Bei diesem Ansatz muss ein gültiges Programm einige Methoden in der von den Methodenprototypen erzwungenen Reihenfolge aufrufen.

Die verschiedenen Zustände sind nicht verwandte Objekte, dies liegt am Substitutionsprinzip. Wenn es für Sie nützlich ist, wenn diese Strukturen einen gemeinsamen Vorfahren haben, erinnern Sie sich daran, dass das Besuchermuster verwendet werden kann, um den konkreten Typ der Instanz einer abstrakten Klasse wiederherzustellen.

Während ich in 3. eine einzigartige DeviceStrategyKlasse beschrieben habe, gibt es Situationen, in denen Sie die bereitgestellten Funktionen auf mehrere Klassen aufteilen möchten.

Zusammenfassend sind die wichtigsten Punkte des von mir beschriebenen Designs:

  1. Aufgrund des Substitutionsprinzips sollten Objekte, die Gerätezustände darstellen, unterschiedlich sein und keine speziellen Vererbungsbeziehungen aufweisen.

  2. Packen Sie Gerätebehandlungen in Startegy-Objekte und nicht in Objekte, die Geräte selbst darstellen, sodass jedes Gerät oder jeder Gerätezustand nur sich selbst sieht und die Strategie alle von ihnen sieht und mögliche Übergänge zwischen ihnen ausdrückt.

Ich würde schwören, dass ich einmal eine Beschreibung einer Telnet-Client-Implementierung gesehen habe, die diesen Zeilen folgt, aber ich konnte sie nicht wiederfinden. Es wäre eine sehr nützliche Referenz gewesen!

¹: Folgen Sie dazu entweder Ihrer Intuition oder finden Sie in Ihrer konkreten Implementierung die Äquivalenzklassen von Methoden für die Beziehung „method₁ ~ method₂ iff. Es ist gültig, sie für dasselbe Objekt zu verwenden. “Vorausgesetzt, Sie haben ein großes Objekt, in dem alle Behandlungen auf Ihrem Gerät zusammengefasst sind. Beide Methoden zur Auflistung von Status liefern fantastische Ergebnisse.

Michael Le Barbier Grünewald
quelle
1
Anstatt separate Strukturen zu definieren, kann es ausreichen, die erforderlichen Schnittstellen zu definieren, die ein Objekt in jeder Phase aufweisen soll. Dann ist es das Step-Builder-Muster .
Joshua Taylor
2

Verwenden Sie ein Builder-Muster.

Haben Sie ein Objekt, das Methoden für alle oben genannten Operationen hat. Diese Vorgänge werden jedoch nicht sofort ausgeführt. Es merkt sich nur jede Operation für später. Da die Operationen nicht sofort ausgeführt werden, spielt die Reihenfolge, in der Sie sie an den Builder übergeben, keine Rolle.

Nachdem Sie alle Operationen im Builder definiert haben, rufen Sie eine execute-Methode auf. Wenn diese Methode aufgerufen wird, führt sie alle oben aufgeführten Schritte in der richtigen Reihenfolge mit den oben gespeicherten Vorgängen aus. Diese Methode ist auch ein guter Ort, um betriebsübergreifende Sicherheitsüberprüfungen durchzuführen (z. B. den Versuch, eine noch nicht eingerichtete Ressource zu konfigurieren), bevor Sie sie auf die Hardware schreiben. Dies erspart Ihnen möglicherweise die Beschädigung der Hardware mit einer unsinnigen Konfiguration (falls Ihre Hardware dafür anfällig ist).

Philipp
quelle
1

Sie müssen nur richtig dokumentieren, wie die Benutzeroberfläche verwendet wird, und ein Tutorial-Beispiel geben.

Möglicherweise haben Sie auch eine Debugging-Bibliotheksvariante, die einige Laufzeitprüfungen durchführt.

Vielleicht definieren und richtig gibt es Namenskonventionen (zB Dokumentation preconfigure*, startup*, postconfigure*, run*....)

Übrigens folgen viele vorhandene Schnittstellen einem ähnlichen Muster (z. B. X11-Toolkits).

Basile Starynkevitch
quelle
Möglicherweise ist ein Statusübergangsdiagramm erforderlich, das dem Aktivitätslebenszyklus der Android-Anwendung ähnelt , um die Informationen zu übermitteln.
Rwong
1

Dies ist in der Tat eine häufige und heimtückische Art von Fehler, da Compiler nur Syntaxbedingungen erzwingen können, während Ihre Client-Programme "grammatikalisch" korrekt sein müssen.

Leider sind Benennungskonventionen gegen diese Art von Fehler fast völlig wirkungslos. Wenn Sie die Leute wirklich ermutigen möchten, nichts Unrammatisches zu tun, sollten Sie ein Befehlsobjekt verteilen, das mit Werten für die Voraussetzungen initialisiert werden muss, damit sie die Schritte nicht in falscher Reihenfolge ausführen können.

Kilian Foth
quelle
Haben Sie so etwas wie bedeuten diese ?
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();
}

Mit diesem Muster können Sie sicher sein, dass jeder Implementierer genau in dieser Reihenfolge ausgeführt wird. Sie können noch einen Schritt weiter gehen und eine ExecutorFactory erstellen, die Executors mit benutzerdefinierten Ausführungspfaden erstellt.

Silviu Burcea
quelle
In einem anderen Kommentar haben Sie dies eine Step Builder-Implementierung genannt, dies ist jedoch nicht der Fall. Wenn Sie eine Instanz von MyStepsRunnable haben, können Sie step3 vor step1 aufrufen. Eine Step-Builder-Implementierung wäre eher im Sinne von ideone.com/UDECgY . Die Idee ist, dass man mit step2 nur etwas erreicht, indem man step1 ausführt. Daher müssen Sie die Methoden in der richtigen Reihenfolge aufrufen. Siehe z . B. stackoverflow.com/q/17256627/1281433 .
Joshua Taylor
Sie können es mit geschützten Methoden (oder sogar mit Standardmethoden) in eine abstrakte Klasse konvertieren, um die Verwendungsmöglichkeiten einzuschränken. Sie werden gezwungen sein, den Executor zu verwenden, aber ich habe das es einen oder zwei Fehler in der aktuellen Implementierung geben könnte.
Silviu Burcea
Das macht es immer noch nicht zu einem Step Builder. In Ihrem Code gibt es nichts, was ein Benutzer tun kann, um Code zwischen den verschiedenen Schritten auszuführen . Die Idee ist nicht nur , Code zu sequenzieren (unabhängig davon, ob er öffentlich oder privat oder auf andere Weise gekapselt ist). Wie Ihr Code zeigt, ist das einfach genug step1(); step2(); step3();. Der Point-of-Step-Builder besteht darin, eine API bereitzustellen, die einige Schritte verfügbar macht, und die Reihenfolge zu erzwingen, in der sie aufgerufen werden. Es sollte einen Programmierer nicht davon abhalten, zwischen den Schritten andere Dinge zu tun.
Joshua Taylor