https://d226lax1qjow5r.cloudfront.net/blog/blogposts/builder-pattern-with-inheritance-in-java/builder-pattern.png

Javaにおける継承を用いたビルダー・パターン

最終更新日 August 3, 2022

所要時間:1 分

この記事は2025年4月に更新されました。

はじめに

オブジェクトをインスタンス化する一般的な方法は、コンストラクターを使って必要なパラメーターを直接渡すことだ。簡潔にするために、関連するパラメーターを1つのクラスにまとめ、そのクラスのインスタンスを個別に構築し、それをパラメーターとしてメイン・オブジェクトに渡すこともできる。

これには、オブジェクトの構築を複数のステップに分けることで、必要なパラメーターの数を減らせるという利点がある。しかし、コンストラクタを使うことにはいくつかの欠点がある。ひとつは、パラメータを渡す順番が重要であることで、パラメータが多い場合は大変なことになり、これらのパラメータが同じ型(Stringなど)の場合はさらにエラーになりやすい。

また、オプションのパラメータがある場合もある。さまざまなシグネチャを持つ複数のコンストラクタを用意することで、これを回避することはできますが、これはユーザーや開発者に多くの定型文と認識負荷を与えることになります。また、複数のオプショナル・パラメータが同じ型である場合、実行不可能になります。

Builderパターンは、オブジェクト指向言語において、オブジェクトの構築を制御するためのよく知られたデザインパターンである。多くの(おそらくほとんどの)デザインパターンがそうであるように、このパターンも言語の欠陥に対処するために存在する。名前付きパラメータを持つ言語(Kotlin、Scala、Python、C#、Rubyなど)では、多くの場合ビルダーの必要性が薄れている。

EffectiveなJavaではビルダー・パターンを使うことが推奨されているが、名前付き引数を持つ言語では、その価値が疑問になってくる(たとえば を参照)。).

オプショナルパラメータがビルダーパターンをどの程度陳腐化させるかについての議論は、この記事の範囲外である。その代わりに、最近リリースされた Vonage Java SDKにおけるMessages API実装についてである。.

MessageRequestクラス

で送信できるさまざまなタイプのメッセージをモデル化する。 メッセージ APIを介して送信できるさまざまなタイプのメッセージをモデル化するために、オブジェクト指向のアプローチが使用され、メッセージ・タイプとサービスの有効な組み合わせごとにクラスが作成される。3レベルの継承階層があります。次の例を見てみよう:

  • MessageRequest

    • MmsRequest

      • MmsVcardRequest

MessageRequestMmsRequestは抽象クラスであり MmsVcardRequestは MMS で vCard を送信する組み合わせを表すクラスである。

基底クラス MessageRequestはコンストラクタでチャネルとメッセージ・タイプを引数にとります。また Builderをパラメータとして取ります。これはメッセージの主な詳細が設定される場所です。いくつかのパラメータはオプションです。 clientRefまた、すべてのメッセージには送信者と受信者が存在するため、これらはベースとなる MessageRequest.

しかし、この継承階層は MmsVcardRequestを構築する際には、この継承階層は透過的である:

MmsVcardRequest message = MmsVcardRequest.builder()
        .from("447900090000").to("447900090001")
        .url("https://www.example.com/contact.vcf")
        .clientRef("vCard-msg-#1")
        .build();

完全なサンプルは コードサンプルレポ.

Javaには名前付きメソッドやオプションのメソッド/コンストラクタ・パラメータがないことを考えると、これはユーザーから見ると多少エレガントに見える。しかしその裏では、上記のコードを継承階層で実現するために、SDK開発者の視点からはすぐには分からない、あるいは直感的ではない設計が必要です。

の各サブクラスに関連するネストされたビルダー・クラスに関連している。 MessageRequest.例として MmsRequestを見てみよう。を設定するだけなので、まだ抽象的です。 Channel.

しかし、あの威圧的な宣言はどうだろう。 Builder宣言はどうだろう?この記事の残りは、あなたがこれを理解する手助けをしようとするものである:

protected abstract static class Builder<M extends MmsRequest, B extends Builder<? extends M, ? extends B>> extends MessageRequest.Builder<M, B>

について<M>パラメータ

の基本ビルダークラスは、パラメータとして MessageRequestクラスは MessageRequestの型と Builder.

前者(M)を説明するのは簡単だ: public abstract M build()MessageRequest.Builderは、ユーザーがパラメーターの設定を終えると呼び出すもので、適切な具象サブクラス MessageRequestサブクラスを返します。

もちろん、Javaの継承は共変の戻り値をサポートしているので、これを省略することもできる。つまり Mを省略することもできる。すると MessageRequest.Builderは次のようになる:

public abstract static class Builder<B extends Builder<? extends B>> {
    protected String from, to, clientRef;

    public B from(String from) {
        this.from = from;
        return (B) this;
    }

    public B to(String to) {
        this.to = to;
        return (B) this;
    }

    public B clientRef(String clientRef) {
        this.clientRef = clientRef;
        return (B) this;
    }

    public abstract MessageRequest build();
}

**

その場合、サブクラスでこれをオーバーライドすることを忘れないようにしなければならない。 MmsRequest.Builderのような抽象クラスであっても:

protected abstract static class Builder<B extends Builder<? extends B>> extends MessageRequest.Builder<B> {
    String url;

    protected B url(String url) {
        this.url = url;
        return (B) this;
    }

    @Override
    public abstract MmsRequest build();
}

具象クラス MmsVcardRequest.Builderも同じように見えるだろう:

public static final class Builder extends MmsRequest.Builder<Builder> {
    Builder() {}

    public Builder url(String url) {
        return super.url(url);
    }

    @Override
    public MmsVcardRequest build() {
        return new MmsVcardRequest(this);
    }
}

では、共変数の戻り値が使えるのに、なぜこの一見冗長なパラメーターを追加しているのだろうか?それは単に、戻り値の型をオーバーライドするのを忘れないようにするためである。

戻り値の型を生成することで、コンパイラーは build()メソッドが正しいシグネチャを持つことを保証します。これがなければ、次のような MmsVcardRequest.Builderも有効であるが、不正確である:

public static final class Builder extends MmsRequest.Builder<Builder> {
    Builder() {}

    public Builder url(String url) {
        return super.url(url);
    }

    @Override
    public MmsRequest build() {
        return new MmsVcardRequest(this);
    }
}

の戻り値の型をオーバーライドすることを忘れないようにした。 MmsRequest.Builder#build()の戻り値の型をオーバーライドすることを覚えているので、コンパイラーは次のようにキャッチする。 build()シグネチャを返します:

@Override
public MessageRequest build() {
    return new MmsVcardRequest(this);
}

というエラーメッセージが表示されます:

com.vonage.client.messages.mms.MmsVcardRequest.Builder' の 'build()' は 'com.vonage.client.messages.mms.MmsRequest.Builder' の 'build()' と衝突します。.

のメソッドのシグネチャを手動でオーバーライドしたのを覚えていたからに他ならない。 MmsRequest.Builder.そうしなければ、エラーは発生しない。戻り値の型をパラメータ化することで、正しい型を宣言せざるを得なくなる。 build()メソッドをオーバーライドする必要はない。 MessageRequest- のサブクラスでメソッドをオーバーライドする必要はない。

について<B>パラメータ

後者のビルダー・パラメーターに話を戻そう。 B.ビルダー・パターンに慣れ親しんでいる人なら、ビルダーに対する各メソッド呼び出しがビルダー自身を返すので、メソッド呼び出しを連鎖させてパラメータを簡単に設定できることを知っているだろう。

これは継承がないときにはうまくいくが、ユーザがどのような順序でもパラメータを設定できるようにしたい。したがって、どのメソッドが最初に呼び出されたかに関係なく、最も具体的な Builder クラスが返されるようにする必要があります。

そうしないと、メソッド呼び出しを連鎖させることができなくなり、毎回戻り値をキャストしなければならなくなる。

これを明確にするために、素朴に現在のビルダーを返す場合を考えてみよう:

public abstract static class Builder<M extends MessageRequest> {
    protected String from, to clientRef;

    public Builder<M> from(String from) {
        this.from = from;
        return this;
    }

    public Builder<M> to(String to) {
        this.to = to;
        return this;
    }

    public Builder<M> clientRef(String clientRef) {
        this.clientRef = clientRef;
        return this;
    }

    public abstract M build();
}

これは問題なくコンパイルでき、使用可能だ。すると MmsRequestのビルダーは次のようになる:

protected abstract static class Builder<M extends MmsRequest> extends MessageRequest.Builder<M> {
    String url;

    protected Builder<M> url(String url) {
        this.url = url;
        return this;
    }
}

最後に、具体的なサブクラス(例と同じ)は次のようになる。 MmsVcardRequestは次のようになる:

public static final class Builder extends MmsRequest.Builder<MmsVcardRequest> {
    Builder() {}

    /**
    * (REQUIRED)
    * Sets the URL of the vCard attachment. Supports only <code>.vcf</code> file extension.
    *
    * @param url The URL as a string.
    * @return This builder.
    */
    public Builder url(String url) {
        return (Builder) super.url(url);
    }

    @Override
    public MmsVcardRequest build() {
        return new MmsVcardRequest(this);
    }
}

の戻り値の型をこのビルダーにキャストしなければならなかったことに注意してほしい。 super.url(url)スーパー・メソッドのリターン・タイプは com.vonage.messages.mms.MmsRequest.Builderであり com.vonage.messages.mms.MmsVcardRequest.Builder.

このメソッドをオーバーライドしたのは、Javadocを追加するためだけであり、機能を変更するためではないことに注意してほしい。しかしこれは、パラメータ化されたビルダー型が解決しようとしている問題を見事に浮き彫りにしている。説明するために、このビルダーを使ってみよう:

MmsVcardRequest message = MmsVcardRequest.builder()
    .from("447900090000").to("447900090001")
    .url("https://www.example.com/path/to/contact.vcf")
    .build();

コンパイラーはエラーを出す: Cannot resolve method 'url' in 'Builder'.対照的に、以下はうまくいく:

MmsVcardRequest message = MmsVcardRequest.builder()
    .url("https://www.example.com/path/to/contact.vcf")
    .from("447900090000").to("447900090001")
    .build();

どうしたんだ?まったく同じ情報なのに、メソッドが呼び出される順番が違う。ビルダーの要点は、メソッドを呼び出す順番を柔軟に変更できるようにすることではないのでしょうか?良いユーザーエクスペリエンスのためには、このような状況を考慮する必要があります。

ユーザーは、どのクラスがどのプロパティに寄与しているかを気にする必要はない。冗長な解決策は、具体的なサブクラスである MessageRequest.Builder.例えば MmsVcardRequestでは

public static final class Builder extends MmsRequest.Builder<MmsVcardRequest> {
    Builder() {}

    public Builder from(String from) {
        return (Builder) super.from(from);
    }

    public Builder to(String to) {
        return (Builder) super.to(to);
    }

    public Builder clientRef(String clientRef) {
        return (Builder) super.clientRef(clientRef);
    }

    public Builder url(String url) {
        return (Builder) super.url(url);
    }

    @Override
    public MmsVcardRequest build() {
        return new MmsVcardRequest(this);
    }
}

しかし、これでは情報を繰り返すことになり、継承の意味がなくなってしまう!そのため、ビルダー型をパラメータ化するのだ。コンパイラーは常に最も具体的なサブタイプが返されるようにする。

しかし、ビルダークラスは拡張することができるため、パラメータ宣言にもこれをエンコードする必要がある。 B extends Builder<? extends M, ? extends B>ではなく B extends Builder<M, B>.

残念ながら、ビルダーを呼び出すたびに、ビルダーのリターン・タイプを Bを呼び出すたびに return thisしかし、私が知る限り、これはコンパイラの制限だ。ありがたいことに、キャストが必要なのは抽象的なビルダー・クラスだけで、具象型では必要ない。

結論

この記事で、抽象クラスや継承が関係する場合にBuilderパターンを使うための、(一見複雑に見えるかもしれないが)ある程度有用なパターンを学んでいただけたと思う。おそらくいつか、言語がオブジェクトをインスタンス化するためのより良い方法を追加したときに、このようなパターンは時代遅れになるだろう。それまでは、少なくともジェネリックスは私たちを助けてくれる!

私たちは常に地域社会の参加を歓迎しています。お気軽に VonageコミュニティSlackまたは ツイッター.改善や改良のご提案がある場合、またはバグを発見した場合は、ご遠慮なく GitHub.


シェア:

https://a.storyblok.com/f/270183/400x400/46a3751f47/sina-madani.png
Sina MadaniVonage 元チームメンバー

シナはVonageのJavaデベロッパー・アドボケイト。アカデミックなバックグラウンドを持ち、自動車、コンピューター、プログラミング、テクノロジー、人間性など、あらゆることに好奇心旺盛。余暇には散歩をしたり、対戦型ビデオゲームをしたりしている。