これまで呼び出し側が $state = $state->insert_coin と状態オブジェクトを付け替えていたのを、VendingMachine クラス(Context)にまとめます。VendingMachine が現在の状態と在庫を属性に持ち、操作を状態オブジェクトに委譲して次の状態を受け取るので、使う側は状態クラスを知らなくて済みます。払い出し中の DispensingState もここで加わります。
すべての状態クラスに insert_coin・select_product・dispense の3つを必ず持たせるため、Moo::Role の requires で VendingMachineState ロールを作り、各クラスに with で適用します。dispense を書き忘れた BrokenState が、呼び出すより前の with の時点でエラーになることと、does でロールを持っているか確かめる方法を見ます。
if/elsif で状態を見分けるのをやめ、WaitingState と CoinInsertedState という状態ごとのクラスを Moo で作ります。各メソッドは自分の状態での振る舞いだけを書き、次の状態のオブジェクトを返す。if/else は消えますが、すべての状態クラスに同じメソッドを持たせる仕組みはまだありません。
第1回の自動販売機に「払い出し中」と「売り切れ」の状態と dispense メソッドを足すと、insert_coin・select_product・dispense のそれぞれに4状態分の if/elsif が並び、在庫のチェックも入れ子になる。状態を1つ足すたびに全部のメソッドを直すことになるこの形を、コードの臭いとして整理します。
Perl と Moo で自動販売機を作りながら、if/else の状態管理を State パターンへ組み直すまでを全10回でたどります。第1回は「待機中」と「コイン投入済み」の2つの状態を state 属性の文字列で持ち、insert_coin と select_product の中を if/elsif で分けて、同じボタンでも状態によって結果が変わる自動販売機を書きます。
Perl でユーザー登録フォームの入力検証を if 文で書き始め、パスワードや年齢のルールを足してネストが深くなったところで、Moo で検証ルールを1つずつクラスに分け、set_next でつなぐ Chain of Responsibility パターンに組み直す全3回の連載の目次です。どこから読むか、困りごとからも選べます。
ネストした validate_user を、Moo の Validator を継承する RequiredValidator や EmailValidator など、1つの検証ルールだけを受け持つクラスに分けます。set_next で順につなぎ、先頭の validate を呼ぶと入力がすべてのバリデータを通ってエラーが1つのハッシュに集まる、Chain of Responsibility パターンの形にします。禁止ワードのチェックは、クラスを1つ作ってチェーンに1つつなげば加えられます。
パスワードの必須と強度、確認用パスワードの一致、利用規約への同意、18歳以上という年齢制限を、第1回の validate_user に if/elsif で足していく。動きはするものの、確認用パスワードのチェックはパスワード強度の else の中にしか書けず、ネストは3段になる。1つの関数に全部の検証が集まったこの状態を、コードの臭いとして5つの問題に整理します。
ユーザー登録フォームの入力検証(バリデーション)を、Perl で全3回かけて組み直します。第1回は名前とメールアドレスの必須チェックと簡易な形式チェックを if 文と早期リターンで書き、最初のエラーで止まる版から、エラーをフィールド名のハッシュに集めてまとめて返す版へ直します。Moo のクラスはまだ出てきません。
Perl と Moo でアドレス帳を CSV・JSON・YAML に書き出すデータエクスポーターを作りながら、形式を足すたびに伸びる if/else を形式ごとのクラスとロールに分け、実行中に差し替えられるようにし、最後に Strategy パターンだと名前を付ける全10回の連載の目次です。どこから読むか、困りごとからも選べます。
9回かけて作ったデータエクスポーターは、GoF の Strategy パターンの実装だった。ExporterRole、CsvExporter などのエクスポーター、DataExporter を Strategy・ConcreteStrategy・Context に当てはめ、XML を足すときに既存のエクスポーターへ手を入れずに済むことを SOLID 原則とつなげて、最初の if/else 版と比べます。
ExporterRole、CsvExporter と JsonExporter、DataExporter、exporter_for がそろった。YamlExporter を作って %exporter_map に yaml を足し、対応形式を返す supported_formats と --help を加えた完成版を1本のスクリプトにまとめ、csv・json・yaml の3形式で実行して出力を確かめます。第1回の if/else とも並べて比べます。
形式ごとの出力はクラスに分けたが、メイン処理には形式名を見てどのエクスポーターを new するか選ぶ if/else がまだ残っている。形式名とクラス名を対応させたハッシュ %exporter_map を DataExporter に持たせ、形式名から new して返すクラスメソッド exporter_for を作って、メイン処理から if/else を消します。
exporter を rw にしたので、エクスポーターではない文字列も入れられてしまい、エラーは export_data で export を呼んだときにやっと出る。exporter 属性に、ExporterRole を持つかを does で確かめる isa を付け、new のときも差し替えのときも、間違った値を入れた時点で isa check failed のエラーで止まるのを確かめます。
DataExporter の exporter 属性は読み取り専用(ro)なので、作ったあとでエクスポーターを替えられない。rw にして exporter(JsonExporter->new) で差し替え、同じ DataExporter で CSV と JSON を続けて出力します。代わりに、エクスポーターではない文字列まで入ってしまう問題が残ります。