メイン処理がエクスポーターを選んで export を直接呼んでいると、同じ出力を別の場所でも使いたいときに、そのコードを写すことになる。exporter 属性にエクスポーターを持ち、export_data で export に処理を任せる DataExporter を Moo で作り、使う側が DataExporter だけを相手にすればよい形にします。
CsvExporter と JsonExporter が同じ export メソッドを持っているのは偶然で、次に作る人が output と名付けても誰も止めない。requires で export を要求する ExporterRole を作って with で適用し、export の無いクラスはプログラムの起動時に missing export のエラーで止まるのを確かめます。
CSV・JSON・YAML・XML の出力処理が1つのスクリプトに並んでいると、1つの形式を直すだけでも他を壊さないかが気になる。データを受け取って文字列を返す export メソッドを持つ CsvExporter と JsonExporter を Moo で作り、出力の中身をクラスへ移します。どのクラスを使うか選ぶ if/else は、まだメインに残ります。
YAML と XML でも出力したいと言われて elsif を2つ足すと、4形式の出力処理が1つの if/else に並ぶ。CSV を直したいだけでも他の形式と同じ場所を触り、形式を足すたびに動いている部分へ手を入れることになる。長いメソッドと分岐の多さというコードの臭いとして、4つの問題を整理します。
アドレス帳を CSV・JSON・YAML で書き出すデータエクスポーターを、Perl と Moo で全10回かけて作ります。第1回は3人分のアドレス帳をハッシュの配列で用意し、コマンドライン引数の形式名を if/elsif で見て、CSV は say で1行ずつ、JSON は JSON::PP で出力するスクリプトから始めます。Moo のクラスはまだ出てきません。
Perl と Moo で簡易テキストエディタを作りながら、1回しか戻せない Undo を操作オブジェクトで何度でも戻せるようにし、Redo とマクロを足して、最後に Command パターンだと名前を付ける全10回の連載の目次です。どこから読むか、困りごとからも選べます。
9回かけて作ったエディタは、GoF の Command パターンの実装だった。Command::Role、InsertCommand などのコマンド、History、Editor、メインスクリプトを Command・ConcreteCommand・Invoker・Receiver・Client に当てはめ、CLI ツールやジョブキューといったエディタ以外の使いどころと、Strategy パターンとの違いを整理します。
Editor、InsertCommand・DeleteCommand、Command::Role、History、MacroCommand を1本のスクリプトにまとめ、i(挿入)・d(削除)・u(undo)・r(redo)・p(表示)・q(終了)を受け付ける対話ループを書きます。範囲外の位置や空のスタックでは、エラーメッセージを出して止まらないようにします。
「foo」を「bar」に3か所置換すると、Undo 1回では1か所しか戻らない。子コマンドを配列に持ち、execute で順に実行して undo で逆順に取り消す MacroCommand を作り、1回の Undo・Redo で一括置換を戻します。逆順でないと壊れる理由と、マクロの中にマクロを入れられる Composite の形も見ます。
Undo で取り消した「Hello」が、やっぱり必要だった。redo_stack から pop して execute を呼び、undo_stack に戻す redo を History に足し、undo と対になる2つのスタックの間のコマンドの移動を表で確かめます。Undo の後に新しい操作をすると Redo の履歴が消えることも確認します。
@history への push と pop がメイン処理のあちこちに書かれていると、Redo 用にもう1つスタックを足すのが難しい。undo_stack と redo_stack を持つ History クラスを作り、実行して積む execute_command(新しい操作で redo_stack を空にする)と、取り出して戻す undo をまとめます。
InsertCommand と DeleteCommand が execute と undo を持っているのはたまたまで、新しいコマンドで undo を書き忘れても Perl は何も言わない。requires で2つのメソッドを要求する Command::Role を作って with で適用し、undo の無いクラスでは with の時点で missing undo のエラーになるのを確かめます。
履歴の配列に InsertCommand を積んでも、それを打ち消す手段がなければ Undo はできない。挿入の逆は削除、削除の逆は挿入という対で undo を書き、消す前の文字列を _deleted_string に取っておく DeleteCommand を足して、履歴から pop して undo を呼ぶ複数回の Undo を動かします。
複数回 Undo するために操作のたびにテキスト全体を配列へ保存すると、1万文字に1文字足すたびに1万文字をコピーすることになる。挿入位置と文字列と対象の Editor を持ち、execute で挿入する InsertCommand を作り、操作オブジェクトを配列に積んで履歴にします。
誤って消した文字を戻したい。操作の直前のテキストを previous_text に保存して undo で戻す方法を Editor に足すと、1回目は戻るのに、2回目の undo では「AB」のまま止まる。操作ごとの text と previous_text を表で追い、すべての操作の履歴が要ることを確かめます。