休日のリファクタリングあそび

いま「良いコード/悪いコードで学ぶ設計入門」を読んでる。今日はその途中で出てきたリファクタリングのサンプルで休日らしく息抜きに遊んでみたのでメモ。とても良い本なので読み終わったら感想を書こうと思ってる。

books.rakuten.co.jp

遊んだコード

おいといた

Commits · bufferings/20220506-refactoring · GitHub

元のコード

第14章にあるサンプル。これを書籍を参考にしながら、自分の好きな方法でリファクタリングする。

public class DeliveryManager {

  public static int deliveryCharge(List<Product> products) {
    int charge = 0;
    int totalPrice = 0;
    for (Product each : products) {
      totalPrice += each.price;
    }
    if (totalPrice < 2000) {
      charge = 500;
    } else {
      charge = 0;
    }
    return charge;
  }
}

既存のメソッドに対してテストを書く

Add test · bufferings/20220506-refactoring@64c2a14 · GitHub

書籍では、先にリファクタリング先の構造を作ってからそれに対してテストを書いてるけど、僕は先に既存のメソッドに対してテストを書いてみたいなと思ったのでそうした。テストメソッド名は日本語でいいかな

class DeliveryManagerTest {
  @Test
  public void 商品の合計金額が2000円未満の場合_配送料は500円() {
    var products = List.of(
        new Product(1, "商品A", 500),
        new Product(2, "商品B", 1499)
    );
    assertEquals(500, DeliveryManager.deliveryCharge(products));
  }

  @Test
  public void 商品の合計金額が2000円以上の場合_配送料は無料() {
    var products = List.of(
        new Product(1, "商品A", 500),
        new Product(2, "商品B", 1500)
    );
    assertEquals(0, DeliveryManager.deliveryCharge(products));
  }
}

ロジックを移動

Move logic · bufferings/20220506-refactoring@5863f31 · GitHub

テストができたので、それを Green にしたままロジックを DeliveryManager から DeliveryCharge に移動

public class DeliveryCharge {
  final int amount;

  public DeliveryCharge(ShoppingCart cart) {
    int charge = 0;
    int totalPrice = 0;
    for (Product each : cart.products) {
      totalPrice += each.price;
    }
    if (totalPrice < 2000) {
      charge = 500;
    } else {
      charge = 0;
    }
    this.amount = charge;
  }
}

DeliveryManager はこうなる

  public static int deliveryCharge(List<Product> products) {
    var cart = new ShoppingCart();
    for (var elem : products) {
      cart = cart.add(elem);
    }
    var charge = new DeliveryCharge(cart);
    return charge.amount;
  }

テスト対象を DeliveryManager から DeliveryCharge に変更

Move test target from DeliverManager to DeliveryCharge · bufferings/20220506-refactoring@6e03cba · GitHub

とりあえずこんな感じで

  @Test
  public void 商品の合計金額が2000円未満の場合_配送料は500円() {
    var products = List.of(
        new Product(1, "商品A", 500),
        new Product(2, "商品B", 1499)
    );

    var cart = new ShoppingCart();
    for (var elem : products) {
      cart = cart.add(elem);
    }
    var charge = new DeliveryCharge(cart);

    assertEquals(500, charge.amount);
  }

DeliveryManager を削除

Delete DeliveryManager · bufferings/20220506-refactoring@ca48d73 · GitHub

いらなくなったので削除

テストをリファクタリング

Refactor test · bufferings/20220506-refactoring@ecf4e3d · GitHub

さっき、とりあえずで動くようにしたテストを、ちゃんと書き直した

  @Test
  public void 商品の合計金額が2000円未満の場合_配送料は500円() {
    var cart = new ShoppingCart();
    cart = cart.add(new Product(1, "商品A", 500));
    cart = cart.add(new Product(2, "商品B", 1499));
    var charge = new DeliveryCharge(cart);

    assertEquals(500, charge.amount);
  }

合計金額の計算を ShoppingCart に移動

書籍にも書いてあるとおりのリファクタリング

Move total price calculation to ShoppingCart · bufferings/20220506-refactoring@366eab5 · GitHub

Stream 使ってみた

  public int totalPrice() {
    return products.stream().mapToInt(product -> product.price).sum();
  }

DeliveryChargeリファクタリング

やっとメインのリファクタリングだね。これも書籍の通り。三項演算子はあんまり好きじゃないので普通に if で

Refactor DeliveryCharge logic · bufferings/20220506-refactoring@b0bf4c6 · GitHub

public class DeliveryCharge {
  private static final int CHARGE_FREE_THRESHOLD = 2000;
  private static final int PAY_CHARGE = 500;
  private static final int CHARGE_FREE = 0;

  final int amount;

  public DeliveryCharge(ShoppingCart cart) {
    int totalPrice = cart.totalPrice();
    if (totalPrice < CHARGE_FREE_THRESHOLD) {
      amount = PAY_CHARGE;
    } else {
      amount = CHARGE_FREE;
    }
  }
}

最後に自分の好みにリファクタリング

Refactor production code · bufferings/20220506-refactoring@7b527ae · GitHub

個人的にはフィールド直接アクセス好きじゃないのでアクセサメソッドをつけた

public class Product {
  private final int id;
  private final String name;
  private final int price;

  public Product(int id, String name, int price) {
    this.id = id;
    this.name = name;
    this.price = price;
  }

  public int id() {
    return id;
  }

  public String name() {
    return name;
  }

  public int price() {
    return price;
  }
}

なんとなく DeliveryCharge にファクトリーメソッドを作った

public class DeliveryCharge {
  private static final int CHARGE_FREE_THRESHOLD = 2000;
  private static final int PAY_CHARGE = 500;
  private static final int CHARGE_FREE = 0;

  public static DeliveryCharge from(ShoppingCart cart) {
    int totalPrice = cart.totalPrice();
    if (fulfillChargeFreeCondition(totalPrice)) {
      return new DeliveryCharge(CHARGE_FREE);
    } else {
      return new DeliveryCharge(PAY_CHARGE);
    }
  }

  private static boolean fulfillChargeFreeCondition(int totalPrice) {
    return totalPrice >= CHARGE_FREE_THRESHOLD;
  }

  private final int amount;

  private DeliveryCharge(int amount) {
    this.amount = amount;
  }

  public int amount() {
    return amount;
  }

}

あとはちょこっとテストを付け足して終わり

楽しかった

休日っぽくふわふわコード書いて楽しかった