Upload page content

You can upload content for the page named below. If you change the page name, you can also upload content for another page. If the page name is empty, we derive the page name from the file name.

File to load page content from
Page name
Comment

Locked History Actions

guice/RefactoringToGuice/part1

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);
  }
}

PizzaUtilitiesPizzaServicesを注入するため、これまた静的注入を指定することにしよう。

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をテストできるってわけ。 次回は、残りの静的呼び出しも片付けてしまおう。