複雑なプロジェクトを作成するときは、優れたコード カルチャを開発することが不可欠です。不変で一貫性のあるオブジェクトの使用は、最も重要なものの 1 つです。
これを無視して、複雑なオブジェクトを標準オブジェクトとして作成することはできますが、プロジェクトが十分に大きくなると、バグの重大な原因になります。
前回の記事で、標準オブジェクトの一貫性と信頼性を向上させる方法を示しました。一言で言えば:
値を設定するときに検証を追加する
null 許容フィールドごとにjava.util.Optional
を使用する
複雑なミューテーションを適切な場所 (責任のあるクラス自体) に配置します。
しかし、これらのアクションは、完全に信頼できるオブジェクトを作成するには十分ではありません。この記事では、オブジェクトを不変にする方法と、雄弁かつ効率的にオブジェクトを作成する方法を示します。
デフォルトのコンストラクター/ゲッター/セッターを使用して単純なシリアライズ可能なオブジェクトを作成する場合は、標準的な方法で問題ありません。しかし、もっと複雑なものを書くとしましょう。そしておそらく、無数の場所で使用されています。
たとえば、HashMaps やマルチスレッド環境で使用されます。したがって、デフォルトの方法で記述することは、もはや良い考えではないようです。 HashMap を壊すことは大したことではなく、スレッド間の矛盾した状態が長く待たされることはありません。
不変オブジェクトの作成について考えるとき、最初に頭に浮かぶことは次のとおりです。
しかし、そのようなオブジェクトとどのように共存するのでしょうか?変更する必要がある場合は、コピーを作成する必要があります。毎回コードやロジックをコピー&ペーストせずにうまくやるにはどうすればよいでしょうか?
アカウントがあるとしましょう。各アカウントにはid
、 status
、および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); }
不変で、一貫性があり、信頼できるオブジェクトと一緒に暮らすのは大変ですが、適切な方法で行うと、はるかに簡単になります。
いずれかを実装する場合、
Builder
を返す責任のあるクラス内にメソッドをcopy
し、それを使用してクラス内に新しいインスタンスを作成します。Optional
を使用する
GitHubで、より多くの単体テストを含む完全に機能する例を見つけることができます。
UnsplashのAdam Nieściorukによるリード写真