パート2
パート1で、ピザプログラムのいくつかの静的呼び出しを片付けた。 でもまだまだテストの邪魔をする静的呼び出しが残ってるぞ。
public class PizzaServices { private final Oven currentOven; /** 訳注:オーブンは注入されるようになった */ @Inject public PizzaServices(Oven currentOven) { this.currentOven = currentOven; } public 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 + currentOven.schedule(TIME_TO_PREPARE, pizzas) + directions.estimateTravelTime(); /** 訳注:ここもダメ */ Invoice invoice = Invoice.create(pizzas, directions.getLengthInKm()); /** 訳注:ここは? */ return new Order(pizzas, invoice, arrivalTime, customer, directions); } } 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(); } }); } }
@Named付の値を注入する
さて今日は、PizzaStore.getStoreAddress()への静的呼び出しをつぶすために、注入とアノテーションを使ってみよう。 単純に、お店の住所(store address)をインジェクタにバインドすることもできるんだけど、 でもそんなことをするとプログラムの中にいろんな住所がある場合に混乱するかもしれない。 ならば、住所をバインディングアノテーションで名前付けしてあげればいいと思うよ。
public class PizzaServices { private final Oven currentOven; private final Address storeAddress; @Inject public PizzaServices(Oven currentOven, @Named("storeAddress") Address storeAddress) { this.currentOven = currentOven; this.storeAddress = storeAddress; } public Order createOrder(List<PizzaSpec> pizzas, Customer customer) { Directions directions = Geography.getDirections( storeAddress, customer.getDeliveryAddress()); ... } }
そして、モジュール内では名前付けされた住所についてannotatedWithを使い、
class PizzaModule extends AbstractModule { protected void configure() { ... bind(Address.class) .annotatedWith(Names.named("storeAddress")) .toInstance(PizzaStore.getStoreAddress()); } }
さてこれで、PizzaStoreクラスの静的メソッドに依存することなく、PizzaServiceインターフェースをテストできるってもんだ。
カスタムアノテーションを使って文字列を省略する
もしかして、"storeAddress"なんて文字列に我慢がならなかったりする? カスタムアノテーションを使えば、これを置き換えることができるんだ。 StoreAddress.javaというファイルを作り、そこにアノテーションを書く、 そのアノテーション自体が結構アノテーションされるわけなんだけど。以下のように、
@Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.FIELD, ElementType.PARAMETER}) @BindingAnnotation public @interface StoreAddress {}
さて、コンストラクタの文字列名称を削除して、このアノテーションを使ってみよう。
@Inject public PizzaServices(Oven currentOven, @StoreAddress Address storeAddress) { this.currentOven = currentOven; this.storeAddress = storeAddress; }
モジュールも変更しとこうね。
class PizzaModule extends AbstractModule { protected void configure() { ... bind(Address.class) .annotatedWith(StoreAddress.class) .toInstance(PizzaStore.getStoreAddress()); } }
静的メソッドをインスタンスメソッドで置き換えつつあるけど、 われらがPizzaServicesクラスは、Geograpy.javaに大きく依存している、getDirectionsメソッドを使うためにね。 ラッキーだな、もうどうすればよいかわかるよね。 パート1でPizzaUtilitiesとPizzaServicesを交換したのと同じように、 Geograpyと(新たな)GeograpyServiceを置き換えればいいんだよね。 そんでもって、こいつをPizzaServicesに注入すればいいってわけ。
public class PizzaServices { private final Oven currentOven; private final Address storeAddress; private final GeographyServices geographyServices; @Inject public PizzaServices(Oven currentOven, @StoreAddress Address storeAddress, GeographyServices geographyServices) { this.currentOven = currentOven; this.storeAddress = storeAddress; this.geographyServices = geographyServices; } public Order createOrder(List<PizzaSpec> pizzas, Customer customer) { Directions directions = geographyServices.getDirections( storeAddress, customer.getDeliveryAddress()); ... } }
これまでの変更で、createOrderを依存なくテストするのに、OvenとGeograpyServicesのサブクラスを作成できるようになった。 これは、我々のテストが高速に動き、false negativesが無いってことだよ。
非静的な注入可能なコードの最もでかいメリットというのは、PizzaServicesの実装を変更したいと思ったときに、edit-compile-executeサイクルがドラマチックに素早くなるということなんだ。
次回は、GeographyServicesとPizzaServicesクラスをインターフェースに置き換えてコード改良をしてみよう。