Entwurfsmuster für "Operation an Objekt zulässig, nur wenn sich das Objekt in einem bestimmten Zustand befindet"

8

Zum Beispiel:

Es können nur Bewerbungen aktualisiert werden, die noch nicht geprüft oder genehmigt wurden. Mit anderen Worten, eine Person kann ihr Job-Appliance-Formular aktualisieren, bis die Personalabteilung mit der Überprüfung beginnt oder es bereits akzeptiert wurde.

Eine Bewerbung kann also in 4 Zuständen erfolgen:

APPLIED (Ausgangszustand), IN_REVIEW, APPROVED, DECLINED

Wie erreiche ich ein solches Verhalten?

Sicherlich kann ich eine update () -Methode in die Anwendungsklasse schreiben, den Anwendungsstatus überprüfen und nichts tun oder eine Ausnahme auslösen, wenn sich die Anwendung nicht im erforderlichen Status befindet

Diese Art von Code macht jedoch nicht deutlich, dass eine solche Regel existiert. Sie ermöglicht es jedem, die update () -Methode aufzurufen, und erst wenn ein Client fehlschlägt, weiß er, dass eine solche Operation nicht zulässig war. Daher muss sich der Kunde bewusst sein, dass ein solcher Versuch fehlschlagen kann. Seien Sie daher vorsichtig. Wenn der Kunde sich solcher Dinge bewusst ist, bedeutet dies auch, dass die Logik nach außen leckt.

Ich habe versucht, für jeden Status unterschiedliche Klassen zu erstellen (ApprovedApplication usw.) und zulässige Operationen nur für zulässige Klassen durchzuführen, aber diese Art von Ansatz fühlt sich auch falsch an.

Gibt es ein offizielles Entwurfsmuster oder einen einfachen Code, um ein solches Verhalten zu implementieren?

uylmz
quelle
7
Diese Dinge werden im Allgemeinen als StateMachines bezeichnet und ihre Implementierung hängt ein wenig von Ihren Anforderungen und den Sprachen ab, mit denen Sie arbeiten.
Telastyn
und wie stellen Sie sicher, dass die richtigen Methoden für die richtigen Zustände verfügbar sind?
Uylmz
1
Es kommt auf die Sprache an. Verschiedene Klassen sind eine übliche Implementierung für populäre Sprachen, obwohl "werfen, wenn nicht im richtigen Zustand" wahrscheinlich am häufigsten ist.
Telastyn
1
Wo liegt ein Problem beim Einfügen und Überprüfen der Methode "canUpdate" vor dem Aufruf von Update?
Euphoric
1
this kind of code does not make it obvious such a rule exists- Deshalb hat Code Dokumentation. Autoren von gutem Code werden den Rat von Euphoric befolgen und eine Methode bereitstellen, mit der die Regel von außen getestet werden kann, bevor die Hardware ausprobiert wird.
Blrfl

Antworten:

4

Diese Art von Situation taucht ziemlich oft auf. Beispielsweise können Dateien nur im geöffneten Zustand bearbeitet werden. Wenn Sie versuchen, nach dem Schließen etwas mit einer Datei zu tun, wird eine Laufzeitausnahme angezeigt.

Ihr Wunsch ( in Ihrer vorherigen Frage zum Ausdruck gebracht ), das Typensystem der Sprache zu verwenden, um sicherzustellen, dass nicht einmal das Falsche passieren kann, ist nobel, da Fehler bei der Kompilierung immer Laufzeitfehlern vorzuziehen sind. Es gibt jedoch kein Entwurfsmuster, das ich für diese Art von Situation kenne, wahrscheinlich weil es mehr Probleme verursachen als lösen würde. (Es wäre unpraktisch.)

Das nächste, was Ihrer mir bekannten Situation am nächsten kommt, ist die Modellierung verschiedener Zustände eines Objekts, die unterschiedlichen Funktionen entsprechen, über zusätzliche Schnittstellen. Auf diese Weise reduzieren Sie jedoch nur die Anzahl der Stellen im Code, an denen möglicherweise ein Laufzeitfehler auftritt Die Möglichkeit eines Laufzeitfehlers wird nicht beseitigt.

In Ihrer Situation würden Sie also eine Reihe von Schnittstellen deklarieren, die beschreiben, was mit Ihrem Objekt in seinen verschiedenen Zuständen getan werden kann, und Ihr Objekt würde bei einem Zustandsübergang einen Verweis auf die richtige Schnittstelle zurückgeben.

So würde beispielsweise die approve()Methode Ihrer Klasse eine ApprovedApplicationSchnittstelle zurückgeben. Die Schnittstelle würde privat implementiert (über eine verschachtelte Klasse), sodass Code, der nur einen Verweis auf eine hat Application, keine der ApprovedApplicationMethoden aufrufen kann . Dann gibt Code, der eine genehmigte Anwendung manipuliert, ausdrücklich seine Absicht an, dies zum Zeitpunkt der Kompilierung zu tun, indem er eine ApprovedApplicationArbeit benötigt. Wenn Sie diese Schnittstelle jedoch irgendwo speichern und diese Schnittstelle nach dem decline()Aufrufen der Methode weiter verwenden, wird natürlich immer noch ein Laufzeitfehler angezeigt. Ich glaube nicht, dass es eine perfekte Lösung für Ihr Problem gibt.

Mike Nakis
quelle
Als Randnotiz sollte es application.approve (jemandWhoCanApprove) oder jemandWhoCanApprove.approve (Anwendung) sein? Ich denke, es sollte das erste sein, da "jemand" möglicherweise keinen Zugriff auf Anwendungsfelder hat, um notwendige Anpassungen vorzunehmen
uylmz
Ich bin mir nicht sicher, aber Sie sollten auch die Möglichkeit prüfen, dass keiner der beiden richtig ist. dh if( someone.hasApprovalPermission( application ) ) { application.approve(); } das Prinzip der Trennung von Bedenken gibt an, dass weder Anwendung, noch jemand, sollten Entscheidungen bezüglich der Berechtigungen und Sicherheit befassen.
Mike Nakis
3

Ich nicke mit dem Kopf über verschiedene Teile der verschiedenen Antworten, aber das OP scheint immer noch das Problem der Flusskontrolle zu haben. Es gibt zu viel zu versuchen, in Worten zu verschmelzen. Ich werde nur einen Code korrigieren - The State Pattern.


Staatsnamen als Vergangenheitsform

"In_Review" ist vielleicht kein Zustand, sondern ein Übergang oder Prozess. Andernfalls sollten Ihre Statusnamen konsistent sein: "Anwenden", "Genehmigen", "Ablehnen" usw. ODER Sie haben auch "Überprüft". Oder nicht.

Der angewendete Status führt einen Überprüfungsübergang durch und setzt den Status auf Überprüft. Der überprüfte Status führt einen Genehmigungsübergang durch und setzt den Status auf Genehmigt (oder Abgelehnt).


// Application class encapsulates state transition,
// the client is unable to directly set state.
public class Application {
    State currentState = null;

    State AppliedState    = new Applied(this);
    State DeclinedState   = new Declined(this);
    State ApprovedState   = new Approved(this);
    State ReviewedState   = new Reviewed(this);

    public class Application (ApplicationDocument myApplication) {
        if(myApplication != null && isComplete()) {
            currentState = AppliedState;
        } else {            
            throw new ArgumentNullException ("Your application is incomplete");
            // some kind of error communication would probably be better
        }
    }

    public apply()    { currentState.apply(); }
    public review()   { currentState.review(); }
    public approve()  { currentState.approve(); }
    public decline()  { currentState.decline(); }


    //These could be done via an enum. I like enums!
    protected void setSubmittingState() {}
    protected void setApproveState() {}
    // etc. ...
}

// could be an interface if we don't have any default or base behavior.
public abstract class State {   
    protected Application theApp;
    // maybe these return an object communicating errors / error state.
    public abstract void apply();
    public abstract void review();
    public abstract void accept();
    public abstract void decline();
}

public class Applied implements State {
    public Applied (Application newApp) {
        if(newApp != null)
            theApp = newApp;
        else
            throw new ArgumentNullException ("null application argument");
     }

    public override void apply() {
        // whatever is appropriate when already in "applied" state
        // do not do any work on behalf of other states!
        // throwing exceptions here is not appropriate, as others
        // have said.
      }

    public override void review() {
        if(recursiveBureaucracyBuckPassing())
            theApp.setReviewedState();
    }

    public override void decline() { // ditto  }
}

public class Reviewed implements State {}
public class Approved implements State {}
public class Declined implements State {}

Bearbeiten - Fehlerbehandlung Kommentare

Ein aktueller Kommentar:

... Wenn Sie versuchen, ein Buch auszuleihen, das bereits an eine andere Person ausgegeben wurde, enthält das Buchmodell die Logik, um zu verhindern, dass sich der Status ändert. Dies kann über einen Rückgabewert (z. B. ein boolesches erfolgreiches Ja / Nein oder Statuscode) oder eine Ausnahme (z. B. IllegalStateChangeException) oder auf andere Weise erfolgen. Unabhängig von den gewählten Mitteln wird dieser Aspekt nicht als Teil dieser (oder einer anderen) Antwort behandelt.

Und von der ursprünglichen Frage:

Diese Art von Code macht jedoch nicht deutlich, dass eine solche Regel existiert. Sie ermöglicht es jedem, die update () -Methode aufzurufen, und erst wenn ein Client fehlschlägt, weiß er, dass eine solche Operation nicht zulässig war.

Es gibt noch mehr Designarbeit zu erledigen. Es gibt keine Unified Field Theory Pattern. Die Verwirrung ergibt sich aus der Annahme, dass das Zustandsübergangs-Framework allgemeine Anwendungsfunktionen und Fehlerbehandlung übernimmt. Das fühlt sich falsch an, weil es so ist. Die angezeigte Antwort dient zur Steuerung der Zustandsänderung.


Sicherlich kann ich eine update () -Methode in die Anwendungsklasse schreiben, den Anwendungsstatus überprüfen und nichts tun oder eine Ausnahme auslösen, wenn sich die Anwendung nicht im erforderlichen Status befindet

Dies deutet darauf hin, dass hier drei Funktionen funktionieren: Der Status, die Aktualisierung und die Interaktion der beiden. In diesem Fall Applicationist nicht der Code, den ich geschrieben habe. Es kann es verwenden, um den aktuellen Status zu bestimmen. Applicationist auch nicht das applicationPaperwork. Applicationist nicht das Zusammenspiel der beiden, sondern könnte eine allgemeine StateContextEvaluatorKlasse sein. Jetzt Applicationwerden diese Komponenteninteraktionen orchestriert und dann entsprechend gehandelt, wie wenn eine Fehlermeldung ausgegeben wird.

Bearbeiten beenden

Radarbob
quelle
Vermisse ich etwas Dies scheint es zu ermöglichen, alle vier Methoden unabhängig vom Status aufzurufen, ohne einen Hinweis darauf zu geben, wie dieses Setup für die Kommunikation mit den aufrufenden Methoden verwendet werden soll, bei denen der Aufruf von apply () beispielsweise aufgrund einer bereits angewendeten Anwendung nicht erfolgreich war.
Kwah
1
Erlaube den Aufruf aller vier Methoden, unabhängig vom Status Ja. Es muss. ohne einen Hinweis darauf, wie dieses Setup für die Kommunikation mit den aufrufenden Methoden verwendet werden soll. Siehe den Kommentar im ApplicationKonstruktor, in dem die Ausnahme ausgelöst wird. Möglicherweise führt ein Anruf AppliedState.Approve()zu einer Benutzermeldung: "Der Antrag muss überprüft werden, bevor er genehmigt werden kann."
Radarbob
1
... der Aufruf von apply () war nicht erfolgreich , da er sich beispielsweise bereits beworben hat . Das ist falsch gedacht. Der Anruf ist erfolgreich. Es gibt jedoch unterschiedliche Verhaltensweisen für unterschiedliche Zustände. Das ist das Zustandsmuster ...... Der Programmierer muss jedoch entscheiden, welches Verhalten angemessen ist. Aber es ist falsch zu denken, dass "OMG ein Fehler !!! Wir müssen apoplektisch werden und das Programm abbrechen!" Ich erwarte AppliedState.apply(), den Benutzer sanft daran zu erinnern, dass der Antrag bereits eingereicht wurde und auf eine Überprüfung wartet. Und das Programm geht weiter.
Radarbob
Unter der Annahme, dass das Statusmuster als Modell verwendet wird, muss der "Fehler" der Benutzeroberfläche mitgeteilt werden. Wenn Sie beispielsweise versuchen, ein Buch auszuleihen, das bereits an eine andere Person ausgegeben wurde, enthält das Buchmodell die Logik, um zu verhindern, dass sich der Status ändert. Dies kann über einen Rückgabewert (z. B. ein boolesches erfolgreiches Ja / Nein oder Statuscode) oder eine Ausnahme (z. B. IllegalStateChangeException) oder ein anderes Mittel erfolgen. Unabhängig von den gewählten Mitteln wird dieser Aspekt nicht als Teil dieser (oder einer anderen) Antwort behandelt.
Kwah
Gott sei Dank hat es jemand gesagt. „Ich brauche ein anderes Verhalten basierend auf Zustand eines Objekts. ... Ja, ja. Sie das wollen Zustand Muster.“ ++ alte Bohne.
RubberDuck
1

Im Allgemeinen beschreiben Sie einen Workflow. Insbesondere fallen Geschäftsfunktionen, die von Staaten wie REVIEWED APPROVED oder DECLINED verkörpert werden, unter die Überschrift "Geschäftsregeln" oder "Geschäftslogik".

Um klar zu sein, sollten Geschäftsregeln nicht in Ausnahmen kodiert werden. Dies würde bedeuten, Ausnahmen für die Programmflusssteuerung zu verwenden, und es gibt viele gute Gründe, warum Sie dies nicht tun sollten. Ausnahmen sollten für außergewöhnliche Bedingungen verwendet werden, und der UNGÜLTIGE Status einer Anwendung ist aus geschäftlicher Sicht völlig außergewöhnlich.

Verwenden Sie Ausnahmen in Fällen, in denen das Programm ohne Benutzereingriff keine Fehlerbedingung beheben kann (z. B. "Datei nicht gefunden").

Es gibt kein spezifisches Muster zum Schreiben von Geschäftslogik, außer den üblichen Techniken zum Anordnen von Geschäftsdatenverarbeitungssystemen und zum Schreiben von Code zur Implementierung Ihrer Prozesse. Wenn die Geschäftsregeln und der Workflow ausgefeilt sind, sollten Sie eine Art Workflow-Server oder eine Geschäftsregel-Engine verwenden.

In jedem Fall können die Zustände REVIEW, APPROVED, DECLINED usw. durch eine private Variable vom Typ Enum in Ihrer Klasse dargestellt werden. Wenn Sie Getter / Setter-Methoden verwenden, können Sie steuern, ob die Setter Änderungen zulassen, indem Sie zuerst den Wert der Enum-Variablen untersuchen. Wenn jemand versucht, einen Setter zu schreiben , wenn der ENUM - Wert im falschen Zustand ist, dann können Sie eine Ausnahme werfen.

Robert Harvey
quelle
Es gibt ein Objekt namens "Anwendung", dessen Eigenschaften nur geändert werden können, wenn sein "Status" "INITIAL" ist. Dies ist kein großer Workflow, wie Dokumente, die von einer Abteilung zur anderen fließen. Was ich nicht tue, ist, dieses Verhalten in einem objektorientierten Sinne zu reflektieren.
Uylmz
@Reek Application sollte die Lese- / Schreibschnittstelle verfügbar machen und die Iteraktionslogik sollte auf einer höheren Ebene stattfinden. Sowohl der Antragsteller als auch die Personalabteilung verwenden dasselbe Objekt, haben jedoch unterschiedliche Berechtigungen - das Anwendungsobjekt sollte sich darüber keine Gedanken machen. Innere Ausnahmen könnten zum Schutz der Systemintegration verwendet werden, aber ich würde nicht in die Defensive gehen (das Bearbeiten von Kontaktinformationen könnte auch für genehmigte Anwendungen erforderlich sein - benötigen nur eine höhere Zugriffsebene).
Schauder
1

Applicationkönnte eine Schnittstelle sein, und Sie könnten eine Implementierung für jeden der Zustände haben. Die Schnittstelle könnte eine moveToNextState()Methode haben, die die gesamte Workflow-Logik verbirgt.

Für die Bedürfnisse des Kunden könnte es auch eine Methode geben, die direkt zurückgibt, was Sie tun können und nicht (dh eine Reihe von Booleschen Werten), anstatt nur den Status, sodass Sie keine "Checkliste" im Kunden benötigen (ich nehme an der Client soll sowieso ein MVC-Controller oder eine Benutzeroberfläche sein).

Anstatt jedoch eine Ausnahme auszulösen, können Sie einfach nichts tun und den Versuch protokollieren. Dies ist zur Laufzeit sicher, Regeln wurden durchgesetzt und der Client hatte Möglichkeiten, die "Update" -Kontrollen auszublenden.

Bigstones
quelle
1

Ein Ansatz für dieses Problem, der in freier Wildbahn äußerst erfolgreich war, ist Hypermedia - die Darstellung des Zustands der Entität wird von Hypermedia-Steuerelementen begleitet, die die Arten von Übergängen beschreiben, die derzeit zulässig sind. Der Verbraucher fragt die Steuerelemente ab, um herauszufinden, was getan werden kann.

Es handelt sich um eine Zustandsmaschine mit einer Abfrage in der Benutzeroberfläche, mit der Sie ermitteln können, welche Ereignisse Sie auslösen dürfen.

Mit anderen Worten: Wir beschreiben das Web (REST).

Ein anderer Ansatz besteht darin, sich eine Vorstellung von verschiedenen Schnittstellen für verschiedene Zustände zu machen und eine Abfrage bereitzustellen, mit der Sie erkennen können, welche Schnittstellen derzeit verfügbar sind. Denken Sie an IUnknown :: QueryInterface oder Downcasting. Der Client-Code spielt Mutter Mai I mit dem Staat, um herauszufinden, was erlaubt ist.

Es ist im Wesentlichen das gleiche Muster - nur eine Schnittstelle zur Darstellung der Hypermedia-Steuerelemente.

VoiceOfUnreason
quelle
Ich mag das. Es könnte mit dem Zustandsmuster kombiniert werden, um eine Sammlung gültiger Staaten zurückzugeben, in die übergegangen werden könnte. Die Befehlskette kommt mir in gewisser Weise in den Sinn.
RubberDuck
1
Ich vermute, dass Sie nicht "Sammlung gültiger Zustände", sondern "Sammlung gültiger Aktionen" wollen. Denken Sie an ein Diagramm: Sie möchten den aktuellen Knoten (Status) und die Liste der Kanten (Aktionen). Sie finden den nächsten Status heraus, wenn Sie Ihre Aktion auswählen.
VoiceOfUnreason
Ja. Du hast Recht. Eine Sammlung gültiger Aktionen, bei denen es sich bei dieser Aktion tatsächlich um einen Zustandsübergang handelt (oder um etwas, das einen auslöst).
RubberDuck
1

Hier ist ein Beispiel dafür, wie Sie dies aus funktionaler Sicht angehen und wie Sie potenzielle Fallstricke vermeiden können. Ich arbeite in Haskell, von dem ich annehme, dass Sie es nicht wissen, also werde ich es im weiteren Verlauf ausführlich erklären.

data Application = Applied ApplicationDetails |
                   InReview ApplicationDetails |
                   Approved ApplicationDetails |
                   Declined ApplicationDetails

Dies definiert einen Datentyp, der sich in einem von vier Zuständen befinden kann, die Ihren Anwendungsstatus entsprechen. ApplicationDetailswird als vorhandener Typ angenommen, der die detaillierten Informationen enthält.

newtype UpdatableApplication = UpdatableApplication Application

Ein Typalias, der explizit von und nach konvertiert werden muss Application. Dies bedeutet, dass, wenn wir die folgende Funktion definieren, die eine akzeptiert und auspackt UpdatableApplicationund etwas Nützliches damit macht,

updateApplication :: UpdatableApplication -> ApplicationDetails -> Application
updateApplication (UpdatableApplication app) details = ...

Dann müssen wir die Anwendung explizit in eine aktualisierbare Anwendung konvertieren, bevor wir sie verwenden können. Dies geschieht mit folgender Funktion:

findUpdatableApplication :: Application -> Maybe UpdatableApplication
findUpdatableApplication app@(Applied _) = Just (UpdatableApplication app)
findUpdatableApplication _               = Nothing

Hier machen wir drei interessante Dinge:

  • Wir überprüfen den Status der Anwendung (mithilfe des Mustervergleichs, der für diese Art von Code sehr praktisch ist) und
  • Wenn es aktualisiert werden kann, verpacken wir es in eine UpdatableApplication(die nur eine Anmerkung zum Kompilierungstyp der hinzugefügten Typänderung enthält, da Haskell über eine spezielle Funktion verfügt, um diese Art von Tricks auf Typebene auszuführen, die zur Laufzeit nichts kostet). , und
  • Wir geben das Ergebnis in einem "Vielleicht" zurück (ähnlich wie Optionin C # oder Optionalin Java - es ist ein Objekt, das ein Ergebnis umschließt, das möglicherweise fehlt).

Um dies tatsächlich zusammenzustellen, müssen wir diese Funktion aufrufen und, wenn das Ergebnis erfolgreich ist, an die Aktualisierungsfunktion weiterleiten ...

case findUpdatableApplication application of
    Just updatableApplication -> do
        storeApplicationInDatabase (updateApplication updatableApplication)
        showConfirmationPage
    Nothing -> do
        showErrorPage

Da die updateApplicationFunktion das umschlossene Objekt benötigt, können wir nicht vergessen, die Voraussetzungen zu überprüfen. Und da die Vorbedingungsprüfungsfunktion das umschlossene Objekt in einem MaybeObjekt zurückgibt, können wir nicht vergessen, das Ergebnis zu überprüfen und entsprechend zu reagieren, wenn es fehlschlägt.

Nun ... Sie könnten dies in einer objektorientierten Sprache tun. Aber es ist weniger bequem:

  • Keine der OO-Sprachen, die ich ausprobiert habe, verfügt über eine einfache Syntax zum Erstellen eines typsicheren Wrapper-Typs.
  • Es ist auch weniger effizient, da sie zumindest für die meisten Sprachen den Wrapper-Typ nicht entfernen können, da er zur Laufzeit vorhanden und erkennbar sein muss (Haskell hat keine Laufzeit-Typprüfung, alle Typprüfungen sind es zur Kompilierungszeit durchgeführt).
  • Während einige OO-Sprachen Typen haben, die denen entsprechen, haben Maybesie normalerweise keine so bequeme Möglichkeit, die Daten zu extrahieren und gleichzeitig den Pfad zu wählen. Auch hier ist der Mustervergleich sehr nützlich.
Jules
quelle
1

Sie können das Muster «Befehl» verwenden und dann den Invoker bitten, eine Liste der gültigen Funktionen entsprechend dem Status der Empfängerklasse bereitzustellen.

Ich habe dasselbe verwendet, um Funktionen für verschiedene Schnittstellen bereitzustellen, die meinen Code aufrufen sollten. Einige der Optionen waren je nach aktuellem Status des Datensatzes nicht verfügbar. Daher hat mein Aufrufer die Liste aktualisiert und auf diese Weise hat jede GUI den Aufrufer gefragt welche Optionen zur Verfügung standen und sie malten sich entsprechend.

bns
quelle