4 Stimmen

Wie kann man die Schnittstelle eines Objekts abhängig von seinem Zustand ändern?

Gibt es für ein komplexes Objekt mit vielen Zuständen ein Muster für die Darstellung verschiedener Funktionen in Abhängigkeit von diesem Zustand?

Ein konkretes Beispiel: Stellen Sie sich eine Printer Objekt.

  • Über die Schnittstelle des Objekts können Sie zunächst die Fähigkeiten des Druckers abfragen, Einstellungen wie die Papierausrichtung ändern und einen Druckauftrag starten.

  • Wenn Sie einen Druckauftrag gestartet haben, können Sie ihn zwar noch abfragen, aber Sie können keinen weiteren Auftrag starten oder bestimmte Druckereinstellungen ändern. Sie können eine Seite starten.

  • Sobald Sie eine Seite begonnen haben, können Sie die eigentlichen Text- und Grafikbefehle eingeben. Sie können die Seite "fertigstellen". Sie können nicht zwei Seiten gleichzeitig öffnen.

  • Einige Druckereinstellungen können nur zwischen den Seiten geändert werden.

Eine Idee ist es, eine Printer Objekt mit einer großen Anzahl von Methoden. Wenn Sie eine Methode zu einem ungünstigen Zeitpunkt aufrufen (z. B. wenn Sie versuchen, die Papierausrichtung in der Mitte einer Seite zu ändern), würde der Aufruf fehlschlagen. Wenn Sie in der Sequenz vorwärts springen und mit dem Aufruf von Grafiken beginnen, könnte die Printer Objekt könnte implizit den Aufruf der StartJob() y StartPage() Methoden nach Bedarf. Der größte Nachteil dieses Ansatzes ist, dass er für den Aufrufer nicht sehr einfach ist. Die Schnittstelle könnte überwältigend sein, und die Anforderungen an die Reihenfolge sind nicht sehr offensichtlich.

Eine andere Idee ist, die Dinge in einzelne Objekte aufzuteilen: Printer , PrintJob und Page . Die Printer Objekt stellt die Abfragemethoden und eine StartJob() Methode. StartJob() gibt eine PrintJob Objekt, das über Abort() , StartPage() und Methoden zur Änderung nur der veränderbaren Einstellungen. StartPage() gibt eine Page Objekt, das eine Schnittstelle für die eigentlichen Grafikaufrufe bietet. Der Nachteil liegt hier in der Mechanik. Wie kann man die Schnittstelle eines Objekts offenlegen, ohne die Kontrolle über die Lebensdauer dieses Objekts aufzugeben? Wenn ich dem Aufrufer einen Zeiger auf ein Page Ich will nicht, dass sie es tun. delete und ich kann ihnen kein weiteres geben, bis sie das erste zurückgeben.

Halten Sie sich nicht zu sehr am Beispiel des Druckens auf. Mir geht es um die allgemeine Frage, wie man je nach Zustand des Objekts unterschiedliche Schnittstellen darstellen kann.

4voto

Eric Petroelje Punkte 58501

Ja, es heißt die Zustandsmuster .

Die allgemeine Idee ist, dass Ihr Printer-Objekt ein PrinterState-Objekt enthält. Alle (oder die meisten) Methoden des Printer-Objekts werden einfach an den enthaltenen PrinterState delegiert. Sie würden dann mehrere PrinterState-Klassen haben, die die Methoden auf unterschiedliche Weise implementieren, je nachdem, was in diesem Zustand erlaubt/nicht erlaubt ist. Die PrinterState-Implementierungen würden auch mit einem "Hook" versehen, der es ihnen ermöglicht, den aktuellen Zustand des Printer-Objekts in einen anderen Zustand zu ändern.

Hier ist ein Beispiel mit ein paar Staaten. Es scheint kompliziert zu sein, aber wenn man ein komplexes zustandsspezifisches Verhalten hat, ist es tatsächlich viel einfacher zu programmieren und zu warten:

public abstract class PrinterState {
    private PrinterStateContext stateContext;

    public PrinterState( PrinterStateContext context ) {
        stateContext = context;
    }

    void StartJob() {;}
}

public class PrinterStateContext {
     public PrinterState currentState;
}

public class PrinterReadyState : PrinterState {

    public PrinterReadyState( PrinterStateContext context ) {
        super(context);
    }

    void StartJob() {
        // Do whatever you do to start a job..

        // Switch to "printing" state.
        stateContext.currentState = new PrinterPrintingState(stateContext);
    }
}

public class PrinterPrintingState : PrinterState {

    public PrinterPrintingState( PrinterStateContext context ) {
         super(context);
    }

    void StartJob() {
        // Already printing, can't start a new job.
        throw new Exception("Can't start new job, already printing");
    }
}

public class Printer : IPrinter {
    private PrinterStateContext stateContext;

    public Printer() {
        stateContext = new PrinterStateContext();
        stateContext.currentState = new PrinterReadyState(stateContext);
    }

    public void StartJob() {
        stateContext.currentState.StartJob();
    }
}

3voto

djna Punkte 53789

Ich würde mich für Ihre separaten Objekte entscheiden. Diese müssen dem Kunden nicht erlauben, etwas Unerwünschtes zu tun.

IPage Job.getPage()

Die IPage-Schnittstelle stellt nur das zur Verfügung, was Sie brauchen. Es ist nicht das "echte" Page-Objekt, sondern eher ein Proxy für eine Page. Das Löschen (oder Verlassen des Geltungsbereichs) des Proxy-Objekts hat keine Auswirkungen auf das echte Objekt.

--- als Reaktion auf Kommentare erweitert ---

Die erste Frage lautet: Kann sich das Objekt, das wir "halten", unter unseren Füßen verändern. Die Seite, auf die sich unser Proxy bezog, ist fertig gedruckt. Wird dadurch unser Proxy ungültig oder wird er stillschweigend zu einem Proxy für die nächste Seite?

Welches Design sinnvoll ist, hängt sehr stark von den Besonderheiten der Problemdomäne ab, und es ist wahrscheinlich nicht hilfreich, mit meinem persönlichen Wissen über Drucker zu argumentieren.

Versuchen wir stattdessen, die Gestaltungsprinzipien zu abstrahieren.

Also erstens: Getrennte Schnittstellen für verschiedene Zustände machen es einfacher, Code zu schreiben. Wir vermeiden einfach Dummheiten wie die Aufforderung an Raupen zu fliegen und an Puppen, sich zu paaren. Allerdings stoßen wir auf das Problem, wie viele Unterzustände es geben soll ... Unterscheiden sich hungrige Raupen von schlafenden Raupen von sehr hungrigen Raupen?

Daher wird es wahrscheinlich immer noch Ausnahmen wie "Ich kann nicht gehen, ich schlafe" geben.

Wenn man die Idee weiterdenkt, sollte man keine Schnittstelle entwerfen, die besagt: "Bevor Sie Methode A aufrufen, müssen Sie Methode B aufrufen", d.h. eine zustandsabhängige Schnittstelle. Stattdessen erlaubt die Schnittstelle nur den Aufruf von A und gibt eine neue Schnittstelle zurück, die B offenlegt.

Im Beispiel des Schmetterlings funktioniert dieses Muster sehr gut.

Das Page-Beispiel scheint mir ein zusätzliches Element zu haben: Zustandsänderungen können aufgrund interner Ereignisse erfolgen. Wir hatten also eine Raupe und plötzlich ist sie ein Schmetterling. Ich glaube, jetzt sind wir bei einem ganz anderen Paradigma. Es ist mehr ereignisgesteuert. Ich denke, wir haben also ganz andere Design-Herausforderungen.

Job.registerPageEventListener( me )

und meine Arbeitsgeräte

boolean pageStarted(IPage)

Vielleicht kann ich true zurückgeben, um zu sagen "drucke es" und false, um zu sagen "halte es", und dann daran arbeiten.

1voto

xtofl Punkte 39285

Grundsätzlich scheinen Sie das State-Muster zu benötigen: Unterschiedliches Verhalten wird durch unterschiedliche Implementierungen modelliert.

Darüber hinaus möchten Sie eine bequeme Schnittstelle: die Printer -> Page -> Job Die Trennung ist ziemlich gut und zeigt deutlich die Abfolge der Aktionen. Sie können die Tatsache modellieren, dass sie ungültig gemacht werden können, indem Sie sie zu einem Proxy für das Hauptobjekt machen.

Das Hauptobjekt kann dann, wenn es in einen anderen Zustand übergeht, alle vorherigen Proxys ungültig machen. Auf diese Weise entkoppeln Sie die Lebensdauer des Objekts von seiner Gültigkeit. Das Löschen eines Proxys würde ihn natürlich auch aus der Liste der Proxys des Hauptobjekts entfernen.

1voto

Steve Jessop Punkte 264569

Wie kann man die Schnittstelle eines Objekts offenlegen, ohne die Kontrolle über die Lebensdauer des Objekts aufzugeben? Wenn ich dem Aufrufer einen Zeiger auf eine Page

Nur um diesen Punkt im Besonderen anzusprechen, unabhängig vom Rest der Frage.

Sie scheinen von einer API zu sprechen, die in etwa so aussieht:

Page *Printer::newPage();

Ich würde davon abraten und stattdessen einen Konstruktor empfehlen, der wie folgt aussieht:

Page::Page(Printer &);

Das heißt, dass Sie nicht ein Page-Objekt im Drucker zuweisen, es an den Aufrufer zurückgeben und sich dann um den Lebenszyklus des Objekts kümmern müssen. Geben Sie stattdessen die Kontrolle über den Lebenszyklus von Objekten ab prinzipiell , um Ihren Benutzern Flexibilität zu bieten. Sie wollen, dass der Benutzer eine Seite beginnt, etwas darauf zeichnet und dann die Seite beendet. Lassen Sie ihn also genau das tun: ein Seitenobjekt erstellen, etwas zeichnen, sehen, ob es funktioniert hat, vielleicht eine flush y cancel Funktionen, vielleicht sogar auch blockUntilDonePrinting y getFailureCode und so weiter. Wenn sie dann mit der Seite fertig sind, zerstören sie sie (oder, was wahrscheinlicher ist, lassen sie sie einfach aus dem Geltungsbereich fallen), und dann können sie eine neue erstellen.

Wenn Sie Fabriken brauchen:

Page *PageFactory::newPage(Printer &);

In jedem Fall muss die Seite selbst wissen, was sie mit einem Drucker zu tun hat, um Dinge zu drucken. Ein Drucker ist nicht dasselbe wie eine Fabrik für Seiten. Nun, in der realen Welt ist er es tatsächlich, aber das bedeutet nicht, dass er es auch in der Software sein sollte, da unser Page-Objekt nicht wirklich eine physische Seite ist, sondern der Prozess des Zeichnens einer Seite. Wenn unser Page-Objekt nur eine Seite repräsentieren würde, dann bräuchte es überhaupt nicht mit dem Printer-Objekt zu interagieren - wir könnten unsere Seiten konstruieren, sie in Postscript serialisieren und uns dann darum kümmern, wie der Printer sie druckt.

Wie auch immer, der Drucker かもしれない als PageFactory dienen, aber hier gibt es zwei verschiedene Anliegen: (1) Verwaltung des Zugriffs auf eine Hardwareressource, die Dinge druckt, und (2) Verwaltung des Arbeitsablaufs von Benutzern, die druckbare Objekte in Software erstellen. Der Drucker muss nicht beides tun, also könnte man sie trennen.

Gleiches gilt für jedes Objekt mit Zuständen - trennen Sie das Objekt selbst (mit zwei oder mehr Zuständen) von einer Sitzung oder einem Arbeitsablauf, der diese Zustände durchläuft.

Ich will nicht, dass sie es löschen.

Es ist völlig in Ordnung, wenn eine API sagt: "Der Benutzer darf den Referand des Zeigers, der von newPage , sondern muss stattdessen Printer::close(Page *) mit dem Zeiger als Parameter". Aber wie gesagt, in C++ muss man, anders als in C, keine solchen APIs erstellen.

Ich kann ihnen kein weiteres geben, bis sie das erste zurückgeben.

Ich würde versuchen, diese Einschränkung zu umgehen (Druckwarteschlange, jemand?), so dass zwar immer nur eine Seite gedruckt wird, aber mehrere Seiten erstellt werden können, die gleichzeitig mit dem Druckertreiber kommunizieren. Aber das Drucken ist nur das Beispiel. Wenn eine Seite wirklich die ausschließliche Verwendung des Druckers während der gesamten Lebensdauer der Seite erfordert (wie es zum Beispiel der Fall wäre, wenn wir über Mutex und MutexSession statt über Drucker und Seite sprechen würden), dann sollte der Drucker eine API haben (vielleicht öffentlich, vielleicht zugänglich über friend (je nachdem, ob die Seitenimplementierung für die Druckerimplementierung eindeutig sein soll). Page verwendet dies, um exklusiven Zugriff zu erhalten (nennen wir es das "Drucker-Token"). Wenn Sie versuchen, eine Seite zu erstellen, wenn bereits eine andere Seite mit demselben Drucker existiert, schlägt dies fehl (oder blockiert, oder was auch immer für den Problembereich angemessen ist).

0voto

RED SOFT ADAIR Punkte 11611

Aus Sicht des Protokolls sind beide Möglichkeiten ähnlich: Bei beiden Implementierungen sind bestimmte Aufrufe zu bestimmten Zeiten ungültig. Im ersten Fall handelt es sich um den Aufruf einer Funktion, im zweiten Fall um den Aufruf zum Abrufen eines verknüpften Objekts.

Aus architektonischer Sicht ist es jedoch immer besser, große Klassen in kleinere, unabhängige Klassen aufzuteilen. Dies wird besser wartbar etc. sein. Ich würde also auch zu diesem Ansatz raten.

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X