Javaのエレガントなビルダー

確かに、経験豊富なプログラマーのほとんどはBuilderパターンに精通しています。 これにより、有用なプロパティを不変性として維持しながら、データ構造の初期化をより視覚的で柔軟にすることができます。 Googleを発行する最初のページから「javaビルダーパターンの例」というクエリに対して古典的な例を示します。 すべての利点を備えたこのパターン実装の最も重要な欠点は、通常のフラットBeanの2倍のコードがあることです。 この追加コードの生成が一般的なIDEにとって問題でない場合、そのようなクラスの編集は非常に面倒になり、とにかく読みやすさが低下します。



ある時点で、それに耐えるのに十分であると判断し、代替案を探しました。 私は言わなければならない、代替案は非常に迅速に発見されました。 Javaには、静的ではない内部クラスに対してめったに使用されないメカニズムがあります。 このようなクラスのインスタンスは、.new演算子を使用して、親クラスのインスタンスを介してのみ作成できます。 重要なのは、そのようなオブジェクトがその親のプライベートフィールドにアクセスできることです。



したがって、不変の構造があります



public class Account { private final String userId; private final String token; public Account(String token, String userId) { this.token = token; this.userId = userId; } public String getUserId() { return userId; } public String getToken() { return token; } }
      
      





ここには2つのフィールドしかありませんが、ビルダーは、コンストラクター内のパラメーターの順序を混同しないようにし、2つまたは両方から1つのフィールドのみを初期化する必要がある場合でも、異なる時点で役立ちます。 フィールドが20になるとき、私は何を言うことができます!



ビルダークラスのフィールドを複製しないように、単純に内部クラスを開始します。 彼は、親クラスのプライベートフィールドにアクセスでき、それらを直接公開できます。 クラスのプライベートコンストラクターをプライベートにし、フィールドから最終修飾子を削除します。



 public class Account { private String userId; private String token; private Account() { // private constructor } public String getUserId() { return userId; } public String getToken() { return token; } public class Builder { private Builder() { // private constructor } public Builder setUserId(String userId) { Account.this.userId = userId; return this; } public Builder setToken(String token) { Account.this.token = token; return this; } public Account build() { return Account.this; } } }
      
      





ビルダーのコンストラクターもプライベートです。それ以外の場合は、アカウントインスタンスにアクセスできます。ビルダーを作成し、それを介して既に作成されたオブジェクトのフィールドを変更できます。 buildメソッドは、既製のオブジェクトを返すだけです(たとえば、必要なフィールドがすべて揃っているかどうかを確認できます)。



最後の仕上げ-メソッドに追加して、ビルダーのインスタンスを作成します。



 public class Account { private String userId; private String token; private Account() { // private constructor } public String getUserId() { return userId; } public String getToken() { return token; } public static Builder newBuilder() { return new Account().new Builder(); } public class Builder { private Builder() { // private constructor } public Builder setUserId(String userId) { Account.this.userId = userId; return this; } public Builder setToken(String token) { Account.this.token = token; return this; } public Account build() { return Account.this; } } }
      
      





従来の実装と比較してください:



 public class Account { private final String userId; private final String token; public Account(String userId, String token) { this.userId = userId; this.token = token; } public String getUserId() { return userId; } public String getToken() { return token; } public static class Builder { private String userId; private String token; public Builder setUserId(String userId) { this.userId = userId; return this; } public Builder setToken(String token) { this.token = token; return this; } public Account build() { return new Account(userId, token); } } }
      
      





いずれの場合も、新しいフィールドを追加するか、トークンフィールドのタイプを変更してください。 フィールドの数が増えると、コードサイズと可読性の違いがより顕著になります。 トピックの冒頭で参照した記事の例を比較します(例のスタイルに合わせて変更しました)。



 public class Person { private final String lastName; private final String firstName; private final String middleName; private final String salutation; private final String suffix; private final String streetAddress; private final String city; private final String state; private final boolean isFemale; private final boolean isEmployed; private final boolean isHomeOwner; public Person( final String newLastName, final String newFirstName, final String newMiddleName, final String newSalutation, final String newSuffix, final String newStreetAddress, final String newCity, final String newState, final boolean newIsFemale, final boolean newIsEmployed, final boolean newIsHomeOwner) { this.lastName = newLastName; this.firstName = newFirstName; this.middleName = newMiddleName; this.salutation = newSalutation; this.suffix = newSuffix; this.streetAddress = newStreetAddress; this.city = newCity; this.state = newState; this.isFemale = newIsFemale; this.isEmployed = newIsEmployed; this.isHomeOwner = newIsHomeOwner; } public String getLastName() { return lastName; } public String getFirstName() { return firstName; } public String getMiddleName() { return middleName; } public String getSalutation() { return salutation; } public String getSuffix() { return suffix; } public String getStreetAddress() { return streetAddress; } public String getCity() { return city; } public String getState() { return state; } public boolean isFemale() { return isFemale; } public boolean isEmployed() { return isEmployed; } public boolean isHomeOwner() { return isHomeOwner; } public static class Builder { private String nestedLastName; private String nestedFirstName; private String nestedMiddleName; private String nestedSalutation; private String nestedSuffix; private String nestedStreetAddress; private String nestedCity; private String nestedState; private boolean nestedIsFemale; private boolean nestedIsEmployed; private boolean nestedIsHomeOwner; public Builder setNestedLastName(String nestedLastName) { this.nestedLastName = nestedLastName; return this; } public Builder setNestedFirstName(String nestedFirstName) { this.nestedFirstName = nestedFirstName; return this; } public Builder setNestedMiddleName(String nestedMiddleName) { this.nestedMiddleName = nestedMiddleName; return this; } public Builder setNestedSalutation(String nestedSalutation) { this.nestedSalutation = nestedSalutation; return this; } public Builder setNestedSuffix(String nestedSuffix) { this.nestedSuffix = nestedSuffix; return this; } public Builder setNestedStreetAddress(String nestedStreetAddress) { this.nestedStreetAddress = nestedStreetAddress; return this; } public Builder setNestedCity(String nestedCity) { this.nestedCity = nestedCity; return this; } public Builder setNestedState(String nestedState) { this.nestedState = nestedState; return this; } public Builder setNestedIsFemale(boolean nestedIsFemale) { this.nestedIsFemale = nestedIsFemale; return this; } public Builder setNestedIsEmployed(boolean nestedIsEmployed) { this.nestedIsEmployed = nestedIsEmployed; return this; } public Builder setNestedIsHomeOwner(boolean nestedIsHomeOwner) { this.nestedIsHomeOwner = nestedIsHomeOwner; return this; } public Person build() { return new Person( nestedLastName, nestedFirstName, nestedMiddleName, nestedSalutation, nestedSuffix, nestedStreetAddress, nestedCity, nestedState, nestedIsFemale, nestedIsEmployed, nestedIsHomeOwner); } } }
      
      





そして、内部クラスを介した実装:



 public class Person { private String lastName; private String firstName; private String middleName; private String salutation; private String suffix; private String streetAddress; private String city; private String state; private boolean isFemale; private boolean isEmployed; private boolean isHomeOwner; private Person() { // private constructor } public String getLastName() { return lastName; } public String getFirstName() { return firstName; } public String getMiddleName() { return middleName; } public String getSalutation() { return salutation; } public String getSuffix() { return suffix; } public String getStreetAddress() { return streetAddress; } public String getCity() { return city; } public String getState() { return state; } public boolean isFemale() { return isFemale; } public boolean isEmployed() { return isEmployed; } public boolean isHomeOwner() { return isHomeOwner; } public static Builder newBuilder() { return new Person().new Builder(); } public class Builder { private Builder() { // private constructor } public Builder setLastName(String lastName) { Person.this.lastName = lastName; return this; } public Builder setFirstName(String firstName) { Person.this.firstName = firstName; return this; } public Builder setMiddleName(String middleName) { Person.this.middleName = middleName; return this; } public Builder setSalutation(String salutation) { Person.this.salutation = salutation; return this; } public Builder setSuffix(String suffix) { Person.this.suffix = suffix; return this; } public Builder setStreetAddress(String streetAddress) { Person.this.streetAddress = streetAddress; return this; } public Builder setCity(String city) { Person.this.city = city; return this; } public Builder setState(String state) { Person.this.state = state; return this; } public Builder setFemale(boolean isFemale) { Person.this.isFemale = isFemale; return this; } public Builder setEmployed(boolean isEmployed) { Person.this.isEmployed = isEmployed; return this; } public Builder setHomeOwner(boolean isHomeOwner) { Person.this.isHomeOwner = isHomeOwner; return this; } public Person build() { return Person.this; } } }
      
      





コードを整理するという観点から、このようなクラスは、セッターが個別の内部クラスにグループ化され、いくつかのnewBuilder()およびbuild()メソッドが追加されるという点でのみ、フィールドとゲッターセッターを持つ通常のフラットBeanと異なり、内部クラスの宣言を持つ行とプライベートデザイナー。



重要な注意事項:



1.ビルダーのビルドメソッドは同じオブジェクトを返します。呼び出した後、ビルダーのメソッドを使用してフィールドを設定し続けると、既に作成されたオブジェクトのフィールドが変更されます。 オブジェクトの新しいインスタンスを毎回作成する場合、これは簡単に修正できます。



 public Account build() { Account account = new Account(); account.userId = Account.this.userId; account.token = Account.this.token; return account; }
      
      





これは、削除しようとしていた重複コードの一部を返します。 通常、ビルダーへのリンクはメソッドを終了しないので、最初に示したオプションを好みます。 ビルダーを頻繁にやり取りし、オブジェクトを再生成するために再利用する場合は、上記のオプションを使用します。



2.コメンテーターのおかげで、私の疑問は払拭されました-そのようなビルダーから取得したオブジェクトは、そのフィールドが最終として宣言されていないという事実のためにスレッドセーフではありません。 この点がアプリケーションで重要な場合は、クラシックビルダーを使用することをお勧めします。



そして最後に-ビルダーの使用。



 Account account = Account.newBuilder() .setToken("hello") .setUserId("habr") .build();
      
      





まあ



 Account.Builder accountBuilder = Account.newBuilder(); ... accountBuilder.setToken("hello"); ... accountBuilder..setUserId("habr"); return accountBuilder.build();
      
      





繰り返しになりますが、Account.newBuilder()は、新しいAccount.Builder()よりもプログラマーの目にきれいです。ただし、これはすでに好みの問題です。



すべてきれいなコード!



UPD:Habréでよく発生するように、コメントはトピック自体よりも有用であることが判明したため、慣れるのをお勧めします。



All Articles