13

英語では、同形異義語のペアは、スペルが同じで意味が異なる 2 つの単語です。

ソフトウェア エンジニアリングでは、ホモグラフィック手法のペアとは、名前が同じで要件が異なる 2 つの手法です。質問をできるだけ明確にするために、不自然な例を見てみましょう。

interface I1 { 
    /** return 1 */ 
    int f()
}
interface I2 {
    /** return 2*/
    int f()
}
interface I12 extends I1, I2 {}

どうすれば実装できI12ますか? C#にはこれを行う方法がありますが、Java にはありません。したがって、唯一の回避策はハックです。リフレクション/バイトコードのトリック/その他を最も確実に使用するにはどうすればよいですか(つまり、完璧なソリューションである必要はありません。最適なソリューションが必要なだけです)。


合法的にリバース エンジニアリングできない既存のクローズド ソースの大規模なレガシー コードには、型のパラメーターが必要であり、その両方をパラメーターとして持つコードとパラメーターとして持つコードI12に委任することに注意してください。したがって、基本的には、いつ として機能する必要があるか、いつとして機能する必要があるかを知るのインスタンスを作成する必要があります。これは、直接の呼び出し元の実行時にバイトコードを調べることで実行できると思います。これは単純なコードであるため、呼び出し側でリフレクションが使用されていないと想定できます。問題は、 の作者がJavaが両方のインターフェースからマージされるとは予想していなかったことです。何も呼ばないI12I1I2I12I1I2I12fI12.f(明らかに、作成者が実際に を呼び出すコードを書いていれば、I12.fそれを販売する前に問題に気付いていただろう)。

私が実際に探しているのはこの質問に対する答えであり、変更できないコードを再構築する方法ではないことに注意してください。可能な限り最良のヒューリスティック、または存在する場合は正確な解決策を探しています。有効な例については、グレイの回答を参照してください(より堅牢なソリューションがあると確信しています)。


以下は、2 つのインターフェース内でのホモグラフィック メソッドの問題がどのように発生するかを示す具体的な例です。そして、ここに別の具体的な例があります:

次の 6 つの単純なクラス/インターフェイスがあります。それは、劇場とそこに出演するアーティストを取り巻くビジネスに似ています。簡単かつ具体的にするために、それらはすべて異なる人々によって作成されたと仮定しましょう。

Set集合論のように集合を表す:

interface Set {
    /** Complements this set,
        i.e: all elements in the set are removed,
        and all other elements in the universe are added. */
    public void complement();
    /** Remove an arbitrary element from the set */
    public void remove();
    public boolean empty();
}

HRDepartmentSet従業員を表すために使用します。洗練されたプロセスを使用して、どの従業員を雇用/解雇するかを解読します。

import java.util.Random;
class HRDepartment {
    private Random random = new Random();
    private Set employees;

    public HRDepartment(Set employees) {
        this.employees = employees;
    }

    public void doHiringAndLayingoffProcess() {
        if (random.nextBoolean())
            employees.complement();
        else
            employees.remove();
        if (employees.empty())
            employees.complement();
    }
}

従業員のユニバースは、Setおそらく雇用主に応募した従業員になります。したがって、complementそのセットで が呼び出されると、既存の従業員はすべて解雇され、以前に応募した他の従業員はすべて雇用されます。

Artistミュージシャンや俳優などのアーティストを表します。アーティストにはエゴがあります。この自尊心は、他の人が彼を褒めると増加する可能性があります。

interface Artist {
    /** Complements the artist. Increases ego. */
    public void complement();
    public int getEgo();
}

TheaterパフォーマンスをArtist行い、Artist補完される可能性があります。劇場の観客は、公演の合間にアーティストを判断できます。パフォーマーのエゴが高ければ高いほど、聴衆は を好きになる可能性が高くなりますがArtist、エゴが特定のポイントを超えると、アーティストは聴衆から否定的に見られます。

import java.util.Random;
public class Theater {
    private Artist artist;
    private Random random = new Random();

    public Theater(Artist artist) {
        this.artist = artist;
    }
    public void perform() {
        if (random.nextBoolean())
            artist.complement();
    }
    public boolean judge() {
        int ego = artist.getEgo();
        if (ego > 10)
            return false;
        return (ego - random.nextInt(15) > 0);
    }
}

ArtistSetは単純にArtistSet:

/** A set of associated artists, e.g: a band. */
interface ArtistSet extends Set, Artist {
}

TheaterManagerショーを実行します。劇場の観客がアーティストを否定的に判断した場合、劇場は人事部門に相談し、人事部門はアーティストを解雇したり、新しい人を雇ったりします。

class TheaterManager {
    private Theater theater;
    private HRDepartment hr;

    public TheaterManager(ArtistSet artists) {
        this.theater = new Theater(artists);
        this.hr = new HRDepartment(artists);
    }

    public void runShow() {
        theater.perform();
        if (!theater.judge()) {
            hr.doHiringAndLayingoffProcess();
        }
    }
}

を実装しようとすると、問題が明らかになりますArtistSet。両方のスーパーインターフェイスcomplementが、何か他のことを行うように指定しているためcomplement、何らかの方法で、同じクラス内に同じシグネチャを持つ 2 つのメソッドを実装する必要があります。Artist.complementの同形異義語ですSet.complement

4

7 に答える 7

4

新しいアイデア、ちょっと厄介です...

public class MyArtistSet implements ArtistSet {

    public void complement() {
        StackTraceElement[] stackTraceElements = Thread.currentThread().getStackTrace();

        // the last element in stackTraceElements is the least recent method invocation
        // so we want the one near the top, probably index 1, but you might have to play
        // with it to figure it out: could do something like this

        boolean callCameFromHR = false;
        boolean callCameFromTheatre = false;

        for(int i = 0; i < 3; i++) {
           if(stackTraceElements[i].getClassName().contains("Theatre")) {
               callCameFromTheatre = true;
           }
           if(stackTraceElements[i].getClassName().contains("HRDepartment")) {
               callCameFromHR = true;
           }
        }

        if(callCameFromHR && callCameFromTheatre) {
            // problem
        }
        else if(callCameFromHR) {
            // respond one way
        }
        else if(callCameFromTheatre) {
            // respond another way
        }
        else {
            // it didn't come from either
        }
    }
}
于 2013-05-11T06:37:05.813 に答える
3

特定のケースの解決方法

ArtistSet は単純に Artist と Set です。

 /** A set of associated artists, e.g: a band. */
 interface ArtistSet extends Set, Artist { }

OO の観点からは、これは有用な宣言ではありません。アーティストは名詞の一種であり、プロパティとアクション (メソッド) が定義された「もの」です。セットは物事の集合体、つまり固有の要素のコレクションです。代わりに、次を試してください。

ArtistSet は単にアーティストのセットです。

 /** A set of associated artists, e.g: a band. */
 interface ArtistSet extends Set<Artist> { };

次に、特定のケースでは、同名メソッドは1つのタイプ内で決して結合されないインターフェース上にあるため、衝突がなく、プログラミングできます...

さらに、ArtistSet実際には新しい宣言で Set を拡張していないため、宣言する必要はありません。型パラメーターをインスタンス化しているだけなので、すべての使用法をSet<Artist>.

より一般的なケースの解決方法

この衝突では、メソッド名は英語の意味で同形異義語である必要さえありません.Javaの異なるコンテキストで使用される、同じ英語の意味を持つ同じ単語にすることができます。型に適用したい 2 つのインターフェイスがあり、それらに競合するセマンティック/処理定義を持つ同じ宣言 (メソッド シグネチャなど) が含まれている場合、衝突が発生します。

Java では、要求した動作を実装することはできません。別の回避策が必要です。 Java では、クラスが複数の異なるインターフェイスから同じメソッド シグネチャに対して複数の実装を提供することを許可していません (区別するために、何らかの形式の修飾/エイリアス/注釈を使用して同じメソッドを複数回実装します)。2 つのインターフェースをオーバーライドする Java 、メソッド名の衝突、 Java - インターフェース実装におけるメソッド名の衝突を参照してください。

たとえば、次の場合

 interface TV {
     void switchOn();
     void switchOff();
     void changeChannel(int ChannelNumber);
 }

 interface Video {
     void switchOn();
     void switchOff();
     void eject();
     void play();
     void stop();
 }

次に、これらの両方であるオブジェクトがある場合は、新しいインターフェイス (オプション) で 2 つを組み合わせるか、次のように入力できます。

interface TVVideo {
     TV getTv();
     Video getVideo();
}


class TVVideoImpl implements TVVideo {
     TV tv;
     Video video;

     public TVVideoImpl() {
         tv = new SomeTVImpl(....);
         video = new SomeVideoImpl(....);
     }

     TV getTv() { return tv };
     Video getVideo() { return video };
}
于 2013-05-15T06:06:19.953 に答える
3

グレイ・ケミーの勇敢な試みにもかかわらず、あなたが述べたように問題は解決できないと思います。一般に、 が与えられた場合、それを呼び出すコードが を期待していたのか、ArtistSetを期待していたのかを知ることはできません。 ArtistSet

さらに、他のさまざまな回答に関するコメントによると、できたとしても、実際にはArtistSetベンダー提供の関数に渡す必要があります。技術的に正しい答えを得るには、あなたは完全に運が悪いです。

仕事を成し遂げるための実際的なプログラミングの問題として、私は次のことを行います(この順序で):

  1. 必要なインターフェースを作成した人、およびインターフェース自体ArtistSetを生成した人にバグレポートを提出してください。ArtistSet
  2. を必要とする機能を提供しているベンダーにサポート リクエストを提出し、 のArtistSet予想される動作を尋ねますcomplement()
  3. complement()例外をスローする関数を実装します。
public class Sybil implements ArtistSet {
  public void complement() { 
    throw new UnsupportedOperationException('What am I supposed to do'); 
  }
  ...
}

真剣に、あなたは何をすべきかわからないからです。このように呼び出された場合、何をするのが正しいでしょうか (そして、どうすれば確実にわかりますか)?

class TalentAgent {
    public void pr(ArtistSet artistsSet) {
      artistSet.complement();
    }
}

例外をスローすることで、スタック トレースを取得できる可能性があります。これにより、呼び出し元が予期している 2 つの動作のどちらかの手がかりが得られます。運が良ければ、誰もその関数を呼び出しませんでした。そのため、ベンダーはこの問題を抱えたコードを出荷するまでに至りました。運が悪いが、それでもいくつか、彼らは例外を処理します。そうでない場合でも、少なくとも今は、呼び出し元が実際に何を期待していたかを判断し、おそらくそれを実装するために確認できるスタック トレースが得られます (永続化をそのようにバグと考えるのは身震いしますが、私はどのように私がこの他の回答でそれを行います)。

ところで、実装の残りの部分では、コンストラクターを介して渡された実際のオブジェクトとオブジェクトにすべてを委譲しArtistSet後で簡単に引き離すことができるようにします。

于 2013-05-18T05:57:02.340 に答える
1

ホモグラフィック メソッドを持つ 2 つのスーパーインターフェイスを持つクラスを実装するにはどうすればよいですか?

Java では、ホモグラフィック メソッドを持つ 2 つのスーパーインターフェイスを持つクラスは、このメソッドの実装が 1 つだけあると見なされます。( Java 言語仕様のセクション 8.4.8を参照してください)。これにより、クラスは、同じ他のインターフェイスをすべて実装し、関数を 1 回だけ実装する複数のインターフェイスから簡単に継承できます。これにより、言語が簡素化されます。これにより、ホモグラフィック メソッドがどのインターフェイスから来たかに基づいてホモグラフィック メソッドを区別するための構文とメソッド ディスパッチ サポートが不要になるためです。

したがって、ホモグラフィック メソッドを持つ 2 つのスーパーインターフェイスを持つクラスを実装する正しい方法は、両方のスーパーインターフェイスの規約を満たす単一のメソッドを提供することです。

C# にはこれを行う方法があります。Javaでどのように行うことができますか? これには構成要素はありませんか?

C# は、Java とは異なる方法でインターフェイスを定義するため、Java にはない機能を備えています。

Java では、言語構造は、すべてのインターフェースがホモグラフィック メソッドの同じ単一の実装を取得することを意味するように定義されています。オブジェクトのコンパイル時のクラスに基づいて、多重継承されたインターフェイス関数の代替動作を作成するための Java 言語構造はありません。これは、Java 言語の設計者が意識的に選択したものです。

そうでない場合、リフレクション/バイトコードのトリック/などで最も確実に行うにはどうすればよいですか?

「それ」はリフレクション/バイトコードのトリックでは実行できません。これは、ホモグラフィック メソッドのどのインターフェイスのバージョンを選択するかを決定するために必要な情報が、必ずしも Java ソース コードに存在するとは限らないためです。与えられた:

interface I1 { 
    // return ASCII character code of first character of String s 
    int f(String s); // f("Hello") returns 72
}
interface I2 {
    // return number of characters in String s 
    int f(String s);  // f("Hello") returns 5
}

interface I12 extends I1, I2 {}

public class C {
  public static int f1(I1 i, String s) { return i.f(s); }  // f1( i, "Hi") == 72
  public static int f2(I2 i, String s) { return i.f(s); }  // f2( i, "Hi") == 2
  public static int f12(I12 i, String s) { return i.f(s);} // f12(i, "Hi") == ???
}

Java 言語仕様によると、実装するクラスは、 、、およびが同じ引数で呼び出されたときにまったく同じ結果を返すI12ようにする必要があります。呼び出し方によって 72 を返したり 5 を返したりすると、プログラムの重大なバグであり、言語仕様に違反します。C.f1()C.f2()C.f12()C.f12(i, "Hello")C.f12()

さらに、 クラス の作成者がCから何らかの一貫した動作を期待した場合、クラス C には、またはf12()の動作であるべきかどうかを示すバイトコードやその他の情報はありません。had in mindの作成者が5 または 72 を返す必要がある場合、コードを見て判断する方法はありません。I1.f(s)I2.f(s)C.f12()C.f("Hello")

一般的に、バイトコードのトリックを使用してホモグラフィック関数に異なる動作を提供することはできませんが、実際には私の例のクラスのようなクラスがありTheaterManagerます。実装するにはどうすればよいArtistSet.complement()ですか?

あなたが尋ねた実際の質問に対する実際の答えは、を必要としない独自の代替実装を作成することです。ライブラリの実装を変更する必要はありません。独自のものを作成する必要があります。TheaterManagerArtistSet

あなたが引用した他の例の質問に対する実際の答えは、基本的に「デリゲート」です。オブジェクトを受け取る関数は、オブジェクトを期待する関数にそのオブジェクトを渡しません。I12.f()I2.f()I12I1

スタック オーバーフローは、一般的な質問と回答のみを対象としています

ここで質問を却下する理由の 1 つは、「それは、インターネットの世界中の視聴者には一般的に当てはまらない非常に狭い状況にのみ関連している」ということです。 私たちは役に立ちたいと思っているので、そのような狭い質問を処理するための好ましい方法は、より広く適用できるように質問を修正することです. この質問については、実際に質問を編集して状況に固有のものを削除するのではなく、広く適用可能なバージョンの質問に回答するというアプローチを取りました。

商用プログラミングの現実の世界では、次のいずれかの方法でI12実装して使用できない限り、次のような壊れたインターフェイスを持つ Java ライブラリは、数十の商用クライアントを蓄積することさえできません。I12.f()

  • 委任するI1.f()
  • 委任するI2.f()
  • 何もしない
  • 例外をスローする
  • I12オブジェクトのいくつかのメンバーの値に基づいて、呼び出しごとに上記の戦略のいずれかを選択します

このライブラリのこの部分を Java で使用している企業が何千も、あるいはほんの一握りでさえある場合、それらの企業がそれらのソリューションの 1 つを使用していることを確信できます。ライブラリがほんの一握りの企業でさえ使用されていない場合、質問は Stack Overflow には狭すぎます。

OK、TheaterManager単純化しすぎました。実際のケースでは、そのクラスを置き換えるのは難しすぎて、あなたが概説した実用的な解決策はどれも好きではありません。ファンシーなJVMトリックでこれを修正することはできませんか?

何を修正したいかによります。すべての呼び出しをマッピングしてからI12.f()スタックを解析して呼び出し元を特定し、それに基づいて動作を選択することにより、特定のライブラリを修正する場合。経由でスタックにアクセスできますThread.currentThread().getStackTrace()

知らない発信者に出くわした場合、その発信者がどのバージョンを必要としているかを判断するのに苦労する可能性があります。たとえば、次のように、ジェネリックから呼び出される場合があります(他の特定の例の実際のケースと同様)。

public class TalentAgent<T extends Artist> {
  public static void butterUp(List<T> people) {
    for (T a: people) {
      a.complement()
    }
  }
}
 

Java では、ジェネリックは消去として実装されます。つまり、すべての型情報はコンパイル時に破棄されます。TalentAgent<Artist>aと a の間にクラスまたはメソッド シグネチャの違いはなく、パラメーターTalentAgent<Set>の正式な型は. 呼び出し元のクラス インターフェイスまたはメソッド シグネチャには、スタックを見て何をすべきかを伝えるものは何もありません。peopleList

そのため、複数の戦略を実装する必要があります。そのうちの 1 つは、呼び出し元がクラスまたは別のクラスを期待しているという手がかりを探して、呼び出し元のメソッドのコードを逆コンパイルすることです。これが発生する可能性のあるすべての方法をカバーするには、非常に洗練されている必要があります。これは、特に、実際に期待しているクラスを事前に知る方法がなく、インターフェイスの 1 つを実装するクラスを期待しているだけであるためです。

実行時に特定のクラスのプロキシを自動的に生成するもの (Java 言語でプロキシがサポートされるずっと前に作成されたもの) など、成熟した非常に洗練されたオープン ソース バイトコード ユーティリティがあります。このケースを処理するためのユーティリティは、このアプローチを追求する際の労力と有用性の比率について多くを語っています。

于 2013-05-20T00:10:35.267 に答える
0

あいまいさを取り除くために私がすることは次のとおりです。

interface Artist {
    void complement(); // [SIC] from OP, really "compliment"
    int getEgo();
}

interface Set {
    void complement(); // as in Set Theory
    void remove();
    boolean empty(); // [SIC] from OP, I prefer: isEmpty()
}

/**
 * This class is to represent a Set of Artists (as a group) -OR-
 * act like a single Artist (with some aggregate behavior).  I
 * choose to implement NEITHER interface so that a caller is
 * forced to designate, for any given operation, which type's
 * behavior is desired.
 */
class GroupOfArtists { // does NOT implement either

    private final Set setBehavior = new Set() {
        @Override public void remove() { /*...*/ }
        @Override public boolean empty() { return true; /* TODO */ }            
        @Override public void complement() {
            // implement Set-specific behavior
        }
    };

    private final Artist artistBehavior = new Artist() {
        @Override public int getEgo() { return Integer.MAX_VALUE; /* TODO */ }            
        @Override public void complement() {
            // implement Artist-specific behavior
        }
    };

    Set asSet() {
        return setBehavior;
    }

    Artist asArtist() {
        return artistBehavior;
    }
}

このオブジェクトを人事部に渡すとしたら、実際にはasSet()、グループ全体を雇用/解雇するために返された値をオブジェクトに渡します。

このオブジェクトをパフォーマンスのために Theatre に渡す場合、実際には返された値を渡して、asArtist()タレントとして扱われます。

これは、さまざまなコンポーネントと直接やり取りすることを制御している限り機能します...

しかし、あなたの問題は、単一のサードパーティ ベンダーが、これらの関数の両方に対して 1 つのオブジェクトを期待するコンポーネント を作成し、およびメソッドTheaterManagerについて認識しないことにあると思います。問題は and を作成したベンダーにあるのではなく、Visitor パターンを使用したり、上で作成したandメソッドをミラーリングするインターフェイスを指定したりする代わりに、それらを組み合わせたベンダーです。1 つのベンダー「C」にそのインターフェイスを修正するよう説得できれば、あなたの世界はもっと幸せになるでしょう。asSetasArtistSetArtistasSetasArtist

幸運を!

于 2013-05-19T02:02:15.930 に答える