Guiceへのリファクタリング
ヒドイコード
こういうのどこにでもあるでしょ!静的なユーティリティメソッドがいろんなものに依存していて、それもまた静的ときたもんだ。
public class PizzaUtilities { private static final int TIME_TO_PREPARE = 6; private static final int MAX_DISTANCE = 20; public static Order createOrder(List<PizzaSpec> pizzas, Customer customer) { /** 訳注:配達すべき方向を取得する */ Directions directions = Geography.getDirections( PizzaStore.getStoreAddress(), customer.getDeliveryAddress()); /** 訳注:方向がわかんない、あるいは遠すぎる場合は例外 */ if (directions == null || directions.getLengthInKm() > MAX_DISTANCE) { throw new InvalidOrderException("Cannot deliver to , " + customer.getDeliveryAddress()); } /** 訳注:到着時刻を計算する。料理時間、オーブンの時間、配達地までの時間 */ int arrivalTime = TIME_TO_PREPARE + Oven.getCurrentOven().schedule(TIME_TO_PREPARE, pizzas) + directions.estimateTravelTime(); /** 訳注:納品書を作成 */ Invoice invoice = Invoice.create(pizzas, directions.getLengthInKm()); /** 訳注:注文オブジェクトを作成 */ return new Order(pizzas, invoice, arrivalTime, customer, directions); } }
なんでこれが気に入らないのか?
Geography, PizzaStore, Oven, Invoiceクラスに静的に依存しているもんで、こいつらもひっくるめてテストをやらなきゃならない。結果として、
PizzaStoreの初期化が遅かったりすると、テストも待たされてしまう。
- このメソッドをテストするためには、Geograpyのコードが必要だ。もし別のチームがそれを作っている最中だとしても。
- テストのsetUp()の中で、Oven.setCurrentOven()をセットすることを覚えておかければ。そうしないとテストは失敗してしまう。
- もし、Invoice.create()が外部サービス、例えば支払処理サービスなんかに依存してたりすると、そのサービスがなきゃテストできないじゃん。
同じメソッドの静的でないバージョン
静的でないバージョンを作ってみよう。
public class PizzaServices { public Order createOrder(List<PizzaSpec> pizzas, Customer customer) { return PizzaUtilities.createOrder(pizzas, customer); } }
静的な呼び出しを静的でない呼び出しに置き換える
PizzaUtilitiesを呼び出すコードを、PizzaServicesの注入に置き換えよう。
class OrderPizzaAction { public void order(HttpSession session) { Customer customer = session.getCurrentCustomer(); PizzaUtilities.createOrder(getPizzaSpecs(), customer); } ... }
上を以下のように変える。
class OrderPizzaAction { private final PizzaServices pizzaServices; @Inject OrderPizzaAction(PizzaServices pizzaServices) { this.pizzaServices = pizzaServices; } public void order(HttpSession session) { Customer customer = session.getCurrentCustomer(); pizzaServices.createOrder(getPizzaSpecs(), customer); } }
OrderPizzaActionのコンストラクタを変える必要があるよ。 これは後回しにしたいっていうんなら、PizzaServicesを静的に注入することにしてもいいけど。
class OrderPizzaAction { @Inject public static PizzaServices pizzaServices; public void order(HttpSession session) { Customer customer = session.getCurrentCustomer(); pizzaServices.createOrder(getPizzaSpecs(), customer); } }
そんでもって、モジュールでは静的注入を指定しとこう。
class PizzaModule extends AbstractModule { protected void configure() { requestStaticInjection(OrderPizzaAction.class); } }
静的注入は、静的から非静的にリファクタリングするあいだ、非常に便利に使えるんだ。
さて、ここまでで既にメリットが生まれた。OrderPizzaActionはGeograpy等々がなくてもテスト可能になった。 createOrderをオーバライドしたPizzaServicesのモックを作って、そいつを渡せばいいのだ。
ロジックを非静的バージョンに変更する
PizzaUtilitiesにある実装ロジックをPizzaServicesに移動してしまおう。 PizzaUtilitiesには呼び出し転送メソッドを残しておくことで、こいつを利用している他の連中をは変更せずに済む。
public class PizzaUtilities { @Inject public static PizzaServices pizzaServices; public static Order createOrder(List<PizzaSpec> pizzas, Customer customer) { return pizzaServices.createOrder(pizzas, customer); } } public class PizzaServices { private static final int TIME_TO_PREPARE = 6; private static final int MAX_DISTANCE = 20; public Order createOrder(List<PizzaSpec> pizzas, Customer customer) { Directions directions = Geography.getDirections( PizzaStore.getStoreAddress(), customer.getDeliveryAddress()); ... return new Order(pizzas, invoice, arrivalTime, customer, directions); } }
PizzaUtilitiesにPizzaServicesを注入するため、これまた静的注入を指定することにしよう。
class PizzaModule extends AbstractModule { protected void configure() { requestStaticInjection(OrderPizzaAction.class); requestStaticInjection(PizzaUtilities.class); } }
非静的バージョンを注入しよう
さて、PizzaUtilitiesに注入することができた、今度はそいつ(PizzaServices)に注入することにしようか。 手近なものはOven.getCurrentOven()シングルトンだ。 こいつを注入できるようにするためには、モジュールでバインド指定する。
public class PizzaServices { private final Oven currentOven; @Inject public PizzaServices(Oven currentOven) { this.currentOven = currentOven; } public Order createOrder(List<PizzaSpec> pizzas, Customer customer) { ... int arrivalTime = TIME_TO_PREPARE + currentOven.schedule(TIME_TO_PREPARE, pizzas) + directions.estimateTravelTime(); ... } }
そしてモジュールでは、
class PizzaModule extends AbstractModule { protected void configure() { requestStaticInjection(OrderPizzaAction.class); requestStaticInjection(PizzaUtilities.class); bind(Oven.class).toProvider(new Provider() { public Oven get() { return Oven.getCurrentOven(); } }); } }
こいつの意味は、Ovenが注入されるときには、いつでも古いOven.getCurrentOven()が呼び出されてオブジェクトが取得されるってことだ。 まぁ、後でこのメソッドも、とっぱらっちゃうけどね。
さてこれで、前もって特別なOvenインスタンスを準備しなくてもPizzaServicesをテストできるってわけ。 次回は、残りの静的呼び出しも片付けてしまおう。