A Day In The Life

とあるプログラマの備忘録

ビジネス・ルールに合わせた独自データ型を活用する(実装編)

オブジェクト指向技術を活用していくための考察第2回目です。
前回の「ビジネス・ルールに合わせた独自データ型を活用する」では実装例を紹介できなかったので、今回は実際にJavaを使った実装例を紹介します。
それではショッピングモールサイトなんかで使われている受注系のシステムを例に進めていきましょう。
全体のクラス概要はこんな感じにします。
今回実装するクラスの概要
受注管理なんかでよくあるのタイプのクラス図ですね。
まずはじめにテストケースを見て全体の動きをつかみましょう。
テストケースでは明細を2行作成して金額の合計値をテストしています。
ポイントはOrderクラスのaddOrderDetail(),removeOrderDetail()メソッドを介さないとOrderDetailの追加・削除ができないようになっているところです。

public class OrderTest extends TestCase {	
  private Order order;
  private OrderDetail orderDetail1;
  private OrderDetail orderDetail2;
  public void setUp() {
    //create order
    Customer cust = new Customer("T-989", "Beans Company");
    order = new Order();
    order.setCustomer(cust);
    order.setOrdered(new Date(2007,6,13));
    //create order details
    Item penCase = new Item("I-9932", "Pen Case", new UnitPrice("300.00"));
    orderDetail1 = new OrderDetail(1, penCase, new Quantity("8"));
    Item pen = new Item("I-9987", "Pen", new UnitPrice("300.98"));
    orderDetail2 = new OrderDetail(2, pen, new Quantity("6"));
  }
  public void testCreateOrder() {
    assertEquals(order.getCustomer().getId(), "T-989");
    assertEquals(order.getCustomer().getName(), "Beans Company");
    assertEquals(order.getOrdered(), new Date(2007,6,13));
  }
  public void testCreateOrderDetails() {
    order.addOrderDetail(orderDetail1);
    assertEquals(1, orderDetail1.getRownum());
    assertEquals(orderDetail1.getItem().getId(), "I-9932");
    assertEquals(orderDetail1.getItem().getName(), "Pen Case");
    assertEquals(orderDetail1.getSubTotal().toString(), "2400.00");
    //合計金額の確認
    assertEquals(order.getTotal().toString(), "2400.00");
    order.addOrderDetail(orderDetail2);
    assertEquals(2, orderDetail2.getRownum());
    assertEquals(orderDetail2.getItem().getId(), "I-9987");
    assertEquals(orderDetail2.getItem().getName(), "Pen");
    assertEquals(orderDetail2.getSubTotal().toString(), "1805.88");
    //合計金額の確認
    assertEquals(order.getTotal().getValue().toString(), "4205.88");
  }
  public void testRemoveOrderDetail() {
    order.addOrderDetail(orderDetail1);
    order.addOrderDetail(orderDetail2);
    order.removeOrderDetail(orderDetail2);
    assertEquals(order.getOrderDetails().size(), 1);
    assertEquals(order.getTotal().toString(), "2400.00");
  }
  public void testUnmodifiedOrderDetails() {
    List<OrderDetail> list = order.getOrderDetails();
    try {
      list.add(orderDetail2);
      fail();
    } catch(UnsupportedOperationException e) {
      assertTrue(true);
    }
  }
}

次はCustomerクラス、Orderクラス、OrderDetailクラス、Itemクラスです。
今回は単純に毎回定価で商品を売るケースを想定しましたので、Itemクラスに単価(UnitPrice)を持たせることにしました。
またユーザー定義型を使用している部分を青色表示していますのでそのあたりを中心に見てみてください。

public class Customer {
  private String id;
  private String name;
  public Customer(String id, String name) {
    this.id = id;
    this.name = name;
  }
  public String getId() {
    return id;
  }
  public String getName() {
    return name;
  }
  ...equalsとhashCodeメソッドは省略
}

public class Order {
  private Customer customer;
  private Date ordered;
  private List<OrderDetail> orderDetails = new ArrayList<OrderDetail>();
  private Total total = new Total();
  public Customer getCustomer() {
    return customer;
  }
  public void setCustomer(Customer customer) {
    this.customer = customer;
  }
  public Date getOrdered() {
    return ordered;
  }
  public void setOrdered(Date ordered) {
    this.ordered = ordered;
  }
  public List&lt;OrderDetail> getOrderDetails() {
    //他のクラスで変更されないように
    return Collections.unmodifiableList(orderDetails);
  }
  public void addOrderDetail(OrderDetail orderDetail) {
    this.orderDetails.add(orderDetail);
    total.addSubTotal(orderDetail.getSubTotal());
  }
  public void removeOrderDetail(OrderDetail orderDetail) {
    if(this.orderDetails.indexOf(orderDetail) != -1) {
      this.orderDetails.remove(orderDetail);
      total.removeSubTotal(orderDetail.getSubTotal());
    }
  }
  //合計金額を取得する
  public Total getTotal() {
    return total;
  }
  ...equalsとhashCodeメソッドは省略
}

public class Item {
  private String id;
  private String name;
  private UnitPrice unitPrice;
  public Item(String id, String name, UnitPrice unitPrice) {
    this.id = id;
    this.name = name;
    this.unitPrice = unitPrice;
  }
  public String getId() {
    return id;
  }
  public String getName() {
    return name;
  }
  public UnitPrice getUnitPrice() {
    return unitPrice;
  }
  ...equalsとhashCodeメソッドは省略
}

public class OrderDetail {
  private int rownum;
  private Item item;
  private Quantity qty;
  public OrderDetail(int rownum, Item item, Quantity qty) {
    this.rownum = rownum;
    this.item = item;
    this.qty = qty;
  }
  public Item getItem() {
    return item;
  }
  public Quantity getQty() {
    return qty;
  }
  public int getRownum() {
    return rownum;
  }
  //金額を求める
  public SubTotal getSubTotal() {
    return item.getUnitPrice().multiply(qty);
  }
  ...equalsとhashCodeメソッドは省略
}

ユーザー定義型UnitPrice、Quantity、SubTotal、Totalはおもに金額の計算で使われていますね。
Orderクラスで合計金額の計算をしていますが、その計算をOrderDetail追加時または削除時に行っているところがポイントです。
それではユーザー定義型の実際のコードを見てみましょう。

public class UnitPrice {
  private BigDecimal value;
  public UnitPrice(String val) {
    this.value = new BigDecimal(val);
  }
  //数量をかける
  public SubTotal multiply(Quantity qty) {
    return new SubTotal(
        this.value.multiply(new BigDecimal(qty.getValue())).toString());
  }
 @Override
  public String toString() {
    return value.toString();
  }
  @Override
  public boolean equals(Object other) {
    return value.equals(other);
  }
  @Override
  public int hashCode() {
    return this.value == null ?
        System.identityHashCode(this) :
        this.value.hashCode();
  }
}

public class Quantity {
  private Integer value;
  public Quantity(String val) {
    this.value = new Integer(val);
  }
  public Integer getValue() {
    return value;
  }
  @Override
  public String toString() {
    return value.toString();
  }
  ...equalsとhashCodeメソッドはUnitPriceクラスと同じ
}

public class SubTotal {
  private BigDecimal value;
  public SubTotal(String val) {
    this.value = new BigDecimal(val);
  }
  public BigDecimal getValue() {
    return value;
  }
  @Override
  public String toString() {
    return value.toString();
  }
  ...equalsとhashCodeメソッドはUnitPriceクラスと同じ
}

public class Total {
  private BigDecimal value = new BigDecimal(0);
  public BigDecimal getValue() {
    return value;
  }
  //金額を追加する
  public void addSubTotal(SubTotal subTotal) {
    value = this.value.add(subTotal.getValue());
  }
  //金額を減らす
  public void removeSubTotal(SubTotal subTotal) {
    value = this.value.subtract(subTotal.getValue());
  }
  @Override
  public String toString() {
    return value.toString();
  }
  ...equalsとhashCodeメソッドはUnitPriceクラスと同じ
}

ユーザー定義型自体は特に複雑なところもなく単純な作りですね。
では全体を整理するために、ユーザー定義型も含めた最終的なクラス図を見てみましょう。
ユーザー定義型も含めたクラス概要
TotalはOrderの一部、UnitPriceはItemの一部、SubTotalとQuantityはOrderDetail一部になっているのがわかるかと思います。
クラス図で構造の理解ができたと思いますのでシーケンス図で明細追加時の動きを見てみましょう。
シーケンス図
個々のクラスが自分の行うべき処理を分担して計算を行っています。
このような設計にすることでどのような利点があるのでしょうか。
例えば従来からの手続き型処理の場合、合計金額を取得する前に以下のような計算をしてやる必要があります。

public void execute() {
  ・・・
  BigDecimal total = new BigDecimal(0);
  for(OrderDetail d : order.getOrderDetails()) {
    d = i.next();
    BigDecimal subTotal = d.getItem().getUnitPrice()
        .multiply(new BigDecimal(d.getQty()));
    total = total.add(subTotal);
  }
  order.setTotal(total);
  ・・・
}

この場合、明細が追加・削除されるたびに毎回計算処理をやり直さないといけません。
また処理が一箇所だけでしか使われないのであればあまり問題はないのですが、例にあげた受注明細の合計を求める処理なんかは結構いろんなところで使われる可能性のある処理ですよね。
毎回コピペして貼り付けたのではメンテナンス性がかなり犠牲になってしまいます。
その点今回のサンプルの場合、必要なところで

order.getTotal();

を呼び出すだけでOKです。
たとえ合計の計算方法が変わった(取引ごとに値引きが必要になったなど)としてもgetTotalメソッドを呼んでいるクラスは一切影響を受けません。こちらのほうがメンテナンス性が良いですね。
今回紹介した受注系システム以外にもいろいろな場面で応用できるかと思いますので実践してみてください。