(jp) =
状態パターンは、モデルをクリーンに保ちながら、状態固有の動作をモデルに追加するための最良の方法の 1 つです。
この章では、状態パターンについて、特にそれをモデルに適用する方法について説明します。 この章は、第 4 章の延長として考えることができます。第 4 章では、モデル クラスがビジネス ロジックを処理できないようにすることで、モデル クラスを管理可能に保つ方法について説明しました。
ビジネス ロジックをモデルから遠ざけると、モデルの状態をどうするかという非常に一般的なユース ケースで問題が生じます。
請求書は保留中または支払済である可能性があり、支払いは失敗または成功している可能性があります。 状態に応じて、モデルは異なる動作をする必要があります。 モデルとビジネス ロジックの間のこのギャップをどのように埋めるのでしょうか?
状態とそれらの間の遷移は、大規模なプロジェクトで頻繁に使用されるケースです。 非常に頻繁に、彼らは自分自身で章に値する.
# 状態パターン
本質的に、状態パターンは単純なパターンですが、非常に強力な機能を可能にします。 請求書の例をもう一度見てみましょう。請求書は保留中または支払い済みの場合があります。 まず、非常に簡単な例を示します。これは、状態パターンによってどのように多くの柔軟性が得られるかを理解してもらいたいからです。
請求書の概要にその請求書の状態を表すバッジが表示され、保留中はオレンジ色、支払い済みは緑色で表示されるとします。
単純な脂肪モデルのアプローチは、次のようになります。
class Invoice extends Model
public function getStateColour(): string
if ($this->state->equals(InvoiceState::PENDING()))
return 'orange';
if ($this->state->equals(InvoiceState::PAID()))
return 'green';
return 'gray';
状態値を表すためにある種の列挙型クラスを使用しているため、次のように改善できます。
class Invoice extends Model
public function getStateColour(): string
return $this->state->getColour();
class InvoiceState extends Enum
private const PENDING = 'pending';
private const PAID = 'paid';
public function getColour(): string
if ($this->value === self::PENDING)
return 'orange';
if ($this->value === self::PAID)
return 'green'
return 'gray';
補足として、この場合は myclabs/php-enum パッケージを使用していると思います。 もう1つの改善点として、配列を使用して上記を少し短く書くことができます。
class InvoiceState extends Enum
public function getColour(): string
return [
self::PENDING => 'orange',
self::PAID => 'green',
][$this->value] ?? 'gray';
どのアプローチを好むにせよ、本質的には、利用可能なすべてのオプションをリストし、そのうちの 1 つが現在のオプションと一致するかどうかを確認し、その結果に基づいて何かを行うことになります。 これは、お好みのシンタックス シュガーに関係なく、大きな if/else ステートメントです。
このアプローチを使用して、責任をモデルまたは列挙型クラスに追加します。 それ 特定の状態が何をすべきかを知る必要があり、 それ 状態がどのように機能するかを知る必要があります。 状態パターンはこれを逆にします。「状態」をコードベースの第一級市民として扱います。 すべての状態は個別のクラスで表され、これらのクラスはそれぞれ 行動する 主題に。
把握するのは難しいですか? 一歩一歩見ていきましょう。
抽象クラスから始めます InvoiceState
、このクラスは具体的な請求書の状態が提供できるすべての機能を記述します。 私たちの場合、状態が色を提供することを望みます。
abstract class InvoiceState
abstract public function colour(): string;
次に、具体的な状態を表す 2 つのクラスを作成します。
class PendingInvoiceState extends InvoiceState
public function colour(): string
return 'orange';
class PaidInvoiceState extends InvoiceState
public function colour(): string
return 'green';
最初に気付くのは、これらの各クラスは、単独で簡単に単体テストできることです。
class InvoiceStateTest extends TestCase
public function the_colour_of_pending_is_orange
$state = new PendingInvoiceState();
$this->assertEquals('orange', $state->colour());
次に、色はパターンを説明するために使用される単純な例であることに注意してください。 状態によってカプセル化された、より複雑なビジネス ロジックを使用することもできます。 この例を見てみましょう: 請求書を支払う必要がありますか? もちろん、これはすでに支払われているかどうかにかかわらず、州によって異なりますが、扱っている請求書の種類によっても異なります。 私たちのシステムが、支払う必要のない貸方票をサポートしている、または価格が 0 の請求書を許可しているとします。このビジネス ロジックは、状態クラスによってカプセル化できます。
ただし、この機能を機能させるために欠けていることが 1 つあります。請求書の支払いが必要かどうかを判断するには、状態クラス内からモデルを参照できる必要があります。 これが、私たちがアブストラクトを持っている理由です InvoiceState
親クラス; そこに必要なメソッドを追加しましょう。
abstract class InvoiceState
protected $invoice;
public function __construct(Invoice $invoice)
abstract public function mustBePaid(): bool;
そして、具体的な状態ごとに実装します。
class PendingInvoiceState extends InvoiceState
public function mustBePaid(): bool
return $this->invoice->total_price > 0
&& $this->invoice->type->equals(InvoiceType::DEBIT());
class PaidInvoiceState extends InvoiceState
public function mustBePaid(): bool
return false;
ここでも、各州の単純な単体テストを作成できます。請求書モデルはこれを簡単に実行できます。
class Invoice extends Model
public function getStateAttribute(): InvoiceState
return new $this->state_class($this);
public function mustBePaid(): bool
return $this->state->mustBePaid();
最後に、データベースで具体的なモデル状態クラスを保存できます。 state_class
フィールドで完了です。 明らかに、このマッピングを手動で行う (データベースへの保存とデータベースへのロード) のは、すぐに面倒になります。 そのため、面倒な作業をすべて処理するパッケージを作成しました。
ただし、状態固有の動作、つまり「状態パターン」は解決策の半分にすぎません。 請求書の状態をある状態から別の状態に移行し、特定の状態のみが他の状態に移行できるようにする必要があります。 それでは、状態遷移を見てみましょう。
# トランジション
ビジネス ロジックをモデルから遠ざけ、データベースから実行可能な方法でのみデータを提供できるようにすることについて話したことを覚えていますか? 状態と遷移にも同じ考え方を適用できます。 データベースに変更を加える、メールを送信するなど、状態を使用する際の副作用を避ける必要があります。 読んだ またはデータを提供します。 一方、トランジションは何も提供しません。 むしろ、許容可能な副作用につながる、モデルの状態がある状態から別の状態に正しく移行されるようにします。
これら 2 つの懸念事項を別々のクラスに分割すると、私が何度も書いたのと同じ利点が得られます。それは、テスト容易性の向上と認知負荷の軽減です。 クラスに責任を 1 つだけ持たせることで、複雑な問題をいくつかの把握しやすいビットに分割しやすくなります。
したがって、トランジション: モデル (この場合は請求書) を受け取り、その請求書の状態 (許可されている場合) を別の状態に変更するクラスです。 場合によっては、ログ メッセージの書き込みや状態遷移に関する通知の送信など、小さな副作用が生じることがあります。 単純な実装は次のようになります。
class PendingToPaidTransition
public function __invoke(Invoice $invoice): Invoice
if (! $invoice->mustBePaid())
throw new InvalidTransitionException(self::class, $invoice);
$invoice->status_class = PaidInvoiceState::class;
$invoice->save();
History::log($invoice, "Pending to Paid");
繰り返しますが、この基本パターンでできることはたくさんあります。
- モデルで許可されているすべての遷移を定義する
- 内部で遷移クラスを使用して、状態を別の状態に直接遷移させる
- 一連のパラメーターに基づいて、どの状態に遷移するかを自動的に決定します
前述のパッケージでも、トランジションのサポートと基本的なトランジション管理が追加されています。 ただし、複雑なステート マシンが必要な場合は、他のパッケージを検討することをお勧めします。 以下の脚注に例を挙げました。
# 遷移のない状態
「状態」について考えるとき、遷移なしには存在できないと考えることがよくあります。 ただし、これは正しくありません。オブジェクトは決して変化しない状態を持つことができ、遷移は状態パターンを適用する必要はありません。 何でこれが大切ですか? さて、私たちをもう一度見てください PendingInvoiceState::mustBePaid
実装:
class PendingInvoiceState extends InvoiceState
public function mustBePaid(): bool
return $this->invoice->total_price > 0
&& $this->invoice->type->equals(InvoiceType::DEBIT());
状態パターンを使用して、コード内の脆弱な if/else ブロックを削減したいので、これでどこに行くのか推測できますか? あなたはそれを考慮しましたか $this->invoice->type->equals(InvoiceType::DEBIT())
実際には変装したif文ですか?
InvoiceType
実際、状態パターンも非常にうまく適用できます。 これは、特定のオブジェクトに対して変更されることのない単純な状態です。 これを見てください。
abstract class InvoiceType
protected $invoice;
abstract public function mustBePaid(): bool;
class CreditInvoiceType extends InvoiceType
public function mustBePaid(): bool
return false
class DebitInvoiceType extends InvoiceType
public function mustBePaid(): bool
return true;
これでリファクタリングできます PendingInvoiceState::mustBePaid
そのようです。
class PendingInvoiceState extends InvoiceState
public function mustBePaid(): bool
return $this->invoice->total_price > 0
&& $this->invoice->type->mustBePaid();
コード内の if/else ステートメントを減らすことで、そのコードをより直線的にすることができ、推論が容易になります。 この正確なトピックに関する Sandi Metz の講演を読むことを強くお勧めします。
私の意見では、状態パターンは素晴らしいものです。 巨大な if/else ステートメントを書くのにもう行き詰まることがなくなります — 実際には 2 つ以上の請求書の状態が存在することがよくあります — これにより、クリーンでテスト可能なコードが可能になります。
これは、既存のコード ベースに段階的に導入できるパターンであり、長期的にプロジェクトを保守可能に保つのに非常に役立つと確信しています。