2011年12月14日水曜日

ImmutableとBuilder

このエントリはJava Advent Calender 2011の第14日目です。

OOPで最も重要なテクニックは何でしょうか。色々な考え方があるでしょうが、ボクはImmutable Objectがまず最初に浮かびます。
かなり前にJava World誌に書いていたJavaデザインノートという連載でも、第1回のテーマはImmutable Objectでした。

Immutable Object(不変オブジェクト)は、生成時に属性を設定した後、属性値を変更することができないオブジェクトです。Javaの場合、StringやDateなどの情報を保持する基本クラスの多くがImmutable Objectになっており、Javaプログラミングを下支えしています。

Immutable Objectの長所は、とにかく属性値が変更されないこと。一度生成したオブジェクトは何の心配もなく、メソッドの引数で渡すことができます。その先で、どのような共有のされかたをしても、知らないところで勝手に内容が変更されるということがありません。マルチスレッドプログラミングでは、この性質はさらに重要になります。

一方、Immutable Objectは以下の短所があります。

  1. 値を変更するためには、オブジェクトをまるごと複写する必要がある。
  2. すべての値の設定をオブジェクト生成時に行う必要があり、プログラミングがちょっと煩雑。

前者の問題は、最近のハードウェア性能の向上で、普通のWebアプリ、業務アプリではまず問題にはならないでしょう。まるごと複写といってもshallow copyとなるので、まるまるデッドコピーとはなるわけではありません。 また、多少の性能劣化があっても、プログラム品質、開発効率の向上の方がより重要というケースは多いはずです。 よほど性能要件の高い場合でも適材適所で使い分ければよいわけです。

そういう意味で、Value Objectは当然として、DTOといった情報量の多いオブジェクトもできるだけImmutable Object化していきたいところです。

となると、現在問題となるのは後者。まずオブジェクト生成時のコンストラクタで全情報を引数で渡さなければなりません。さらに、オブジェクトの変更(つまりデータを変更して複写)では、オブジェクトのほとんどの情報と変更したいデータを用意して、コンストラクタに渡すという手間が必要になります。 StringやDateのようなValue Objectでは、オブジェクトが管理する情報が1つやせいぜい2つなので全く問題ありませんが、DTO等の場合は管理する情報が数十個になるケースも考えられ、かなり煩雑になります。数十個と言わずとも5個ぐらいでも十分嫌になります。実際のプログラミングではこの手間が嫌で、Immutableの方がよいと分かっていても普通のMutable Objectを選択することになりがちです。これを何とかしたいですね。

この問題を解決するのがBuilderです。

プログラムはこんな感じ。

Immutable Object「Person」と、そのBuilderである「Person.Builder」を定義しています。 Personは、Immutable Objectであることを活用して属性をpublicにして外から扱いやすくしています。 Person.Builderの方は、普通のMutableオブジェクトで、値を設定しやすくするための工夫を好きなだけ行うことができます。ここではwithXXXメソッドでメソッドチェインを実現してみました。

  1. package advent2011;  
  2.   
  3. import java.util.List;  
  4. import java.util.ArrayList;  
  5. import static java.util.Collections.unmodifiableList;  
  6.   
  7. public class Person {  
  8.     public final String name;  
  9.     public final int age;  
  10.     public final String address;  
  11.     public final List<String> phones;  
  12.   
  13.     public Person(String name, int age, String address,  
  14.                   List<String> phones) {  
  15.         this.name = name;  
  16.         this.age = age;  
  17.         this.address = address;  
  18.         this.phones = unmodifiableList(new ArrayList<String>(phones));  
  19.     }  
  20.   
  21.     public static class Builder {  
  22.         public String name;  
  23.         public int age;  
  24.         public String address;  
  25.         public ArrayList<String> phones = new ArrayList<String>();  
  26.   
  27.         public Builder() {  
  28.         }  
  29.   
  30.         public Person build() {  
  31.             return new Person(name, age, address, phones);  
  32.         }  
  33.   
  34.         public Builder withName(String name) {  
  35.             this.name = name;  
  36.             return this;  
  37.         }  
  38.   
  39.         public Builder withAge(int age) {  
  40.             this.age = age;  
  41.             return this;  
  42.         }  
  43.   
  44.         public Builder withAddress(String address) {  
  45.             this.address = address;  
  46.             return this;  
  47.         }  
  48.   
  49.         public Builder withPhone(String phone) {  
  50.             this.phones.add(phone);  
  51.             return this;  
  52.         }  
  53.     }  
  54. }  

こんな感じで使います。

  1. package advent2011;  
  2.   
  3. import org.junit.Test;  
  4. import static org.hamcrest.core.Is.is;  
  5. import static org.junit.Assert.*;  
  6.   
  7. public class PersonTest {  
  8.     @Test  
  9.     public void buildPerson() {  
  10.         Person.Builder builder = new Person.Builder();  
  11.         builder.withName("Taro")  
  12.             .withAge(30)  
  13.             .withAddress("Yokohama")  
  14.             .withPhone("045-123-4567");  
  15.         Person person = builder.build();  
  16.         assertThat(person.name, is("Taro"));  
  17.         assertThat(person.age, is(30));  
  18.         assertThat(person.address, is("Yokohama"));  
  19.         assertThat(person.phones.get(0), is("045-123-4567"));  
  20.         System.out.println(person);  
  21.     }  
  22. }  

Personオブジェクトを生成す時の手間は、普通のMutableオブジェクトの時と同じ。 buildメソッドで返ってくるPersonオブジェクトはImmutableオブジェクトなので、安全に使うことができます。 どちらの目的も叶える解ですね!

新たな問題

さて、Builderを使うとImmutableオブジェクトが使いやすくなるのは分かったのですが、新たに別の問題が発生します。

このBuilderを用意する手間がなかなか大変なんです。コーディング量も多いですし、コーディング内容が単純作業のためが短調で面白くない、という問題もあります。 この手間があるので、このままでは結局使わないテクニックになってしまいそうです。

単調なコーディングあるところ、自動生成あり。ここで登場するのが自動生成です。クラスの形が分かっていれば、その情報からJavaプログラムのソースコードを生成すればよることが可能です。

SimpleModelerService

現在開発中のSimpleModelerServiceでは、この問題に対応するためにDSLからJavaプログラムを自動生成する機能を提供する予定です。DSLにはScala DSLに加えて、CSV、マインドマップ(XMind)、Excelなどが利用できるようになる予定です。 今回は、CSVを使って試してみましょう。

以下の内容のCSVファイルを用意します。

  1. person,name;age(int);address?;phone+  

CSVの第1カラムにクラス名、第2カラムに属性のリストを定義しています。「(int)」はint型、「?」「+」は多重度(?は0または1、+は1以上)を意味しています。

これを以下のフォームに設定して「生成」すると、色々なコードが生成されたものをzipで固めたものが返ってきます。

この中にある、DDPerson.javaに今回のテーマであるImmutableオブジェクトとBuilderが記述されています。ぜひ覗いてみてください。

ファイル名:

ImmutableオブジェクトとそのBuilderの作成は、手間がかかるのでつい手抜きをしたくなりますが、プログラムの自動生成を活用すれば楽々クリアできますね。
SimpleModelerのJava生成機能は、近々公開したいと思いますので、ぜひご利用ください。

1 件のコメント: