Revision 1 as of 2009-12-14 05:46:50

Clear message
Locked History Actions

guice/RefactoringToGuice/part2

パート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();
      }
    });
  }
}

Injecting @Named values

Today I'll use injection and annotations to replace the static call to PizzaStore.getStoreAddress(). We could just bind the store address in the injector, but that could be confusing if there are multiple addresses in the program. The fix is to name this address with a binding annotation:

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

And in the Module, we bind the named address using annotatedWith:

class PizzaModule extends AbstractModule {
  protected void configure() {
    ...
    bind(Address.class)
        .annotatedWith(Names.named("storeAddress"))
        .toInstance(PizzaStore.getStoreAddress());
  }
}

Now we can test the PizzaService interface without a dependency on static methods in the PizzaStore class.

Removing Strings with custom annotations

If the use of the String "storeAddress" upsets you, we can replace that name with a custom annotation. In the file StoreAddress.java, we create the annotation, which itself must be heavily annotated:

@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.FIELD, ElementType.PARAMETER})
@BindingAnnotation
public @interface StoreAddress {}

Now we can replace the String name from the constructor with our annotation:

  @Inject
  public PizzaServices(Oven currentOven,
      @StoreAddress Address storeAddress) {
    this.currentOven = currentOven;
    this.storeAddress = storeAddress;
  }

And update our module:

class PizzaModule extends AbstractModule {
  protected void configure() {
    ...
    bind(Address.class)
        .annotatedWith(StoreAddress.class)
        .toInstance(PizzaStore.getStoreAddress());
  }
}

Replacing a static method with an instance method Our PizzaServices class still has a big dependency on Geography.java for its getDirections method. Fortunately, we already know how to do this - just as we swapped PizzaUtilities with PizzaServices in Part 1, we can replace Geography with GeographyServices here. Then we can inject that into our PizzaServices class:

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

With the changes so far, we can create custom subclasses of Oven and GeographyServices to test createOrder without dependencies. This means that our test will run faster and provide no false negatives.

The biggest benefit I get from non-static, injectable code is that if I need to make changes to the implementation of PizzaServices, the edit-compile-execute cycle is dramatically faster.

In a future post, I'll improve this code by replacing the GeographyServices and PizzaServices classes with interfaces.