1. ホーム
  2. java

ビルダーパターンと継承

2023-09-24 03:13:09

質問

オブジェクト階層があり、継承ツリーが深くなるにつれて複雑になっています。これらのどれも抽象的ではなく、したがって、それらのインスタンスはすべて、多かれ少なかれ洗練された目的を果たすものです。

パラメータの数が非常に多いので、いくつかのコンストラクタをコーディングするよりも、プロパティを設定するためにBuilderパターンを使用したいと思います。すべての組み合わせに対応する必要があるので、継承ツリーのリーフクラスは伸縮自在のコンストラクタを持つことになります。

私は設計中にいくつかの問題にぶつかったとき、ここで答えを探しました。まず、この問題を説明するために、簡単で浅い例を挙げましょう。

public class Rabbit
{
    public String sex;
    public String name;

    public Rabbit(Builder builder)
    {
        sex = builder.sex;
        name = builder.name;
    }

    public static class Builder
    {
        protected String sex;
        protected String name;

        public Builder() { }

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

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

        public Rabbit build()
        {
            return new Rabbit(this);
        }
    }
}

public class Lop extends Rabbit
{
    public float earLength;
    public String furColour;

    public Lop(LopBuilder builder)
    {
        super(builder);
        this.earLength = builder.earLength;
        this.furColour = builder.furColour;
    }

    public static class LopBuilder extends Rabbit.Builder
    {
        protected float earLength;
        protected String furColour;

        public LopBuilder() { }

        public Builder earLength(float length)
        {
            this.earLength = length;
            return this;
        }

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

        public Lop build()
        {
            return new Lop(this);
        }
    }
}

さて、いくつかのコードができたので、画像化するために Lop :

Lop lop = new Lop.LopBuilder().furColour("Gray").name("Rabbit").earLength(4.6f);

この呼び出しは、最後に連結された呼び出しが解決できないため、コンパイルされません。 Builder メソッドを定義していない earLength . そのため、この方法ではすべての呼び出しを特定の順序で連結する必要があり、特に深い階層ツリーでは非常に非現実的です。

さて、答えを探している間に、私は以下のものに出会いました。 Java Builder クラスのサブクラス化 を使用することを示唆しています。 不思議なほど再帰的なジェネリックパターン . しかし、私の階層には抽象クラスがないため、この解決策は私には使えません。しかし、このアプローチは、機能するために抽象化とポリモーフィズムに依存しており、それが私のニーズに適応できるとは思えない理由です。

私が現在解決しているアプローチは、スーパークラスのすべてのメソッドをオーバーライドすることです。 Builder のすべてのメソッドをオーバーライドして、単純に次のようにすることです。

public ConcreteBuilder someOverridenMethod(Object someParameter)
{
    super(someParameter);
    return this;
}

このアプローチで、私はチェーンコールを発行できるインスタンスを返されることを保証できます。これは Telescoping Anti-pattern ほどひどくはありませんが、2 番目に近いもので、私はこれを少し厄介なものだと考えています。

私が気づいていない、私の問題に対する別の解決策はありますか? できれば、デザイン パターンと一致する解決策をお願いします。ありがとうございます。

どのように解決するのですか?

これは確かに再帰的バインドで可能ですが、サブタイプビルダもジェネリックである必要があり、いくつかの中間抽象クラスが必要です。少し面倒ですが、それでも非ジェネリックバージョンよりは簡単です。

/**
 * Extend this for Mammal subtype builders.
 */
abstract class GenericMammalBuilder<B extends GenericMammalBuilder<B>> {
    String sex;
    String name;

    B sex(String sex) {
        this.sex = sex;
        return self();
    }

    B name(String name) {
        this.name = name;
        return self();
    }

    abstract Mammal build();

    @SuppressWarnings("unchecked")
    final B self() {
        return (B) this;
    }
}

/**
 * Use this to actually build new Mammal instances.
 */
final class MammalBuilder extends GenericMammalBuilder<MammalBuilder> {
    @Override
    Mammal build() {
        return new Mammal(this);
    }
}

/**
 * Extend this for Rabbit subtype builders, e.g. LopBuilder.
 */
abstract class GenericRabbitBuilder<B extends GenericRabbitBuilder<B>>
        extends GenericMammalBuilder<B> {
    Color furColor;

    B furColor(Color furColor) {
        this.furColor = furColor;
        return self();
    }

    @Override
    abstract Rabbit build();
}

/**
 * Use this to actually build new Rabbit instances.
 */
final class RabbitBuilder extends GenericRabbitBuilder<RabbitBuilder> {
    @Override
    Rabbit build() {
        return new Rabbit(this);
    }
}

葉っぱのクラスが "concrete"にならないようにする方法があります、ここで、もし、これがあったら。

class MammalBuilder<B extends MammalBuilder<B>> {
    ...
}
class RabbitBuilder<B extends RabbitBuilder<B>>
        extends MammalBuilder<B> {
    ...
}

次に、ダイヤモンドで新しいインスタンスを作成し、参照タイプにワイルドカードを使用する必要があります。

static RabbitBuilder<?> builder() {
    return new RabbitBuilder<>();
}

これは、型変数に束縛されることで、例えば RabbitBuilder の全てのメソッドが RabbitBuilder を含む型を返します。

というのも、あらゆるところでワイルドカードを使用する必要があり、新しいインスタンスを作成するには、ダイアモンドまたは 生タイプ . いずれにせよ、少し厄介なことになりますね。


ちなみに、これについては

@SuppressWarnings("unchecked")
final B self() {
    return (B) this;
}

そのチェックされていないキャストを回避する方法があります。それは、メソッドを抽象化することです。

abstract B self();

そして、リーフのサブクラスでそれをオーバーライドします。

@Override
RabbitBuilder self() { return this; }

この方法の問題点は、より型安全ではあるものの、サブクラスが返せるのは this . 基本的に、どちらの方法でも、サブクラスは何か間違ったことをする機会があるので、私はそれらのアプローチのうちの1つを他よりも好む理由はあまり見当たりません。