web-dev-qa-db-ja.com

継承とジェネリックを備えた流暢なAPI

一連の「メッセージ」オブジェクトを構成およびインスタンス化するための流暢なAPIを書いています。メッセージタイプの階層があります。

Fluent APIを使用するときにサブクラスのメソッドにアクセスできるようにするために、ジェネリックを使用してサブクラスをパラメーター化し、すべてのFluentメソッド( "with"で始まる)がジェネリック型を返すようにしました。流暢なメソッドの本体のほとんどを省略したことに注意してください。それらの中で多くの設定が行われます。

_public abstract class Message<T extends Message<T>> {

    protected Message() {

    }

    public T withID(String id) {
        return (T) this;
    }
}
_

具象サブクラスは、ジェネリック型を同様に再定義します。

_public class CommandMessage<T extends CommandMessage<T>> extends Message<CommandMessage<T>> {

    protected CommandMessage() {
        super();
    }

    public static CommandMessage newMessage() {
        return new CommandMessage();
    }

    public T withCommand(String command) {
        return (T) this;
    }
}

public class CommandWithParamsMessage extends
    CommandMessage<CommandWithParamsMessage> {

    public static CommandWithParamsMessage newMessage() {
        return new CommandWithParamsMessage();
    }

    public CommandWithParamsMessage withParameter(String paramName,
        String paramValue) {
        contents.put(paramName, paramValue);
        return this;
    }
}
_

このコードは機能します。つまり、任意のクラスをインスタンス化して、すべての流暢なメソッドを使用できます。

_CommandWithParamsMessage msg = CommandWithParamsMessage.newMessage()
        .withID("do")
        .withCommand("doAction")
        .withParameter("arg", "value");
_

ここでは、流暢なメソッドを任意の順序で呼び出すことが主要な目標です。

ただし、コンパイラはすべてのreturn (T) thisが安全でないことを警告します。

タイプセーフ:メッセージからTへの未チェックのキャスト

このコードを本当に安全にするためにどのように階層を再編成できるかわかりません。機能はしますが、この方法でジェネリックを使用することは、非常に複雑に感じられます。特に、警告を無視しただけで実行時例外が発生する状況を予測することはできません。新しいメッセージタイプがあるので、コードを拡張可能に保つ必要があります。解決策が継承を完全に回避することである場合、代替案の提案も取得したいと思います。

同様の問題に対処する otherquestions here SOがあります。それらはすべての中間クラスが抽象であり、 protected abstract self()のようなメソッドです。それでも、最終的には安全ではありません。

31
Boj

あなたのコードは基本的にジェネリックの安全でない使用です。たとえば、メッセージを拡張する新しいクラスを作成し、Threatとし、新しいメソッドdoSomething()を作成した場合、この新しいクラスによってパラメーター化されたメッセージを作成し、Messageのインスタンスを作成して、それをキャストしようとします。そのサブクラスに。ただし、これは脅威のインスタンスではなくメッセージのインスタンスであるため、このメッセージを呼び出そうとすると例外が発生します。 MessageがdoSOmething()を実行することはできないためです。

また、ここではジェネリックスを使用する必要もありません。単純な古い継承は問題なく機能します。サブタイプは戻り値のタイプをより具体的にすることでメソッドをオーバーライドできるため、次のことが可能です。

public abstract class Message {

    protected Message() {

    }

    public Message withID(String id) {
        return this;
    }
}

その後

public class CommandMessage extends Message {

    protected CommandMessage() {
        super();
    }

    public static CommandMessage newMessage() {
        return new CommandMessage();
    }

    public CommandMessage withCommand(String command) {
        return this;
    }
}

正しい順序で引数を呼び出すことを理解すれば、これは問題なく機能します。

CommandWithParamsMessage.newMessage()
    .withID("do")
    .withCommand("doAction")
    .withParameter("arg", "value");

失敗しますが

CommandWithParamsMessage.newMessage().withParameter("arg", "value")
.withCommand("doAction").withID("do")

成功するのは、「アップタイプ」だけなので、最終的に「メッセージ」クラスを返すからです。 「uptype」しないようにするには、継承されたコマンドを単に上書きします。これで、メソッドはすべて元の型を返すため、任意の順序でメソッドを呼び出すことができます。

例えば。

public class CommandWithParamsMessage extends
CommandMessage {

    public static CommandWithParamsMessage newMessage() {
        return new CommandWithParamsMessage();
    }

    public CommandWithParamsMessage withParameter(String paramName,
        String paramValue) {
        contents.put(paramName, paramValue);
        return this;
    }

    @Override
    public CommandWithParamsMessage withCommand(String command){
        super.withCommand(command);
        return this;
   }

    @Override
    public CommandWithParamsMessage withID(String s){
        super.withID(s);
        return this;
    }
}

ここで、上記の2つの流暢な呼び出しのいずれかを使用してCommandWithParamsMessageを流暢に返します。

これはあなたの問題を解決しますか、それとも私はあなたの意図を誤解しましたか?

19
phil_20686

私は以前にこのようなことをしたことがあります。醜くなります。実際、私は何度も使ったことがあります。通常それは消えてしまい、より良いデザインを見つけようとします。とはいえ、少し先に進むには、次のことを試してください。

抽象クラスで次のようなメソッドを宣言します。

protected abstract T self();

これは、returnステートメントのthisを置き換えることができます。サブクラスは、Tの境界に一致するものを返す必要がありますが、同じオブジェクトを返すことは保証されません。

14
William Price

このようにシグネチャを変更すると、警告が表示されたり、キャストが必要になることはありません。

abstract class Message<T extends Message<T>> {

    public T withID(String id) {
        return self();
    }

    protected abstract T self();
}

abstract class CommandMessage<T extends CommandMessage<T>> extends Message<T> {

    public T withCommand(String command) {
        // do some work ...
        return self();
    }
}

class CommandWithParamsMessage extends CommandMessage<CommandWithParamsMessage> {

    public static CommandWithParamsMessage newMessage() {
        return new CommandWithParamsMessage();
    }

    public CommandWithParamsMessage withParameter(String paramName, String paramValue) {
        // do some work ...
        return this;
    }

    @Override protected CommandWithParamsMessage self() {
        return this;
    }
}
8
Harmlezz

コンパイラは、コードの正当性を実際に確認できないため、この安全でない操作について警告します。これは実際には安全ではなく、この警告を防ぐためにあなたができることは何もありません。安全でない操作はコンパイルチェックされませんが、実行時には依然として正当な場合があります。ただし、コンパイラチェックを回避する場合は、@SupressWarning("unchecked")アノテーションの目的である正しい型を使用するために独自のコードを検証する必要があります。

これをあなたの例に適用するには:

public abstract class Message<T extends Message<T>> {

  // ...

  @SupressWarning("unchecked")
  public T withID(String id) {
    return (T) this;
  }
}

実際には、このMessageインスタンスは常にTで表される型であることを確実に伝えることができるため、問題ありません。しかし、Javaコンパイラは(まだ)できません。他の抑制警告と同様に、注釈を使用するための鍵は、そのスコープを最小化することです!そうでない場合、作成後に誤って注釈抑制を簡単に保持できます。タイプセーフの以前の手動チェックを無効とするコード変更。

thisインスタンスのみを返すため、別の回答で推奨されているように、特定のメソッドにタスクを簡単に外部委託できます。 protectedメソッドを次のように定義します

@SupressWarning("unchecked")
public T self() {
  (T) this;
}

そして、いつでも次のようにミューテーターを呼び出すことができます:

public T withID(String id) {
  return self();
}

別のオプションとして、そして実装が可能である場合は、インターフェースによってAPIを公開するだけで、完全なビルダーを実装する不変のビルダーを検討してください。これは私が最近流暢なインターフェースを通常構築する方法です:

interface Two<T> { T make() }
interface One { <S> Two<S> do(S value) }

class MyBuilder<T> implements One, Two<T> {

  public static One newInstance() {
    return new MyBuilder<Object>(null);
  }

  private T value; // private constructors omitted

  public <S> Two<S> do(S value) {
    return new MyBuilder<S>(value);
  }

  public T make() {
    return value;
  }
}

もちろん、未使用のフィールドを避けて、よりスマートな構造を作成できます。このアプローチを使用した私の例を確認したい場合は、流暢なインターフェイスをかなり頻繁に使用する2つのプロジェクトを見てください。

  1. Byte Buddy :実行時にJavaクラスを定義するためのAPI。
  2. PDFコンバーター :JavaからMS Wordを使用してファイルを変換するための変換ソフトウェア。
4

これは元の問題の解決策ではありません。それはあなたの実際の意図を捉え、元の問題が現れないアプローチをスケッチする試みにすぎません。 (私はジェネリックが好きですが、CommandMessage<T extends CommandMessage<T>> extends Message<CommandMessage<T>>のようなクラス名は私を震えさせます...)

これは最初に尋ねたものとは構造的にかなり異なっていることを知っています。また、可能な答えの範囲を絞り込んで、次のようになる質問の詳細を省略した可能性があります適用されなくなりました。

しかし、ifあなたの意図を正しく理解したので、サブタイプを流暢な呼び出しで処理させることを検討できます。

ここでの考え方は、最初は単純にMessageのみ作成できるということです。

Message m0 = Message.newMessage();
Message m1 = m0.withID("id");

このメッセージでは、withIDメソッドを呼び出すことができます。これは、すべてのメッセージに共通する唯一のメソッドです。この場合、withIDメソッドはMessageを返します。

これまで、メッセージはCommandMessageでも他の特殊な形式でもありません。ただし、withCommandメソッドを呼び出すときは、明らかにCommandMessageを作成する必要があるので、CommandMessageを返すだけです。

CommandMessage m2 = m1.withCommand("command");

同様に、withParameterメソッドを呼び出すと、CommandWithParamsMessageを受け取ります。

CommandWithParamsMessage m3 = m2.withParameter("name", "value");

このアイデアは大体(!)ドイツ語の ブログエントリ に触発されていますが、コードはこの概念を使用してタイプセーフな「Select-From-Where」クエリを作成する方法をうまく示しています。

ここでは、アプローチを概説し、ユースケースに大まかに適合させます。もちろん、実装がどのように実際に使用されるかに依存する詳細もいくつかありますが、そのアイデアが明確になることを願っています。

import Java.util.HashMap;
import Java.util.Map;


public class FluentTest
{
    public static void main(String[] args) 
    {
        CommandWithParamsMessage msg = Message.newMessage().
                withID("do").
                withCommand("doAction").
                withParameter("arg", "value");


        Message m0 = Message.newMessage();
        Message m1 = m0.withID("id");
        CommandMessage m2 = m1.withCommand("command");
        CommandWithParamsMessage m3 = m2.withParameter("name", "value");
        CommandWithParamsMessage m4 = m3.withCommand("otherCommand");
        CommandWithParamsMessage m5 = m4.withID("otherID");
    }
}

class Message 
{
    protected String id;
    protected Map<String, String> contents;

    static Message newMessage()
    {
        return new Message();
    }

    private Message() 
    {
        contents = new HashMap<>();
    }

    protected Message(Map<String, String> contents) 
    {
        this.contents = contents;
    }

    public Message withID(String id) 
    {
        this.id = id;
        return this;
    }

    public CommandMessage withCommand(String command) 
    {
        Map<String, String> newContents = new HashMap<String, String>(contents);
        newContents.put("command", command);
        return new CommandMessage(newContents);
    }

}

class CommandMessage extends Message 
{
    protected CommandMessage(Map<String, String> contents) 
    {
        super(contents);
    }

    @Override
    public CommandMessage withID(String id) 
    {
        this.id = id;
        return this;
    }

    public CommandWithParamsMessage withParameter(String paramName, String paramValue) 
    {
        Map<String, String> newContents = new HashMap<String, String>(contents);
        newContents.put(paramName, paramValue);
        return new CommandWithParamsMessage(newContents);
    }

}

class CommandWithParamsMessage extends CommandMessage 
{
    protected CommandWithParamsMessage(Map<String, String> contents) 
    {
        super(contents);
    }

    @Override
    public CommandWithParamsMessage withID(String id) 
    {
        this.id = id;
        return this;
    }

    @Override
    public CommandWithParamsMessage withCommand(String command) 
    {
        this.contents.put("command", command);
        return this;
    }
}
3
Marco13