getterにロジックを書かない方がいい

getterにはロジックを書かない方がいい。

単純なgetterとロジックを含むgetterの判別がつきにくい

理由の一つは、単純なgetterとロジックを含むgetterの判別が利用者側からつきにくくなるから。

例えばsalePrice, saleStart, saleEndという3つのフィールドがJavaBeansの中にあるとする。このとき、getSalePrice()が単純にsalePriceを返すのか、あるいは内部で現在日時とsaleStart, saleEndを比較し、有効な場合のみsalePriceを返すのか、Javadocをみたり実装をみたりしないとわからない。

コードを書くときにJavadocをみたり実装をみたりするのはいいとして、コードを読むときにも利用するgetterのJavadocや実装をみなければいけないようなコードはいいコードとはいえない。

本来のフィールド値を取得したい場合に困る

もう一つの理由は、本来のフィールド値を取得したい場合に困るから。

先ほどのsalePriceの例だと、現在日時とは関係なくsalePriceの値が欲しい場合に、getterにロジックが書いてあると困る。

ロジックといえないようなものは本記事の対象外

本記事で対象としているのは、先ほど例をあげたような金額の有効性を判定するようなロジックを含むものである。

例えば外部のREST APIを呼び出すときのRequestBodyのJSONにnullが許可されておらず、空のデータを示す際に空文字""を使う場合などで、JavaBeansのgetterでnullを空文字に変換する程度のロジックといえないようなものは、積極的にgetterの中にコードを書いていい。

public class RequestBody {

    private String something;

    public String getSomething() {
        return Objects.requireNonNullElse(something, "");
    }

}

getterにロジックを書く代わりに独自メソッドを用意する

いいコードにするには、getterにロジックを書く代わりに専用のメソッドを用意する。

先程のgetSalePrice()の例でいえば、computeSalePrice()validSalePrice()のような名前でメソッドを用意する。

private String salePrice;
private LocalDateTime saleStart;
private LocalDateTime saleEnd;

public String computeSalePrice() {
    if (salePrice == null || saleStart == null || saleEnd == null) {
        return null;
    }

    var now = LocalDateTime.now();
    if (now.compareTo(saleStart) >= 0 && now.compareTo(saleEnd) <= 0) {
        return salePrice;
    } else {
        return null;
    }
}

また、フィールド値をそのまま返すメソッドも必要で、並べて書く際にはrawSalePrice()のように、そのまま値を返していることがわかる名前にするといい。

フレームワークやライブラリの都合で、JavaBeansにgetterとsetterを書かざるを得ない場合

フレームワークやライブラリの都合で、JavaBeansにgetterとsetterを書かざるを得ない場合は、getterと独自メソッドを併用するしかない。

ただJavaBeansを利用する側が誤ってgetterを利用しないようにしなければならない。getterはあくまでフレームワーク/ライブラリ用に定義しているだけであって、人間が利用してはいけない。

誤ってgetterを利用するのを防ぐために@Deprecatedアノテーションをgetterに付与する。

private String salePrice;
private LocalDateTime saleStart;
private LocalDateTime saleEnd;

/**
 * <pre>
 * フレームワーク/ライブラリ用に定義する。
 * 開発者はcomputeSalePrice()を利用する。
 * </pre>
 * @see #computeSalePrice
 */
@Deprecated
public String getSalePrice() {
    return this.salePrice;
}

public String computeSalePrice() {
    略
}

このようにしておけば、getterを利用した際にIDEが警告を出してくれるし、CIでSpotBugs等によりミスを検知することも可能となる。

Lombokのgetterを上書きする

現在のLTSはJava11だが、recordが未対応のこともあり、実際のJavaの開発においては、Lombokを利用していることが多いと思う。

getter/setterを求めるフレームワークに応じるために、Lombokの@Data@Getterをclassに付与していても、独自メソッドを用意するフィールドに対しては、getterをあえて定義した上で@Deprecatedを付与すればよい。

@Data
public class Product {

    private String salePrice; // @Dataによりsetterが生成される。getterは自分で定義したものが利用される
    private LocalDateTime saleStart; // @Dataによりgetter/setterが生成される
    private LocalDateTime saleEnd; // @Dataによりgetter/setterが生成される

    /**
     * <pre>
     * フレームワーク/ライブラリ用に定義する。
     * 開発者はcomputeSalePrice()を利用する。
     * </pre>
     * @see #computeSalePrice
     */
    @Deprecated
    public String getSalePrice() {
        return this.salePrice;
    }

    public String computeSalePrice() {
        略
    }

}

参考

こちらの記事(JavaBeans 規約に従いつつカプセル化もしたいのですが)を参考にした。

全てこちらの記事通りにしているわけではないが、以下の部分が特に参考になった。

以前システム設計の増田さんの講演をお聴きする機会があったんですが、そこで「アクセッサはドメインオブジェクトのロジックではない。だからアクセッサは禁止している。*3」というようなことを話されていました。ここで前述の通り「ライブラリ・フレームワークの都合上 getter/setter が必要になる場合があるはずだけどどうするんだろう」と疑問に思ったのですが、なんと getter/setter に deprecated を付けることで、ライブラリ・フレームワークに対応しつつプロダクトコードからの使用を事実上禁止しているということでした。これはなるほど!と思ったものです。