NHN Cloud Meetup 編集部
持続可能なソフトウェアのコーディング(4)不変クラス
2020.02.27
1,526
不変クラス(Immutable Class)
不変クラスとは、オブジェクト生成後に、その値を変更することができないことを意味します。Javaでは代表的な不変クラスとしてStringがあります。Integer、Doubleのようなプリミティブ型を包むラッパー(Wrapper)クラスも不変クラスです。今回は、不変クラスを使用する理由や、どのような利点があるかについて調べてみましょう。また、不変クラスの設計方法や見逃しがちな部分についても簡単に説明したいと思います。
不変クラスには次のような利点があります。
– スレッドセーフ(Thread-Safe)があり、マルチスレッド環境で安全に使用できる。
次のような価格表があるとしましょう。そして、このクラスは不変クラスがありません。このとき発生し得る状況を考えてみましょう。
// 問題発生の余地があるLombokアノテーションです。 @Data public class PriceTag { private Long priceTagId; private BigDecimal listPrice; private BigDecimal discountPrice; private BigDecimal memberShipPrice; public void initialize(PriceEntity priceEntity, DiscountEntity discountEntity, MemberShipEntity memberShipEntity){ this.priceTagId = priceEntity.getPriceId(); this.listPrice = priceEntity.getListPrice(); this.discountPrice = priceEntity.getListPrice() .subtract(discountEntity.getDiscountAmount()); this.memberShipPrice = priceEntity.getListPrice() .subtract(priceEntity.getListPrice().mutiply(memberShipEntity.getDiscountRate())); } }
上記のPriceTagクラスは価格表を意味し、さまざまなデータを組み合わせて、販売、割引、会員割引が存在します。もし、このクラスを次のように使用すると、コードは正常に動作するでしょうか?
public class OrderService{ public FinalPrice calculate(Long memberId, Long productId){ // CASE #1. デフォルトコンストラクタ(default constructor)によるオブジェクトを生成し、メンバー変数値が空のpriceTagオブジェクトを生成。 PriceTag priceTag = new PriceTag(); //... コード省略 this.applyMemberShip(priceTag); //... コード省略 return priceTag.getDiscountPrice(); } private void applyMemberShip(PriceTag priceTag){ // CASE #2 引数の値を変更する場合。最初の記事でやってはいけないアクションであると話しましたね。 BigDecimal memberShipPrice = // 省略 priceTag.setMemberShipPrice(memberShipPrice); } }
OrderServiceオブジェクトのcalculate()メソッドのうち、priceTagオブジェクトの値はprivate applyMemberShip()メソッドによって変更されます。applyMemberShip()メソッドの実行順序が変われば、calculate()メソッドの結果値は以前とは違う形で出力されるかもしれません。みなさんはこのようなPriceTagオブジェクトを信用することができますか?そこで、不変オブジェクトを作成して使用しましょう。不変クラスを設計する方法は次のとおりです。
2.クラスのメンバー変数を必ずfinalで宣言する
3.コンストラクタをうまく管理すること
4.メンバー変数はsetterメソッドではなく、getterメソッドを作って使用すること
上記の方法で、もう一度PriceTagを設計してみましょう。
// 4番の項目を適用しました。 @Getter // 1番の項目を適用しました。 public final class PriceTag { // 2番の項目を適用しました。 private final Long priceTagId; private final BigDecimal listPrice; private final BigDecimal discountPrice; private final BigDecimal memberShipPrice; // 3番の項目を適用しました。 public static PriceTag of(PriceEntity priceEntity, DiscountEntity discountEntity, MemberShipEntity memberShipEntity){ PriceTag priceTag = new PriceTag(); priceTag.priceTagId = priceEntity.getPriceId(); priceTag.listPrice = priceEntity.getListPrice(); priceTag.discountPrice = priceEntity.getListPrice().subtract(discountEntity.getDiscountAmount()); priceTag.memberShipPrice = priceEntity.getListPrice().subtract(priceEntity.getListPrice().mutiply(memberShipEntity.getDiscountRate())); return priceTag; } // 3番の項目の一部です。 // デフォルトコンストラクタをprivateで宣言し、外部でデフォルトコンストラクタを作成できないようにします。 private PriceTag() { } }
このようにリファクタリングすると、次のようなPriceTagのメソッドは使用することができません。
// メンバー変数が空であるpriceTagの状態を出力できません。 // 上記PriceTagのコンストラクタをprivateで宣言したためです。 PriceTag priceTag = new PriceTag(); // メンバー変数が次のメソッドによって途中で変更されることはありません。 priceTag.initliaze(......) // 途中でmemberShipPriceが変更されたpriceTagの状態が出力できません。 priceTag.setMemberShipPrice(new BigDecimal("1000.00"));
不変クラスにおいて見逃しがちな部分
- 不変クラス作成時にコンストラクタを管理できない場合
Javaではコンストラクタを宣言しなければ、デフォルトコンストラクタ(default constructor)が自動的に生じ、他のクラスからこれを自由に呼び出すことができます。当該クラスを苦労して設計しても、デフォルトコンストラクタが存在し、複数の状態(state)を持つオブジェクトを作成することができます。これではもはや不変クラスとは言えません。したがって、上記のPriceTagのサンプルのようにプライベートコンストラクタ(private constructor)を宣言しましょう。PriceTagの使用者は、このオブジェクトが不変(immutable)かどうかを気にすることなく、生成できるすべての方法で作成します。 - 不変クラスはTestCaseを作成するとき不便である
上記のPriceTagオブジェクトを生成するためには、staticファクトリーメソッドであるof()を使用する必要があります。したがって、PriceEntity priceEntity、DiscountEntity discountEntity、MemberShipEntity memberShipEntityオブジェクトをすべて作成する必要があります。しかし、Javaリフレクションを使用すれば、ある程度は楽になります。SpringではReflectionTestUtilsのようなユーティリティ性クラスもあります。もちろんコードの変化に影響を受けやすいですが、不変クラスが与えるメリットはそれをカバーするのに十分です。 - 次のようなコードは作成できない
次のコードのように、finalを宣言したとしてもimmutableListを作成することができません。
@Test public void testImmutable(){ final List<String> immutableList = new ArrayList<>(); // finalはimmutableと何ら関連しません。 immutableList.add("1"); System.out.println(immutableList); }
- クラス宣言のとき、finalキーワードを使用すること
多くの開発者がクラスをfinalキーワードとして宣言していないようです。上記のPriceTagのクラス宣言を確認してみましょう。finalキーワードを宣言しないと、クラスの継承が発生する可能性があり、PriceTagのメソッドがオーバーライドされてしまうことがあります。したがって不変クラスになることができません。
表明プログラミングと不変クラス
実用主義のプログラマーは表明プログラミングをするように提案しています。これは、絶対に起こることがない状況を作るという意味です。上記のPriceTagクラスを例に挙げてみましょう。一般的な価格表において、次のような状況は発生すべきではありません。
> 開発条件です。
> 代金の支払いは、購入者から生産者/販売者に支払う流れです。
> 割引したとき、基本価格より高くなってはいけませんね。
このような状況が発生しないように表明することが重要です。そうすることで、PriceTagオブジェクトのメソッドを使用するオブジェクトは信頼性があり、強固なプログラミングになることができます。これを適用すると、次のようにコーディングできます。
public static PriceTag of(PriceEntity priceEntity, DiscountEntity discountEntity, MemberShipEntity memberShipEntity){ if (priceEntity == null) throw new IllegalArgumentException("priceEntity is null"); if (priceEntity.getListPrice() == null) throw new IllegalArgumentException("priceEntity.listPrice is null #" + priceEntity.getPriceId()); if (priceEntity.getListPrice().doubleValue() < 0) throw new IllegalStateException("priceEntity.listPrice is negative #" + priceEntity.getPriceId()); PriceTag priceTag = new PriceTag(); // 省略 if (priceTag.discountPrice > priceTag.listPrice || priceTag.memberShipPrice > priceTag.listPrice) throw new IllegalStateException("discountPrice, memberShipPrice is bigger than list price #" + priceEntity.getPriceId()); return priceTag; }
このように、さまざまな状況について表明すると、より強固な不変オブジェクトを作成することができます。この表明プログラミングは、不変クラスを設計するときだけでなく、一部のメソッドで引数を検証するために使用することもできます。
クラス不変式(Class Invariant)
モジュール(class)は、互いのインターフェース(method)を介して相互にデータを転送し、それぞれ有機的に動作して1つの機能(Feature)を実行します。このとき、オブジェクトが互いに信頼できなければ、プログラムはどうなってしまうでしょう。
Eiffel言語の創始者であるバートランド・メイヤーは、契約による設計(Design By Contract)という概念を提示しています。ソフトウェアのモジュールは、権利と責任を文書化し、それらを検証することが重要になります。たとえば、送料、税金、品物の価格を連結するメソッドがあるとしましょう。そのメソッドの責任は、3つの料金を計算して全体の金額を返すことです。そして、引数がnull値または負の値であれば例外を返すことが権利です。メイヤーは次のような3つの状態を提案しています。
– 後行条件 : メソッド実行後、リターンしてからの状態
– クラス不変式 : 呼び出し者の立場で、この条件が常に真であるとクラスが保証すること
表明プログラミング + 不変クラスがクラス不変式の基本だと言えます。したがって、PriceTagも不変クラスであり、クラス不変式だと言えるでしょう。コンストラクタを呼び出すと、そのクラスのメンバー変数の値は、次のような条件が常に真となります。
PriceTagクラスのlistPriceは決して負数になれない
PriceTagクラスのdiscountPriceやmemberShipPriceは、listPriceより大ではない
クラス不変式は、不変クラスやEntityクラスを加えて、以下のように作成することができます。下記のgetTotalPrice()が呼び出されると、戻り値は常に0以上であり、not nullである条件が常に真です。
public class OrderService { //... public BigDecimal getTotalPrice(BigDecimal listPrice, BigDecimal deliverPrice, Double taxRate) { this.assertNotNull(listPrice); this.assertNotNull(deliverPrice); this.assertNotNull(taxRate); this.assertPositive(listPrice); this.assertPositive(deliverPrice); this.assertPositive(taxRate); BigDecimal tax = listPrice.multiply(new BigDecimal(taxRate)); return listPrice.add(tax).add(deliverPrice); } //... }