paint-brush
Java で不変で信頼できるオブジェクトを扱う方法を学ぶ@artemsutulov
624 測定値
624 測定値

Java で不変で信頼できるオブジェクトを扱う方法を学ぶ

Artem Sutulov1m2022/05/28
Read on Terminal Reader
Read this story w/o Javascript

長すぎる; 読むには

複雑なプロジェクトを書くときは、優れたコード文化を育むことが不可欠です。不変で一貫性のあるオブジェクトの使用は、最も重要なものの 1 つです。これを無視して、複雑なオブジェクトを標準オブジェクトとして作成することはできますが、プロジェクトが十分に大きくなると、バグの原因になります。この記事では、オブジェクトを不変にする方法と、それらを雄弁かつ効率的にする方法を示します。デフォルトの方法では、セッターのないオブジェクトにはセッターがありません。これを行う唯一の方法は、新しいインスタンスを作成し、責任のあるクラス自体に新しい値を配置することです。

Companies Mentioned

Mention Thumbnail
Mention Thumbnail
featured image - Java で不変で信頼できるオブジェクトを扱う方法を学ぶ
Artem Sutulov HackerNoon profile picture

複雑なプロジェクトを作成するときは、優れたコード カルチャを開発することが不可欠です。不変で一貫性のあるオブジェクトの使用は、最も重要なものの 1 つです。


これを無視して、複雑なオブジェクトを標準オブジェクトとして作成することはできますが、プロジェクトが十分に大きくなると、バグの重大な原因になります。


前回の記事で、標準オブジェクトの一貫性と信頼性を向上させる方法を示しました。一言で言えば:


  • 値を設定するときに検証を追加する

  • null 許容フィールドごとにjava.util.Optionalを使用する

  • 複雑なミューテーションを適切な場所 (責任のあるクラス自体) に配置します。


しかし、これらのアクションは、完全に信頼できるオブジェクトを作成するには十分ではありません。この記事では、オブジェクトを不変にする方法と、雄弁かつ効率的にオブジェクトを作成する方法を示します。

問題

デフォルトのコンストラクター/ゲッター/セッターを使用して単純なシリアライズ可能なオブジェクトを作成する場合は、標準的な方法で問題ありません。しかし、もっと複雑なものを書くとしましょう。そしておそらく、無数の場所で使用されています。


たとえば、HashMaps やマルチスレッド環境で使用されます。したがって、デフォルトの方法で記述することは、もはや良い考えではないようです。 HashMap を壊すことは大したことではなく、スレッド間の矛盾した状態が長く待たされることはありません。


不変オブジェクトの作成について考えるとき、最初に頭に浮かぶことは次のとおりです。


  1. setter メソッドを作成しない
  2. すべてのフィールドを final にする
  3. インスタンスを可変オブジェクトと共有しない
  4. サブクラスによるメソッドのオーバーライドを許可しない (この記事では省略します)


しかし、そのようなオブジェクトとどのように共存するのでしょうか?変更する必要がある場合は、コピーを作成する必要があります。毎回コードやロジックをコピー&ペーストせずにうまくやるにはどうすればよいでしょうか?

サンプルクラスについて一言

アカウントがあるとしましょう。各アカウントにはidstatus 、およびemailがあります。アカウントはメールで確認できます。ステータスがCREATEDの場合、メールが入力されるとは想定していません。ただし、それがVERIFIEDまたはINACTIVEの場合は、電子メールを入力する必要があります。



アカウントのステータス


 public enum AccountStatus { CREATED, VERIFIED, INACTIVE }


正規のAccount.java実装:


 public class Account { private final String id; private final AccountStatus status; private final String email; public Account(String id, AccountStatus status, String email) { this.id = id; this.status = status; this.email = email; } // equals / hashCode / getters }


アカウントを作成したとしましょう。次に、ビジネス ロジックのどこかで、メールを変更する必要があります。


 var account = new Account("some-id", CREATED, null);


どうすればそれができますか?デフォルトの方法は機能しません。不変クラスを持つセッターを持つことはできません。


 account.setEmail("[email protected]");// we can not do that, we have no setters


これを行う唯一の方法は、新しいインスタンスを作成し、以前の値をコンストラクターに配置することです。


 var withEmail = new Account(account.getId(), CREATED, "[email protected]");


しかし、これはフィールドの値を変更する最良の方法ではありません。大量のコピー/貼り付けが発生し、Account クラスはその一貫性に責任を負いません。

解決

推奨される解決策は、Account クラスからミューテーション メソッドを提供し、責任のあるクラス内にコピー ロジックを実装することです。また、必要な検証を追加し、電子メール フィールドに Optional を使用することが不可欠であるため、NPE や一貫性の問題は発生しません。


オブジェクトを構築するには、'Builder' パターンを使用します。これはかなり有名で、IDE が自動的に生成するためのプラグインがたくさんあります。


 public class Account { private final String id; private final AccountStatus status; private final Optional<String> email; public Account(Builder builder) { this.id = notEmpty(builder.id); this.status = notNull(builder.status); this.email = checkEmail(builder.email); } public Account verify(String email) { return copy() .status(VERIFIED) .email(of(email)) .build(); } public Account changeEmail(String email) { return copy() .email(of(email)) .build(); } public Account deactivate() { return copy() .status(INACTIVE) .build(); } private Optional<String> checkEmail(Optional<String> email) { isTrue( notNull(email).map(StringUtils::isNotBlank).orElse(false) || this.status.equals(CREATED), "Email must be filled when status %s", this.status ); return email; } public static final class Builder { private String id; private AccountStatus status; private Optional<String> email = empty(); private Builder() { } public static Builder account() { return new Builder(); } public Builder id(String id) { this.id = id; return this; } public Builder status(AccountStatus status) { this.status = status; return this; } public Builder email(Optional<String> email) { this.email = email; return this; } public Account build() { return new Account(this); } } // equals / hashCode / getters }


ご覧のとおり、このクラスには完全なコピーでBuilderを返すプライベート メソッドcopyがあります。これにより、すべてのフィールドのコピーと貼り付けが不要になりますが、このメソッドがAccount.javaの外部からアクセスできないことが重要です。そのビルダーが外部にあると、状態と一貫性を制御できなくなるためです。

新しい方法でアカウントを変更する

それでは、アカウントを作成しましょう。


 var account = account() .id("example-id") .status(CREATED) .email((empty()) .build();


メールを変更する必要がある場合、コピーを作成する必要はありません。 Account自体からメソッドを呼び出すだけです。


 var withNewEmail = account.changeEmail("[email protected]");


単体テストでのデモンストレーション:


 @Test void should_successfully_change_email() { // given var account = account() .id("example-id") .status(VERIFIED) .email(of("[email protected]")) .build(); var newEmail = "[email protected]"; // when var withNewEmail = account.changeEmail(newEmail); // then assertThat(withNewEmail.getId()).isEqualTo(account.getId()); assertThat(withNewEmail.getStatus()).isEqualTo(account.getStatus()); assertThat(withNewEmail.getEmail()).isEqualTo(of(newEmail)); }


アカウントを確認するために、ステータスがVERIFIEDで新しい電子メールのコピーは作成されません。メソッドverifyを呼び出すだけで、コピーが作成されるだけでなく、電子メールの有効性もチェックされます。


 @Test void should_successfully_verify_account() { // given var created = account() .id("example-id") .status(CREATED) .build(); var email = "[email protected]"; // when var verified = created.verify(email); // then assertThat(verified.getId()).isEqualTo(created.getId()); assertThat(verified.getStatus()).isEqualTo(VERIFIED); assertThat(verified.getEmail().get()).isEqualTo(email); }


結論

不変で、一貫性があり、信頼できるオブジェクトと一緒に暮らすのは大変ですが、適切な方法で行うと、はるかに簡単になります。


いずれかを実装する場合、忘れないで:

  1. すべてのフィールドを final にする
  2. セッターを提供しない
  3. 可変オブジェクトへのリンクを共有しない
  4. サブクラスがメソッドをオーバーライドできないようにする
  5. クラスからミューテーション メソッドを提供する
  6. 実装するプライベートBuilderを返す責任のあるクラス内にメソッドをcopyし、それを使用してクラス内に新しいインスタンスを作成します。
  7. 値の検証を使用してフィールドの一貫性を維持する
  8. Null 許容フィールドでOptionalを使用する


GitHubで、より多くの単体テストを含む完全に機能する例を見つけることができます。


UnsplashAdam Nieściorukによるリード写真