はじめに
ここ1年くらいでやっとオブジェクト指向らしいコードが書けるようになってきました。
オブジェクト指向を知ったのは1995年なので、20年以上かかったことになります。
そんな自分がオブジェクト指向らしいコードが書けるようになったきっかけは
Value Objectを意識して書くようにしたからです。
Value Objectとは
一番シンプルな定義はMartin Fowler先生が書いている記事の翻訳(バリューオブジェクト)がいいかなと1。
- フィールドの値が同じなら2つのValue Objectは同じ
- 不変
Javaでの実装例
Javaの場合は以下の2つを満たすといいです。
直接は関係ありませんが、toString()も実装しています。
- equals()とhashCode()を実装
- フィールドは全てfinal2
例えば、java.time.Year風のクラス、MyYearを実装してみます。
equals()とhashCode()はきちんと実装するのは面倒なので、IDE(IntelliJ IDEA)に任せています。
public final class MyYear {
private final int year;
private MyYear(int year) {
this.year = year;
}
public static MyYear of(int year) {
return new MyYear(year);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
MyYear myYear = (MyYear) o;
return year == myYear.year;
}
@Override
public int hashCode() {
return year;
}
@Override
public String toString() {
return "MyYear{" +
"year=" + year +
'}';
}
}
実装にはApache Commons Langを使うのもいいでしょう。
リフレクションを使いますが、記述はシンプルになります。
(リフレクションを使わないAPIもあります)
なお、toString()にSHORT_PREFIX_STYLEを使っているのは、
オブジェクトのハッシュコードは不要で、フィールドの値のみが必要だからです。
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
public final class MyYear {
private final int year;
private MyYear(int year) {
this.year = year;
}
public static MyYear of(int year) {
return new MyYear(year);
}
@Override
public boolean equals(Object o) {
return EqualsBuilder.reflectionEquals(this, o);
}
@Override
public int hashCode() {
return HashCodeBuilder.reflectionHashCode(this);
}
@Override
public String toString() {
return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);
}
}
メリット
とりあえず大きなメリットを3つほど。
ロジックを集約しやすい
例えば来年を返すメソッドが欲しい場合、MyYearメソッドに実装できます。
ロジックが1つのクラスに集中し、変更が容易になります。
public MyYear nextYear() {
return new MyYear(this.year + 1);
}
Mapのキーに使える
以下のようにキーとして使えます。
import java.util.HashMap;
public final class Main {
public static void main(String[] args) {
var myMap = new HashMap<>();
myMap.put(MyYear.of(2018), "平成30年");
System.out.println(myMap.get(MyYear.of(2018))); // => 平成30年。equals()がない場合はnull
}
}
テストが書きやすい
普通にオブジェクトの比較でOKです。
equals()を定義しないと、getterを定義してフィールドの値をわざわざ取得しないといけません。
import org.junit.Test;
import static org.assertj.core.api.Assertions.assertThat;
public final class MyYearTest {
@Test
public void testMyYearNext() {
MyYear now = MyYear.of(2018);
MyYear next = MyYear.of(2019);
assertThat(now.nextYear()).isEqualTo(next);
}
}
なぜValue Objectなのか
オブジェクト指向の定義にはしばしば「カプセル化」と「継承」、「ポリモーフィズム」が出てきます。ポリモーフィズムはともかく、他の2つがどうもしっくり来ないんですよね。
カプセル化はsetter/getterのことではないけれど、じゃあどういう書き方をすればいいのか、継承は本当に必要なのか、そういうのが自分の中で整理できてませんでした。
でもValue Objectをちゃんと作るようになってからは、迷いが少なくなり、理解しやすいコードが書けるようになってきました。唯一の弱点は「最初書くのがめんどい」ですが、これは必要なコストだと思います。
おわりに
本当はもしかしたらオブジェクト指向よりドメイン駆動設計の方が理解しやすいかもしれない、そう考えてたりしますが、さすがにそれは突飛すぎると思ったので、ひとまずValue Objectに絞って書いてみました。
もっと本気(?)でやるなら「OOコード養成ギブス」とも呼ばれる以下のエッセイが参考になると思います。ただこれはかなりキツイです。自分は無理・・・(ヽ´ω`)
-
原文(ValueObject)にはいろいろ加筆されていますが、今回はあくまで定義なので、古い記述を使います。 ↩
-
finalをつけてもListなどが入ると不変とは言えませんが、ここではその説明は省略。 ↩