広告

Java : 読み取り専用(const) のインタフェースを作る

読み取り専用のインタフェースを作ることで、想定外のオブジェクト変更が減ることが期待できます。
それはプログラムの品質向上にもつながるでしょう。

本記事では、具体的なコードつきで読み取り専用インタフェースの作り方を解説します。


概要

あるオブジェクトがあり、値を取得するメソッド(getter) と値を設定するメソッド(setter) を持っているとします。
つまり、オブジェクトは変更可能です。

さて、変更可能なオブジェクトですが、

  • Aクラスからのアクセスでは 変更を許可する
  • Bクラスからのアクセスでは 変更を許可しない

としたいとします。

シーケンス図

これは割とよくあるケースかなと思います。

つまり、クラス・メソッドの公開範囲を

  • getter のみ
  • getter + setter

に分けたい、ということですね。

別の言い方をすると、

  • オブジェクトを変更しないメソッドのみ (読み取り専用)
  • すべてのメソッド

に分ける、という感じです。

これは…

 オブジェクトを変更するメソッドの公開範囲を必要最低限にする
  ↓
 想定外のオブジェクト変更が減る
  ↓
 プログラムの品質も高くなる

ということが期待できます。

メソッドの公開範囲を制御するには、public や package private といった アクセス修飾子 を使う方法があります。
しかし、アクセス修飾子では解決できないケースもあるでしょう。

本記事では、アクセス修飾子を使わないで制御する、

  • 読み取り専用インタフェースを作る

という方法についてご紹介します。

ノート

  • C++ の経験があるかたは、メンバ関数につける const キーワードを思いつくかもしれません。
    しかし、Java 20 の時点では、C++ の const に相当する機能はありません。(final はちょっと違いますしね…)
  • 参考 : Why is there no Constant feature in Java? - Stack Overflow

読み取り専用インタフェース

最初に、簡単にですが読み取り専用インタフェースの例を見てみましょう。

public interface ReadOnlyA {
    int getValue();
}

public class A implements ReadOnlyA {
    private int value;

    @Override
    public int getValue() {
        return value;
    }

    public void setValue(int value) {
        this.value = value;
    }
}

基準となる A クラスがあります。
そして、値を取得(getter) する getValue メソッドを、インタフェース(ReadOnlyA) に切り出します。

クラス図

これが、基本の形となります。

それでは、もう少し具体的な例で見てみましょう。

使わない例

まずは、読み取り専用インタフェースを使わない例です。

例として、商品を表す Product クラスを作ります。

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

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

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getPrice() {
        return price;
    }

    public void setPrice(int price) {
        this.price = price;
    }
}

name は商品名、price は値段です。
それぞれの値を設定/取得するためのメソッドもあります。

final var apple = new Product("りんご", 100);

System.out.println(apple.getName()); // りんご
System.out.println(apple.getPrice()); // 100

apple.setName("りんご (安売り中)");
apple.setPrice(50);

System.out.println(apple.getName()); // りんご (安売り中)
System.out.println(apple.getPrice()); // 50

使い方はこんな感じですね。

さて、この Product クラスを利用する、別のクラスを2つ追加しましょう。

1つ目は Printer クラスです。
Product の内容をコンソールへ表示するだけのクラスです。

public class Printer {

    public void print(Product product) {
        System.out.println("-------");
        System.out.println("商品名 : " + product.getName());
        System.out.println("値段  : " + product.getPrice() + "円");
    }
}
final var printer = new Printer();

final var apple = new Product("りんご", 100);
final var banana = new Product("バナナ", 200);

printer.print(apple);
printer.print(banana);

// 結果
// ↓
//-------
//商品名 : りんご
//値段  : 100円
//-------
//商品名 : バナナ
//値段  : 200円

もう1つは、Sale クラスです。
update メソッドで、セールのために値段を安く再設定します。

public class Sale {

    public void update(Product product) {
        final var name = product.getName();
        product.setName(name + " (安売り中)");

        final var price = product.getPrice();
        product.setPrice(price / 2);
    }
}
final var printer = new Printer();
final var sale = new Sale();

final var apple = new Product("りんご", 100);
printer.print(apple);

sale.update(apple);
printer.print(apple);

// 結果
// ↓
//-------
//商品名 : りんご
//値段  : 100円
//-------
//商品名 : りんご (安売り中)
//値段  : 50円

クラスの関係は次のようになります。

クラス図

Sale クラスは、 Product に対して値の 設定取得 が必要です。

しかし、Printer クラスは、Product の内容をただ表示するだけです。
明らかに 値の設定は不要 です。値の 取得だけ できれば問題ありません。

こんなときに使えるのが、読み取り専用インタフェースです。


使う例

それでは、読み取り専用インタフェースを作ってみましょう。

といっても大げさなものではありません。
単純に、値を取得するメソッドをインタフェースとして切り出すだけです。

最初に、ReadOnlyProduct インタフェースを宣言します。

public interface ReadOnlyProduct {
    String getName();

    int getPrice();
}

ReadOnlyProduct インタフェースには、値を取得する getNamegetPrice を定義します。

そして Product クラスでは、この ReadOnlyProduct インタフェースを実装(implements) します。

public class Product implements ReadOnlyProduct {
    private String name;
    private int price;

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

    @Override
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public int getPrice() {
        return price;
    }

    public void setPrice(int price) {
        this.price = price;
    }
}

最後に Printer.print メソッドのパラメータの型を、

 Product
 ↓
 ReadOnlyProduct

に変更します。

public class Printer {

    public void print(ReadOnlyProduct product) {
        System.out.println("-------");
        System.out.println("商品名 : " + product.getName());
        System.out.println("値段  : " + product.getPrice() + "円");
    }
}

これで、Printer の print メソッドでは、product オブジェクトの変更はできなくなりました。
値の取得のみが可能となります。

もちろん、次の例のように、値を変更する setName メソッドを呼び出すとコンパイルエラーになります。

public class Printer {

    public void print(ReadOnlyProduct product) {
        ...

        product.setName("aaa"); // コンパイルエラー
    }
}

各クラスの関係を図にすると次のようになります。

クラス図2

使い方は、読み取り専用インタフェースを使わないときと、特に変わりません。

final var printer = new Printer();
final var sale = new Sale();

final var apple = new Product("りんご", 100);
printer.print(apple);

sale.update(apple);
printer.print(apple);

// 結果
// ↓
//-------
//商品名 : りんご
//値段  : 100円
//-------
//商品名 : りんご (安売り中)
//値段  : 50円

注意点

変更可能なオブジェクトを返すメソッドは注意が必要です。
getter メソッド経由で、オブジェクトの変更が可能となってしまうからです。

コード例で見てみましょう。

public class A {
    private int value;

    public int getValue() {
        return value;
    }

    public void setValue(int value) {
        this.value = value;
    }
}
public interface ReadOnlyX {
    A getA();
}

public class X implements ReadOnlyX {
    private A a;

    @Override
    public A getA() {
        return a;
    }

    public void setA(A a) {
        this.a = a;
    }
}

クラス図3

ポイントは、クラスA が変更可能なオブジェクトであることです。

次の testReadOnly メソッドは、読み取り専用インタフェースの ReadOnlyX をパラメータとして受け取ります。
しかし、getA メソッドを経由してオブジェクトの変更が可能です。

public void testReadOnly(ReadOnlyX x) {
    // getter 経由でオブジェクトの変更が可能!
    x.getA().setValue(999);
}
final var x = new X();
x.setA(new A());

System.out.println(x.getA().getValue()); // 0

testReadOnly(x);

System.out.println(x.getA().getValue()); // 999

このように、変更可能なオブジェクトを返す getter メソッドは注意が必要です。
基本的には、このようなメソッドは読み取り専用インタフェースに含めないほうがよいでしょう。

もし、getA メソッドを読み取り専用インタフェースに含めたい場合は、次のようにします。

  • A クラスにも読み取り専用インタフェース(ReadOnlyA) を作る
  • getA メソッドで、ReadOnlyA を返す
public interface ReadOnlyA {
    int getValue();
}

public class A implements ReadOnlyA {
    private int value;

    @Override
    public int getValue() {
        return value;
    }

    public void setValue(int value) {
        this.value = value;
    }
}
public interface ReadOnlyX {
    ReadOnlyA getA();
}

public class X implements ReadOnlyX {
    private A a;

    @Override
    public ReadOnlyA getA() {
        return a;
    }

    public void setA(A a) {
        this.a = a;
    }
}

補足 (もう1つの方法)

もし、オブジェクトが比較的小さい場合は、読み取り専用のインタフェースを作らずに、必要なパラメータをまとめた新しいクラスを作ってしまうのも1つの方法です。

その場合は レコードクラス を使うのが便利でおすすめです。

具体的なコード例で見てみましょう。
先ほども使った Product クラスを例にします。

まずは、Product の 商品名(name) と 値段(price) を持つレコードクラスを作ります。

public record ConstProduct(String name, int price) {
}

そして、Product クラスから ConstProduct を生成できるようにします。

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

... 省略 ...

    public ConstProduct toConst() {
        return new ConstProduct(name, price);
    }
}

Product に対して 値の取得 しか必要のないところでは、Product の代わりに ConstProduct を使うようにします。

public class Printer {

    public void print(ConstProduct product) {
        System.out.println("-------");
        System.out.println("商品名 : " + product.name());
        System.out.println("値段  : " + product.price() + "円");
    }
}

読み取り専用のインタフェースを作るか、今回の例のように別のクラス(ConstProduct) に分けるべきか…どちらがよいかは難しいところです。

別のクラスにすることによって、オブジェクトを完全な 不変 にすることもできます。
それは別クラスにすることのメリットかもしれません。

不変オブジェクトのメリットについては、下記の記事にて解説しています。
合わせてご参照いただけたら幸いです。

まとめ

読み取り専用のインタフェースを作ることで、クラス・メソッドの公開範囲を

  • オブジェクトを変更しないメソッドのみ (読み取り専用)
  • すべてのメソッド

に分けることができます。

オブジェクトを変更するメソッドの公開範囲を必要最低限にすることで、想定外のオブジェクト変更が減ることが期待できます。
それは、不具合のリスクを減らし、プログラムの品質向上にもつながるでしょう。

ぜひ有効に活用していきたいですね。


関連記事

ページの先頭へ