web-dev-qa-db-ja.com

インターフェイス内のビルダーパターン。悪い設計決定?

私が思いついたビルダーパターンの実装を評価するのに苦労しています。

コンテキストはAPIライブラリーなので、実装を後で変更できるようにしながら、安定したインターフェースを持つために実装を公開しないようにしています。

私の考えでは、インターフェイスのファイルに実装への参照があっても、インターフェイスと実装は実際には結合されていません。

事実上、静的にネストされたクラスは、動作的にはトップレベルのクラスです...

この設計の利点は、クライアントが

Person person = new Person.Builder().age(20).firstName("John") .lastName("Doe").build();

専用のFactoryタイプクラスは必要ありません。

何かが足りないのですか、それともこれは有効な設計決定ですか?

Person.Java

package net.mhi.rd;

public interface Person {

    int getAge();
    String getFirstName();
    String getLastName();

    public static class Builder {
        int age;
        String firstName;
        String lastName;

        public Builder age(int age) {
            this.age = age;
            return this;
        }

        public Builder firstName(String firstName) {
            this.firstName = firstName;
            return this;
        }

        public Builder lastName(String lastName) {
            this.lastName = lastName;
            return this;
        }

        public Person build() {
            return new PersonImpl(this);
        }
    }    
}

PersonImpl.Java

package net.mhi.rd;

class PersonImpl implements Person {

    private final int age;
    private final String firstName;
    private final String lastName;

    PersonImpl(Person.Builder builder) {
        this.age = builder.age;
        this.firstName = builder.firstName;
        this.lastName = builder.lastName;
    }

    @Override
    public int getAge() {
        return age;
    }

    @Override
    public String getFirstName() {
        return firstName;
    }

    @Override
    public String getLastName() {
        return lastName;
    }
}
5
mhi

主な問題は、PersonとPersonImplの間に循環依存関係を作成したことです... Personは実装クラスへの参照を持つべきではありませんが、ビルダーの「new PersonImpl()」呼び出しは、ハードなコンパイル時依存関係を作成します具体的なクラスで。

  • 修正は、PersonBuilderを独自のクラスにスピンオフするか、PersonBuilderをPersonImplに移動することだと思います。とにかくインターフェース内でクラスを宣言するのは少し奇妙です-私がこれまで一度も行ったことがないとは言えませんが、インターフェースをシンプルに保つことが望ましいようです。

  • PersonImplでビルダーをコンストラクター引数として渡すと、実装クラスがビルダーに密接に結合されます。あなたはビルダーとビルダーの間のより明確な分離を保つ方が良いでしょう。ビルダーは(明らかに)PersonImplについて知っている必要がありますが、PersonImplがビルダーを参照しなかった場合に最適です。

  • Personの実装が複数あると予想しますか?それはデータクラスのように見えますが、最初にインターフェイスを抽出する必要があるかどうかはわかりません。

  • そして、一般的なパターンとして、このようなクラスを数十個作成する場合、このパターンはかなり面倒になると思います。各クラスにインターフェース、実装、およびビルダーがあります。これは、別個のインターフェースを必要としない可能性がある何かのための多くの追加コードのようです。

Josh Blockが思いついたBuilderのバリエーションを以下に示します。内部クラスを使用して内部値にアクセスおよび設定する点が似ているため、クラスは効果的に不変になります。あなたのコードとの主な違いは、独立したインターフェースがないということです。

http://rwhansen.blogspot.com/2007/07/theres-builder-pattern-that-joshua.html

TL; DR

  • 依存関係はビルダーに行く必要があります-> Impl->インターフェイス

  • インターフェイスを省略し、ビルダーをImpl内にネストすると、ビルダーと実装の間の循環依存関係を隠しながら、内部実装の詳細に1回限りのアクセスを提供できます。

10
Rob

私は、Robが作成した冒頭の段落とポイント#3に非常に同意します。それらについては、私の意見では最も深刻な設計上の欠陥であるため、詳しく説明します。

複数の種類のPerson実装を作成する場合は、実際にPersonのインターフェースを作成する必要があるだけです。そうでなければ、それを持っている理由は本当にありません。 Personインターフェースを使用すると、プログラムの他の部分に影響を与えずにPersonImplクラスを変更できると誰かが主張するかもしれません。より具体的には、このインターフェースは、プログラムの外部の部分がPersonImplと直接通信することを防ぎます。これは、疎結合に適していることがわかっています。ただし、Personインターフェースの実装が1つしか存在しない場合、それはそれを使用する目的がなく、追加のコードになるだけであることを意味します。コードが増えると複雑さが増し、複雑さが増すため、コードの読み取り、理解、維持が難しくなります。

1種類以上のPersonを使用する予定で、インターフェイスで実装クラスへの明示的な参照を設定するという仮定のもとでは、インターフェイスを新しい実装に拡張することが難しくなるため、間違いです。自分がしていることをしている理由を常に思い出してください。インターフェースを設計する場合、既存のコードを変更すると通常バグが発生するため、既存のコードを変更する必要がほとんどないようにすることで、柔軟性と保守性の向上を常に目指しています。インターフェースでは、build()メソッドでPersonImplを記述しています。 Personインターフェイスの新しい実装を作成するたびに、インターフェイスのクラスのコードを変更する必要があるため、これは問題です。この場合、コードは多くないので問題にはならないかもしれませんが、コードが大きくなるにつれて、このようなものがコードで失われるのは非常に簡単です。

enter image description here

個人的なメモとして、デザインのアイデアを伝えるときは、UMLまたは同様のグラフィック表現でコードを補完することをお勧めします。それらはその目的のために特別に設計されており、ほとんどすべての場合、他の方法では特定できなかったコードの設計上の欠陥を特定するのに役立ちました。

3
kiwicomb123