「オブジェクトが特定の状態にある場合にのみ許可されるオブジェクトの操作」の設計パターン


8

例えば:

まだレビューまたは承認されていない求人応募のみを更新できます。言い換えれば、人は、HRがレビューを開始するまで、またはすでに承認されるまで、ジョブアプライアンスフォームを更新できます。

したがって、求人応募は次の4つの状態になります。

APPLIED(初期状態)、IN_REVIEW、APPROVED、DECLINED

どうすればこのような動作を実現できますか?

確かに、Applicationクラスにupdate()メソッドを記述し、アプリケーションの状態を確認し、アプリケーションが必要な状態でない場合は何もしないか、例外をスローすることができます

しかし、この種のコードは、そのようなルールが存在することを明らかにしていません。それにより、だれでもupdate()メソッドを呼び出すことができ、クライアントが失敗した後にのみ、そのような操作が許可されなかったことがわかります。したがって、クライアントはそのような試みが失敗する可能性があることを認識する必要があるため、注意が必要です。クライアントがそのようなことを認識していることは、ロジックが外部にリークしていることも意味します。

状態ごとに異なるクラス(ApprovedApplicationなど)を作成して、許可されたクラスにのみ許可された操作を実行してみましたが、この種のアプローチも間違っているように感じます。

そのような振る舞いを実装するための公式の設計パターン、または単純なコードはありますか?


7
これらは一般にStateMachinesと呼ばれ、それらの実装は、要件や使用している言語によって多少異なります。
Telastyn 2015

そして、適切な方法が適切な州で利用可能であることをどのように保証しますか?
uylmz

1
言語に依存します。異なるクラスは一般的な言語の一般的な実装ですが、「正しい状態でない場合はスローする」がおそらく最も一般的です。
Telastyn

1
「canUpdate」メソッドを含め、Updateを呼び出す前にそれをチェックする際の問題はどこにありますか?
陶酔の

1
this kind of code does not make it obvious such a rule exists-これがコードにドキュメントがある理由です。作家良いコードが陶酔のアドバイスを取り、それをハードウェアをしようとする前に、外部のテストにルールをできるようにする方法を提供します。
Blrfl

回答:


4

このような状況はかなり頻繁に発生します。たとえば、ファイルは開いているときにのみ操作できます。ファイルが閉じられた後でファイルを操作しようとすると、ランタイム例外が発生します。

コンパイル時のエラーは実行時のエラーよりも常に望ましいので、間違ったことが起こらないようにするために言語の型システムを使用たいという(前の質問で表された)希望は高貴です。ただし、このタイプの状況で私が知っているデザインパターンはありません。おそらく、解決するよりも多くの問題が発生するためです。(それは非現実的でしょう。)

私が知っているあなたの状況に最も近いのは、追加のインターフェイスを介してさまざまな機能に対応するオブジェクトのさまざまな状態をモデル化することですが、この方法では、ランタイムエラーが発生する可能性のあるコードの場所数を減らすだけです。実行時エラーの可能性を排除していません。

したがって、あなたの状況では、さまざまな状態のオブジェクトで何ができるかを記述する多くのインターフェイスを宣言し、オブジェクトは状態遷移時に正しいインターフェイスへの参照を返します。

したがって、たとえば、approve()クラスのメソッドはApprovedApplicationインターフェイスを返します。インターフェースは(ネストされたクラスを介して)非公開で実装されるため、への参照のみを持つコードApplicationは、ApprovedApplicationメソッドを呼び出すことができません。次に、承認されたアプリケーションを操作するコードは、でのApprovedApplication作業を要求することにより、コンパイル時にそうする意図を明示的に示します。ただし、もちろん、このインターフェイスをどこかに保存し、decline()メソッドが呼び出された後にこのインターフェイスの使用を続行した場合でも、ランタイムエラーが発生します。私はあなたの問題に完璧な解決策があるとは思いません。


補足として、それはapplication.approve(someoneWhoCanApprove)またはsomeoneWhoCanApprove.approve(application)のどちらにすべきですか?「誰か」が必要な調整を行うためにアプリケーションのフィールドにアクセスできない可能性があるので、それが最初であると思います
uylmz

よくわかりませんが、どちらも正しくない可能性についても検討する必要があります。つまり、関心の分離if( someone.hasApprovalPermission( application ) ) { application.approve(); } の原則は、アプリケーションも誰かも、権限とセキュリティに関する決定を行うことに関与すべきではないことを示しています。
Mike Nakis、2015

3

私はさまざまな答えのさまざまなビットに頭をうなずいていますが、OPにはまだフロー制御の懸念があるようです。言葉で合体しようとするには多すぎる。いくつかのコードを修正するつもりです-状態パターン。


過去形としての州名

「In_Review」はおそらく状態ではなく、遷移またはプロセスです。それ以外の場合、州名は一貫している必要があります:「適用中」、「承認中」、「拒否中」など。または「確認済み」。か否か。

適用済み状態は、レビュー遷移を行い、状態をレビュー済みに設定します。レビュー済みの状態は、承認の移行を行い、状態を承認済み(または拒否)に設定します。


// 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 {}

編集-エラー処理コメント

最近のコメント:

...すでに他の人に発行されている本を貸し出そうとしている場合、Bookモデルには、状態が変化しないようにするロジックが含まれています。これは、戻り値(例:ブール値で成功したyay / nay、またはステータスコード)、例外(例:IllegalStateChangeException)、またはその他の手段による可能性があります。選択した手段に関係なく、この側面はこの(または任意の)回答の一部としてカバーされていません。

そして元の質問から:

しかし、この種のコードは、そのようなルールが存在することを明らかにしていません。それにより、だれでもupdate()メソッドを呼び出すことができ、クライアントが失敗した後にのみ、そのような操作が許可されなかったことがわかります。

やるべき設計作業はまだあります。ありませんUnified Field Theory Pattern。混乱は、状態遷移フレームワークが一般的なアプリケーション機能とエラー処理を行うと想定することから生じます。それは間違っていると感じています。示されている答え、状態変化を制御するために設計されています。


確かに、Applicationクラスにupdate()メソッドを記述し、アプリケーションの状態を確認し、アプリケーションが必要な状態でない場合は何もしないか、例外をスローすることができます

これは、ここで機能する3つの機能があることを示唆しています:状態、更新、および2つの相互作用。この場合Application、私が書いたコードではありません。現在の状態を判断するために使用する場合があります。どちらでもApplicationありませんapplicationPaperworkApplication2つの相互作用ではありませんが、一般的なStateContextEvaluatorクラスである可能性があります。これで、Applicationこれらのコンポーネントの相互作用を調整し、それに応じてエラーメッセージを出力するように動作します。

編集を終了


何か不足していますか?これにより、状態に関係なく、apply()呼び出しがすでに適用されているなどの理由で失敗した呼び出しメソッドへの通信にこの設定がどのように使用されるかについてのヒントなしに、4つのメソッドすべてを呼び出すことができるようです。
kwah

1
状態に関係なく、4つのメソッドすべての呼び出しを許可します はい。ちがいない。この設定が呼び出しメソッドとの通信にどのように使用されるかについてのヒントなしApplication例外がスロー されるコンストラクターのコメントを参照してください。たぶん電話をかける AppliedState.Approve()と、「承認される前にアプリケーションを確認する必要があります」というメッセージが表示される場合があります。
radarbob

1
... たとえば、すでに適用されているため、apply()コールは失敗しました。それは間違った考えです。呼び出しは成功しました。ただし、州によって動作は異なります。それが状態パターンです……しかし、プログラマーはどの振る舞いが適切であるかを決定しなければなりません。しかし、「OMGはエラーです!!!私たちはアポトーシスを起こし、プログラムを中止する必要があります!」という考えは間違っています。私は期待してAppliedState.apply()優しくアプリケーションが既に提出されており、審査を待っていることをユーザに思い出させるでしょう。そして、プログラムは継続します。
radarbob

状態パターンがモデルとして使用されていると想定すると、「障害」をユーザーインターフェイスに通知する必要があります。たとえば、すでに他の人に発行されている本を貸し出そうとしている場合、Bookモデルには、状態が変化しないようにするロジックが含まれています。これは、戻り値(例:ブール値で成功したyay / nay、またはステータスコード)、例外(例:IllegalStateChangeException)、またはその他の手段による可能性があります。選択した手段に関係なく、この側面はこの(または任意の)回答の一部としてカバーされていません。
kwah

誰かがそれを言った神に感謝します。「オブジェクトの状態に基づいて異なる動作が必要です。...はい、はい。状態パターンが必要です。」++古い豆。
RubberDuck 2016年

1

一般的に、あなたが説明しているのはワークフローです。 より具体的には、REVIEWED APPROVEDまたはDECLINEDなどの状態によって具体化されるビジネス機能は、「ビジネスルール」または「ビジネスロジック」の見出しに該当します。

ただし、明確にするために、ビジネスルールを例外にエンコードしないでください。 これを行うには、プログラムフロー制御に例外を使用することになります。これを行わない方がよい理由はたくさんあります。例外は例外的な状況で使用する必要があり、アプリケーションのINVALID状態はビジネスの観点からはまったく例外ではありません。

プログラムがユーザーの介入なしにエラー状態から回復できない場合(たとえば、「ファイルが見つかりません」)は、例外を使用します。

ビジネスロジックを記述するための特定のパターンはありませんが、ビジネスデータ処理システムを配置し、プロセスを実装するためのコードを記述するための通常の手法はあります。ビジネスルールとワークフローが複雑な場合は、なんらかのワークフローサーバーまたはビジネスルールエンジンの使用を検討してください。

いずれの場合も、状態REVIEW、APPROVED、DECLINEDなどは、クラスの列挙型プライベート変数で表すことができます。getter / setterメソッドを使用する場合、最初にenum変数の値を調べることにより、setterが変更を許可するかどうかを制御できます。列挙型の値が誤った状態にあるときに、誰かがセッターに書き込もうとした場合、その後、あなたは例外をスローすることができます。


「アプリケーション」と呼ばれるオブジェクトがあり、その「状態」が「初期」である場合にのみプロパティを変更できます。これは、ある部門から別の部門に流れるドキュメントのような大きなワークフローではありません。私が失敗しているのは、この動作をオブジェクト指向の意味で反映することです。
uylmz

@Reekアプリケーションは読み取り/書き込みインターフェースを公開する必要があり、反復ロジックはより高いレベルで行われる必要があります。申請者とHRの両方が同じオブジェクトを使用しますが、特権は異なります-アプリケーションオブジェクトはそれについて心配する必要はありません。内部の例外はシステム統合を保護するために使用される可能性がありますが、私は防御に行くつもりはありません(連絡先情報の編集は、承認されたアプリケーションであっても必要になる可能性があります-より高いアクセスレベルが必要です)。
2015

1

Applicationインターフェースにすることができ、各状態の実装を持つことができます。インターフェイスにはmoveToNextState()メソッドを含めることができ、これによりすべてのワークフローロジックが非表示になります。

クライアントのニーズのために、状態だけでなく、できることとできないこと(つまりブール値のセット)を直接返すメソッドもあるので、クライアントに「チェックリスト」は必要ありません(私はクライアントはMVCコントローラーまたはUIになります)。

ただし、例外をスローする代わりに、何もせずに試行をログに記録することができます。これは実行時に安全であり、ルールが適用され、クライアントには「更新」コントロールを非表示にする方法がありました。


1

実際に非常に成功しているこの問題への1つのアプローチはハイパーメディアです。エンティティの状態の表現には、現在許可されている遷移の種類を説明するハイパーメディアコントロールが伴います。消費者はコントロールに問い合わせて、何ができるかを発見します。

これはステートマシンであり、インターフェイスにクエリがあり、起動を許可されているイベントを検出できます。

つまり、Web(REST)を記述しています。

もう1つのアプローチは、さまざまな状態のさまざまなインターフェイスについて考え、現在利用可能なインターフェイスを検出できるクエリを提供することです。IUnknown :: QueryInterfaceまたはダウンキャストを考えてください。クライアントコードは、許可されているものを見つけるための状態でマザーメイIを再生します。

これは基本的に同じパターンです-ハイパーメディアコントロールを表すためにインターフェイスを使用するだけです。


私はこれが好き。Stateパターンと組み合わせて、遷移可能な有効な状態のコレクションを返すことができます。指揮系統はある意味で頭に浮かぶ。
RubberDuck、2016年

1
私の推測では、「有効な状態のコレクション」ではなく「有効なアクションのコレクション」は必要ではないでしょう。グラフを考えてみましょう。現在のノード(状態)とエッジのリスト(アクション)が必要です。アクションを選択すると、次の状態がわかります。
VoiceOfUnreason 2016年

はい。あなたは正しいです。そのアクションが実際には状態遷移(またはそれをトリガーするもの)である有効なアクションのコレクション。
RubberDuck

1

これは、機能的な観点からこれにアプローチする方法の例と、潜在的な落とし穴を回避するのにどのように役立つかを示しています。私はHaskellで働いていますが、あなたが知らないと想定しているので、詳しく説明しながら説明します。

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

これは、アプリケーションの状態に対応する4つの状態のいずれかになるデータ型を定義します。 ApplicationDetails詳細情報を含む既存のタイプであると想定されます。

newtype UpdatableApplication = UpdatableApplication Application

との間の明示的な変換が必要なタイプエイリアスApplication。つまり、次の関数を定義し、それを受け入れてアンラップし、UpdatableApplicationそれを使用して何か便利なことを行うと、

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

次に、アプリケーションを使用する前に、明示的にアプリケーションをUpdatableApplicationに変換する必要があります。これは、次の関数を使用して行われます。

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

ここでは、3つの興味深いことを行います。

  • アプリケーションの状態をチェックします(この種のコードに非常に便利なパターンマッチングを使用)。
  • 更新できる場合は、それをラップしますUpdatableApplication(これには、追加される型の変更のコンパイル型のメモのみが含まれます。Haskellには、このような型レベルのトリッカーキーを実行する特定の機能があるため、実行時に何もかかりません) 、および
  • 「Maybe」で結果を返します(OptionC#またはOptionalJava と同様です。欠落している可能性のある結果をラップするオブジェクトです)。

これを実際に組み合わせるには、この関数を呼び出し、結果が成功した場合は、更新関数に渡す必要があります...

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

updateApplication関数にはラップされたオブジェクトが必要なため、前提条件を確認することを忘れないでください。また、前提条件チェック関数はオブジェクト内のラップされたMaybeオブジェクトを返すため、結果をチェックし、失敗した場合もそれに応じて応答することを忘れることはできません。

これで、オブジェクト指向言語でこれを行うことができます。しかし、それほど便利ではありません。

  • 私が試したオブジェクト指向言語には、タイプセーフなラッパー型を作成するための単純な構文がないため、これは定型です。
  • また、少なくともほとんどの言語では、ラッパータイプを削除することができないため、効率が低下します。これは、ラッパータイプが存在し、実行時に検出可能である必要があるためです(Haskellにはランタイムタイプチェックがありません。すべてのタイプチェックはコンパイル時に実行されます)。
  • 一部のオブジェクト指向言語には同等のタイプMaybeがありますが、通常、データを抽出して同時に取得するパスを選択する便利な方法がありません。ここでもパターンマッチングは非常に役立ちます。

1

«command»パターンを使用して、受信者クラスの状態に応じて有効な関数のリストを提供するように起動者に依頼することができます。

コードを呼び出すことになっているさまざまなインターフェイスに機能を提供するために同じものを使用しましたが、レコードの現在の状態によっては一部のオプションが使用できなかったため、呼び出し元がリストを更新し、すべてのGUIが呼び出し元に問い合わせましたどのオプションが利用可能であり、それらはそれに応じて自分自身を描いた。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.