継承ツリーが深くなるにつれて複雑さが増すオブジェクト階層があります。これらはいずれも抽象的ではないため、すべてのインスタンスは多かれ少なかれ洗練された目的を果たします。
パラメーターの数が非常に多いため、いくつかのコンストラクターをコーディングするのではなく、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クラス に遭遇しました。これは Curiously Recursive Generic Pattern 。 、私の階層には抽象クラスが含まれていないため、このソリューションは機能しませんが、このアプローチは機能に抽象化とポリモーフィズムに依存しているため、ニーズに適応できないと考えています。
私が現在解決しているアプローチは、階層内のスーパークラスBuilder
のすべてのメソッドをオーバーライドし、次のことを行うことです。
public ConcreteBuilder someOverridenMethod(Object someParameter)
{
super(someParameter);
return this;
}
このアプローチにより、チェーンコールを発行できるインスタンスが返されることを保証できます。これはTelescoping Anti-patternほど悪くはありませんが、すぐ近くにあり、少し「ハッキング」されていると思います。
私の気づいていない私の問題の別の解決策はありますか?設計パターンと一致するソリューションが望ましい。ありがとうございました!
これは確かに再帰的なバインドで可能ですが、サブタイプビルダーもジェネリックである必要があり、いくつかの暫定的な抽象クラスが必要です。少し面倒ですが、一般的でないバージョンよりも簡単です。
/**
* 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);
}
}
「具象」リーフクラスを使用しないようにする方法があります。
class MammalBuilder<B extends MammalBuilder<B>> {
...
}
class RabbitBuilder<B extends RabbitBuilder<B>>
extends MammalBuilder<B> {
...
}
次に、ダイヤモンドを使用して新しいインスタンスを作成し、参照タイプでワイルドカードを使用する必要があります。
static RabbitBuilder<?> builder() {
return new RabbitBuilder<>();
}
これは、型変数のバインドにより、たとえばtype引数が単なるワイルドカードであっても、RabbitBuilder
にはRabbitBuilder
を含む戻り値の型があります。
ただし、どこでもワイルドカードを使用する必要があり、ダイヤモンドまたは raw type を使用してのみ新しいインスタンスを作成できるため、私はそのことをあまり好きではありません。どちらにしても、あなたは少し不器用になってしまうと思います。
ところで、これについて:
@SuppressWarnings("unchecked")
final B self() {
return (B) this;
}
この未チェックのキャストを回避する方法があります。これはメソッドを抽象化することです:
abstract B self();
そして、リーフサブクラスでそれをオーバーライドします:
@Override
RabbitBuilder self() { return this; }
この方法で問題になるのは、タイプセーフではありますが、サブクラスはthis
以外の何かを返す可能性があることです。基本的に、いずれにせよ、サブクラスには何か間違ったことをする機会があるため、これらのアプローチの1つを他のアプローチよりも好む理由はあまりありません。
それでも誰かが同じ問題にぶつかった場合は、「継承よりも構成を優先する」設計パターンに適合する次の解決策をお勧めします。
親クラス
その主な要素は、親クラスBuilderが実装する必要があるインターフェイスです。
public interface RabbitBuilder<T> {
public T sex(String sex);
public T name(String name);
}
変更された変更後の親クラスは次のとおりです。
public class Rabbit {
public String sex;
public String name;
public Rabbit(Builder builder) {
sex = builder.sex;
name = builder.name;
}
public static class Builder implements RabbitBuilder<Builder> {
protected String sex;
protected String name;
public Builder() {}
public Rabbit build() {
return new Rabbit(this);
}
@Override
public Builder sex(String sex) {
this.sex = sex;
return this;
}
@Override
public Builder name(String name) {
this.name = name;
return this;
}
}
}
子クラス
子クラスBuilder
は、同じインターフェースを(異なるジェネリック型で)実装する必要があります。
public static class LopBuilder implements RabbitBuilder<LopBuilder>
子クラスBuilder
内で、parent Builder
を参照するフィールド:
private Rabbit.Builder baseBuilder;
これにより、確実に親Builder
メソッドが子で呼び出されますが、実装は異なります。
@Override
public LopBuilder sex(String sex) {
baseBuilder.sex(sex);
return this;
}
@Override
public LopBuilder name(String name) {
baseBuilder.name(name);
return this;
}
public Rabbit build() {
return new Lop(this);
}
Builderのコンストラクター:
public LopBuilder() {
baseBuilder = new Rabbit.Builder();
}
構築された子クラスのコンストラクタ:
public Lop(LopBuilder builder) {
super(builder.baseBuilder);
}
同じ問題に直面して、私はemcmanusが提案したソリューションを使用しました: https://community.Oracle.com/blogs/emcmanus/2010/10/24/using-builder-pattern-subclasses
私はここで彼/彼女の好ましい解決策を再コピーしています。 Shape
とRectangle
の2つのクラスがあるとします。 Rectangle
はShape
から継承します。
public class Shape {
private final double opacity;
public double getOpacity() {
return opacity;
}
protected static abstract class Init<T extends Init<T>> {
private double opacity;
protected abstract T self();
public T opacity(double opacity) {
this.opacity = opacity;
return self();
}
public Shape build() {
return new Shape(this);
}
}
public static class Builder extends Init<Builder> {
@Override
protected Builder self() {
return this;
}
}
protected Shape(Init<?> init) {
this.opacity = init.opacity;
}
}
抽象のInit
内部クラスと、実際の実装であるBuilder
内部クラスがあります。 Rectangle
を実装するときに役立ちます:
public class Rectangle extends Shape {
private final double height;
public double getHeight() {
return height;
}
protected static abstract class Init<T extends Init<T>> extends Shape.Init<T> {
private double height;
public T height(double height) {
this.height = height;
return self();
}
public Rectangle build() {
return new Rectangle(this);
}
}
public static class Builder extends Init<Builder> {
@Override
protected Builder self() {
return this;
}
}
protected Rectangle(Init<?> init) {
super(init);
this.height = init.height;
}
}
Rectangle
をインスタンス化するには:
new Rectangle.Builder().opacity(1.0D).height(1.0D).build();
繰り返しますが、Shape.Init
を継承する抽象Init
クラスと、実際の実装であるBuild
です。各Builder
クラスはself
メソッドを実装します。このメソッドは、自身の正しくキャストされたバージョンを返す役割を果たします。
Shape.Init <-- Shape.Builder
^
|
|
Rectangle.Init <-- Rectangle.Builder
このフォームはほとんど機能するようです。それはあまりきれいではありませんが、あなたの問題を避けるように見えます:
class Rabbit<B extends Rabbit.Builder<B>> {
String name;
public Rabbit(Builder<B> builder) {
this.name = builder.colour;
}
public static class Builder<B extends Rabbit.Builder<B>> {
protected String colour;
public B colour(String colour) {
this.colour = colour;
return (B)this;
}
public Rabbit<B> build () {
return new Rabbit<>(this);
}
}
}
class Lop<B extends Lop.Builder<B>> extends Rabbit<B> {
float earLength;
public Lop(Builder<B> builder) {
super(builder);
this.earLength = builder.earLength;
}
public static class Builder<B extends Lop.Builder<B>> extends Rabbit.Builder<B> {
protected float earLength;
public B earLength(float earLength) {
this.earLength = earLength;
return (B)this;
}
@Override
public Lop<B> build () {
return new Lop<>(this);
}
}
}
public class Test {
public void test() {
Rabbit rabbit = new Rabbit.Builder<>().colour("White").build();
Lop lop1 = new Lop.Builder<>().earLength(1.4F).colour("Brown").build();
Lop lop2 = new Lop.Builder<>().colour("Brown").earLength(1.4F).build();
//Lop.Builder<Lop, Lop.Builder> builder = new Lop.Builder<>();
}
public static void main(String args[]) {
try {
new Test().test();
} catch (Throwable t) {
t.printStackTrace(System.err);
}
}
}
Rabbit
とLop
(両方の形式)を正常に構築できましたが、この段階では、Builder
オブジェクトの1つを完全な型で実際にインスタンス化する方法はわかりません。
このメソッドの本質は、Builder
メソッドの(B)
へのキャストに依存しています。これにより、オブジェクトのタイプとBuilder
のタイプを定義し、構築中にオブジェクト内に保持することができます。
誰かがこれの正しい構文を解決できれば(これは間違っています)、感謝します。
Lop.Builder<Lop.Builder> builder = new Lop.Builder<>();
私はいくつかの実験を行ったが、これは私にとって非常にうまくいくことがわかった。開始時に実際のインスタンスを作成し、そのインスタンスのすべてのセッターを呼び出すことを好むことに注意してください。これは単なる好みです。
受け入れられた答えとの主な違いは
コード:
public class MySuper {
private int superProperty;
public MySuper() { }
public void setSuperProperty(int superProperty) {
this.superProperty = superProperty;
}
public static SuperBuilder<? extends MySuper, ? extends SuperBuilder> newBuilder() {
return new SuperBuilder<>(new MySuper());
}
public static class SuperBuilder<R extends MySuper, B extends SuperBuilder<R, B>> {
private final R mySuper;
public SuperBuilder(R mySuper) {
this.mySuper = mySuper;
}
public B withSuper(int value) {
mySuper.setSuperProperty(value);
return (B) this;
}
public R build() {
return mySuper;
}
}
}
そして、サブクラスは次のようになります。
public class MySub extends MySuper {
int subProperty;
public MySub() {
}
public void setSubProperty(int subProperty) {
this.subProperty = subProperty;
}
public static SubBuilder<? extends MySub, ? extends SubBuilder> newBuilder() {
return new SubBuilder(new MySub());
}
public static class SubBuilder<R extends MySub, B extends SubBuilder<R, B>>
extends SuperBuilder<R, B> {
private final R mySub;
public SubBuilder(R mySub) {
super(mySub);
this.mySub = mySub;
}
public B withSub(int value) {
mySub.setSubProperty(value);
return (B) this;
}
}
}
およびサブサブクラス
public class MySubSub extends MySub {
private int subSubProperty;
public MySubSub() {
}
public void setSubSubProperty(int subProperty) {
this.subSubProperty = subProperty;
}
public static SubSubBuilder<? extends MySubSub, ? extends SubSubBuilder> newBuilder() {
return new SubSubBuilder<>(new MySubSub());
}
public static class SubSubBuilder<R extends MySubSub, B extends SubSubBuilder<R, B>>
extends SubBuilder<R, B> {
private final R mySubSub;
public SubSubBuilder(R mySub) {
super(mySub);
this.mySubSub = mySub;
}
public B withSubSub(int value) {
mySubSub.setSubSubProperty(value);
return (B)this;
}
}
}
完全に機能することを確認するために、このテストを使用しました。
MySubSub subSub = MySubSub
.newBuilder()
.withSuper (1)
.withSub (2)
.withSubSub(3)
.withSub (2)
.withSuper (1)
.withSubSub(3)
.withSuper (1)
.withSub (2)
.build();
次のIEEE会議の貢献 JavaのFluent Builderの改良版 は、問題に対する包括的なソリューションを提供します。
元の質問を継承欠損と準不変性の2つの副問題に分析し、これら2つの副問題の解決方法を示します。 Javaの従来のビルダーパターンでコードを再利用すると、継承サポートの問題が発生します。
ジェネリックを使用できないので、おそらく主なタスクは、何らかの形でタイピングを緩めることです。それらのプロパティを後でどのように処理するのかわかりませんが、HashMapを使用してそれらをキーと値のペアとして保存するとどうなりますか?そのため、ビルダーにはset(key、value)ラッパーメソッドが1つしかありません(または、ビルダーはもう必要ないかもしれません)。
欠点は、保存されたデータを処理する際の追加の型キャストです。
このケースが緩すぎる場合、既存のプロパティを保持できますが、「キー」名に基づいてリフレクションを使用してセッターメソッドを検索する一般的なsetメソッドを使用できます。反省はやり過ぎだと思いますが。
最も簡単な解決策は、親クラスのセッターメソッドを単純にオーバーライドすることです。
ジェネリックを避け、使いやすく、拡張し、理解しやすく、またsuper.setterを呼び出すときのコードの重複も避けます。
public class Lop extends Rabbit {
public final float earLength;
public final String furColour;
public Lop(final 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() {}
@Override
public LopBuilder sex(final String sex) {
super.sex(sex);
return this;
}
@Override
public LopBuilder name(final String name) {
super.name(name);
return this;
}
public LopBuilder earLength(final float length) {
this.earLength = length;
return this;
}
public LopBuilder furColour(final String colour) {
this.furColour = colour;
return this;
}
@Override
public Lop build() {
return new Lop(this);
}
}
}