Commandパターンの例
皆さんのシステムには下記のリスト1に示すservice
のようなメソッドがありますか? このようなメソッドを、私は多くのシステムで目にしてきました。なぜこうなるのかは知りませんが、しばしばこういったメソッドのコードを変更したり、新しい種類のリクエストのサポートを追加したりする羽目になります。変更する際にはコードを壊さないように、メソッドの動作を単体テストで確認するようにしています。
しかし、リスト1のようなコードのテストを作成するには一体どこから手をつけたらよいでしょうか。
public class LibrarySystem { ... public void service(Request request) { String command = request.getParameter("command"); if (command.equals("return")) { String barCode = request.getParameter("barCode"); Date date = new Date(); String branchName = request.getParameter("branch"); Branch branch = findBranch(branchName); checkIn(barCode, date, branch); } else if (command.equals("checkout")) { String barCode = request.getParameter("barCode"); Date date = new Date(); String patronId = request.getParameter("id"); checkOut(patronId, barCode, date); } else if (command.equals("newBranch")) { String branchName = request.getParameter("branch"); addBranch(branchName); } else if (command.equals("addBook")) { String branchName = request.getParameter("branch"); String author = request.getParameter("author"); String title = request.getParameter("title"); String classification = request.getParameter("classification"); String year = request.getParameter("year"); Book book = new Book(author, title, classification, year); String copyNumberParm = request.getParameter("copyNumber"); int copyNumber = 1; if (copyNumberParm != null) { copyNumber = Integer.parseInt(copyNumberParm); } addNew(branchName, book, copyNumber); } else if (command.equals("addPatron")) { String name = request.getParameter("name"); String id = request.getParameter("id"); Patron patron = new Patron(name, id); add(patron); } } public void add(Patron patron) { ... } ...
switch
ステートメントやif/else
ステートメントがあるときはポリモーフィズムを採用するチャンスなのですが、今回はそうではなく、テスト駆動型のアプローチに基づき、単体テストに適した形に変更しながら、コードをゆっくりと確実にリファクタリングしていくことにします。
まずはaddPatron
コマンドから始めましょう。このコードの最初の単体テストをリスト2に示します。このテストではまさに、コードをスタンドアロンユニットとして切り離すことを試みています。具体的には、service
メソッド内のコードによって、LibrarySystemのadd(Patron)
メソッドが適切な値を含むPatron引数を使って呼び出されることを確認します。最終的には、この方法でテストすることが最適とはおそらく言えないでしょうが、とりあえずはこれで良しとします。
public class LibrarySystemTest { @Test public void addPatron() { final List<Patron> added = new ArrayList<Patron>(); LibrarySystem system = new LibrarySystem() { @Override public void add(Patron patron) { added.add(patron); } }; Request request = new Request(); request.setParameter("command", "addPatron"); request.setParameter("name", "patronName"); request.setParameter("id", "patronId"); system.service(request); assertEquals(1, added.size()); assertEquals("patronName", added.get(0).getName()); assertEquals("patronId", added.get(0).getId()); } }
単体テストがうまく機能することがわかったら、コードを壊していないことを慎重に確認しながら、service
メソッド内のaddCommand
のコードを変更します。変更後のelse
以降のコードは以下のようになります。
} else if (command.equals("addPatron")) { AddPatronCommand addPatron = new AddPatronCommand(request, this); addPatron.execute(); }
AddPatronCommandクラスは、一部のコードを新しいクラスに移動して作成したものです(リスト3を参照)。一時的に、LibrarySystemの参照をこのクラスのコンストラクタに渡しました。その結果、LibrarySystemとAddPatronCommandが互いに相手の存在を認識するという密結合の関係になってしまいました。これは望ましい状況ではないので、コードをチェックインするまでに何とか解決しようと思います。
public class AddPatronCommand { private String name; private String id; private final LibrarySystem system; public AddPatronCommand(Request request, LibrarySystem system) { this.system = system; name = request.getParameter("name"); id = request.getParameter("id"); } public void execute() { Patron patron = new Patron(name, id); system.add(patron); } }