Java21からの「データ指向」は「オブジェクト指向」と何が違うのか

特にJava20,21以降で話題になっている「データ指向プログラミング」とは何か、オブジェクト指向と何が違うのかの個人的まとめです。

※本来は型や継承・implementsの縛りも関係しますが、この記事では「データは不変である」という要素に重きを置いています。

オブジェクト指向を置き換えるものではない

データ指向プログラミングはオブジェクト指向と共存する概念で、JavaのアップデートによってJavaがオブジェクト指向言語から脱却するという意味ではありません。

オブジェクト指向をより保守性高く使う上でベースとなる考え方・テクニックのことです。

Java20で「データ指向」という言葉が出てきた背景

保守性、(言語レベルで)セキュリティの高いJavaの書き方が今まで多数ベストプラクティスとして広まってきました。「プロパティは不変にすべき」「不変ゆえのgetter/setterの簡略化」などです。後続言語のKotlinやSwiftは言語レベルでそれをサポートするようになりました。

2023年になり、Javaもそれらの考え方を言語レベルで本格的にサポートするようになってきました。それらのアップデート内容は総じて言うと上述の考え方を一言で言う「データ指向」という考え方のもと実施されているようです。

データ指向プログラミングの原則

以下、データ指向のことを DOP と呼びます。(Data-Oriented Programming)

1. 「データ」は不変

DOPにおいて「データ」は不変です。例えばインスタンスにおいて一度値がセットされたプロパティには変更を許しません。意図しないデータの書き換えでバグやデータの不整合の可能性を減らすためです。

これを実現するには、Javaの record が役立ちます。

サンプル:データ指向を意識しない場合

Javaをはじめ、オブジェクト指向のプログラミング言語ではクラスにgetterとsetterを書くことが一般的です。

プロパティを読み取り専用にしたいときはプロパティをfinalにしてsetterを書かないという選択もありますが、もちろんfinalを書かなければその効果は消えます。setterを書かなかったとしても、プロパティに再代入するメソッドは他にいくらでも作成可能です。

public class Person {
    private String name;
    private int age;
    // (略)コンストラクタ
    // (略)getterとsetter
    // setter以外にもプロパティを書き換えるメソッドは書ける
}

public class Main {
    public static void main(String[] args) {
        Person person = new Person("Alice", 30);
        System.out.println(person.getName());
    }
}

サンプル:データ指向の場合

recordはデータ指向を支えるキーワードです。recordでクラスを作成することでプロパティがfinalであることが保証され、setterを書く余地すら排除されます。

つまりrecordを使う限りそのプロパティへの再代入は文法上不可能になり、データ指向の考えに沿ったコードになります。

public record Person(String name, int age) { }

public class Main {
    public static void main(String[] args) {
        Person person = new Person("Alice", 30);
        System.out.println(person.name());
    }
}

2. ロジックはデータから独立させる

データとそれを操作するロジックを分離することもDOPのベースにある考え方のひとつです。データは単なる「データ」であって、振る舞いを持たせません。データの整合性を保持しながら、システムの柔軟性と拡張性を高めることを目指しています。

サンプル:データ指向を意識しない場合

一般的なオブジェクト指向プログラミングでは、プロパティとそれに関連するメソッドがクラスに一緒に定義されます。データとロジックの結びつきが強固になるのは必然です。

メソッドからプロパティを書き換えることが可能になると、メソッドを呼び出す順番によって実行結果が変わる可能性が出てきます。その複雑さが現れるのはテストを書くときでしょう。

public class Person {
    private String name;
    private int age;
    // (略)コンストラクタ, getter, setter

    public String generateGreet() {
        return "こんにちは、" + this.name + "といいます。";
    }
}

public class Main {
    public static void main(String[] args) {
        Person person = new Person("Alice", 30);
        System.out.println(person.generateGreet());
        // こんにちは、Aliceといいます。
    }
}

痒い例:テストが複雑になる

クラスにデータとメソッドを一緒に書いた場合、メソッドがプロパティを書き換える(再代入など)可能性を考慮してテストを書かなければなりません。テストケースは増え、メソッドを実行する順番も動作に影響するでしょう。結果としてテストは複雑になります。

このクラスについてよく知らない人がこのクラスの generateGreetMessage() のテストコードを書いたとします。このとき、その人はテストで generateGreetMessage() を実行したことで nameage が書き換わる可能性を意識すべき余地は残ります。

public class PersonTest {
    
    @Test
    public void testIsSenior() {
        Person person = new Person("Alice", 70);
        assertEquals("こんにちは、Aliceといいます。", person.generateGreet());

                // ↑この実行で person.name や person.age
        // が書き換わる余地は文法上残されている。
        // personのメソッドの動作がgenerateGreetを実行したことで
        // 変わるかもしれないことを意識する必要が出てくる
    }
}

サンプル:データ指向の場合

DOPではビジネスロジックは独立した関数やサービスとして実装します。データは整合性を保ちやすくなり、処理の独立性も高くなります。

public record Person(String name, int age) { }

public class PersonService {
    // ビジネスロジックはPersonから独立した関数やサービスとして実装
    public static String generateGreet(Person person) {
        return "こんにちは、" + person.name() + "といいます。";
    }
}

public class Main {
    public static void main(String[] args) {
        Person person = new Person("Alice", 30);
        System.out.println(PersonService.generateGreet(person));
        // こんにちは、Aliceといいます。
    }
}

恩恵の例:テストしやすくなる

ビジネスロジックはサービスクラスや他の独立した処理に分離するのがDOPのベースにある考え方です。結果、テストの対象はPersonというよりその処理やサービスクラスになります。

Personはどれほど参照されようと中身の name や age は変化しません。単なるパラメータとして渡すだけで良くなり、期待される出力を確認することだけに意識を集中してテストケースを作成できます。

public class PersonServiceTest {
    
    @Test
    public void testIsSenior() {
        Person person = new Person("Alice", 70);
        assertEquals("こんにちは、Aliceといいます。", PersonService.generateGreet(person));

        // ↑この実行でpersonの中身が書き換わることを恐れなくて良い。
        // personに再代入が無い限り、
        // PersonService.generateGreet(person) はいつも同じ結果を返す
    }
}

要は

  • オブジェクト指向プログラミングの代わりではない。共存して使用する新しいプログラミングのスタイル。
  • データは不変にし、振る舞いはサービスクラスや他の独立した処理に分離。
    • 何度実行しても同じ結果を期待できる仕組みにすることで整合性、保守性の向上を目指す。

Javaがデータ指向を推す背景

処理がインスタンスの外に独立するということはビジネスロジックのクラスがプロジェクト内に増え、結果コードの管理負担が増大することは容易に想像できます。

しかしJavaOneの登壇者が述べる通り、昨今「大規模開発」と呼ばれるプロジェクトは小さなサービスの集合体の形を取っているケースが少なくありません。

Data-Oriented Programming in Java – YouTube

Javaのオブジェクト指向を支える「型」の概念も、大規模開発のためというより「データ指向」という考えを支えるツールに今や代わりつつあるという考え方のようです。

カテゴリー: Java