Comment implémenter des interfaces avec des méthodes homographiques en Java?


En anglais, une paire d'homographes est deux mots qui ont la même orthographe mais des significations différentes.

En génie logiciel, une paire de méthodes homographiques est deux méthodes ayant le même nom mais des exigences différentes. Voyons un exemple artificiel pour rendre la question aussi claire que possible:

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

Comment puis-je implémenter I12? C # a un moyen de le faire, mais Java ne le fait pas. Donc le seul moyen de contourner est un hack. Comment peut-il être fait avec reflection / bytecode tricks / etc le plus fiable (c'est-à-dire que ce n'est pas nécessairement une solution parfaite, je veux juste celle qui fonctionne le mieux)?


Notez qu'un morceau massif de code hérité à source fermée existant que je ne peux pas légalement désosser nécessite un paramètre de type I12 et délègue le I12 à la fois au code qui a I1 comme paramètre et au code qui a I2 comme paramètre. Donc, fondamentalement, j'ai besoin de faire une instance de {[7] } qui sait quand elle devrait agir comme I1 et quand il devrait agir comme I2, ce qui, je crois, peut être fait en en regardant le bytecode à l'exécution de l'appelant immédiat. Nous pouvons supposer qu'aucune réflexion n'est utilisée par les appelants, car il s'agit d'un code simple. Le problème est que l'auteur de I12 ne s'attendait pas à ce que Java fusionne f à partir des deux interfaces, alors maintenant je dois trouver le meilleur hack autour du problème. Rien n'appelle I12.f (évidemment, si l'auteur a écrit un code qui appelle réellement I12.f, il aurait remarqué le problème avant de la vendre).

Notez que je cherche en fait une réponse à cette question, pas comment restructurer le code que je ne peux pas changer. Je cherche la meilleure heuristique possible ou une solution exacte si elle existe. Voir la réponse de Gray pour un exemple valide (je suis sûr qu'il existe des solutions plus robustes).


Voici un exemple concret de la façon dont le problème des méthodes homographiques à l'intérieur de deux interfaces peut se produire. Et voici un autre exemple concret:

J'ai les 6 classes/interfaces simples suivantes. Cela ressemble à une entreprise autour d'un théâtre et des artistes qui s'y produisent. Pour plus de simplicité et pour être précis, supposons qu'ils sont tous créés par des personnes différentes.

Set représente un ensemble, comme dans la théorie des ensembles:

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();
}

HRDepartment utilise Set pour représenter les employés. Il utilise un processus sophistiqué pour décoder quels employés embaucher/licencier:

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();
    }
}

L'univers d'un Set de salariés serait probablement les salariés qui ont demandé à l'employeur. Ainsi, lorsque complement est appelé sur cet ensemble, tous les employés existants sont licenciés et tous les autres qui ont postulé précédemment sont embauchés.

Artist représente un artiste, comme un musicien ou un acteur. Un artiste a un ego. Cet ego peut augmenter quand les autres le complimentent:

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

Theater effectue un Artist, ce qui provoque éventuellement le Artist à compléter. Le public du théâtre peut juger l'artiste entre les représentations. Plus l'ego de l'artiste est élevé, plus le public est susceptible d'aimer le Artist, mais si l'ego dépasse un certain point, l'artiste sera perçu négativement par le public:

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 est tout simplement un Artist et a Set:

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

TheaterManager dirige le spectacle. Si le public du théâtre juge négativement l'artiste, le théâtre parle au département des ressources humaines, qui à son tour licenciera des artistes, en embauchera de nouveaux, etc.:

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();
        }
    }
}

Le problème devient clair une fois que vous essayez d'implémenter un ArtistSet: les deux superinterfaces spécifient que complement devrait faire autre chose, vous devez donc implémenter deux méthodes complement avec la même signature dans la même classe, d'une manière ou d'une autre. Artist.complement est une homographie de Set.complement.

Author: Community, 2013-05-11

7 answers

Nouvelle idée, un peu désordonnée...

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
        }
    }
}
 4
Author: Gray Kemmey, 2013-05-11 07:23:53

Malgré la vaillante tentative de Gray Kemmey, je dirais que le problème tel que vous l'avez déclaré n'est pas résoluble. En règle générale, étant donné un ArtistSet, vous ne pouvez pas savoir si le code l'appelant attendait un Artist ou un Set.

De plus, même si vous pouviez, selon vos commentaires sur diverses autres réponses, vous avez en fait l'obligation de passer un ArtistSet à une fonction fournie par le fournisseur, ce qui signifie que cette fonction n'a donné aucune idée au compilateur ou aux humains de ce qu'il attend. Vous êtes complètement hors de la chance pour toute sorte de réponse techniquement correcte.

Comme la programmation pratique est importante pour faire le travail, je ferais ce qui suit (dans cet ordre):

  1. Déposer un rapport de bogue avec celui qui a créé une interface nécessitant ArtistSet et celui qui a généré l'interface ArtistSet elle-même.
  2. Déposez une demande de support auprès du fournisseur fournissant la fonction nécessitant un ArtistSet et demandez-leur ce qu'ils attendent du comportement de complement().
  3. Implémentez la fonction complement() pour lancer une exception.
public class Sybil implements ArtistSet {
  public void complement() { 
    throw new UnsupportedOperationException('What am I supposed to do'); 
  }
  ...
}

Parce que sérieusement, tu ne sais pas quoi faire. Quelle serait la bonne chose à faire quand on l'appelle comme ça (et comment savez-vous bien sûr)?

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

En lançant une exception, vous avez une chance d'obtenir une trace de pile qui vous donne un indice sur lequel des deux comportements l'appelant attend. Avec de la chance, personne n'appelle cette fonction, c'est pourquoi le fournisseur est allé aussi loin que le code d'expédition avec ceci problème. Avec moins de chance mais encore un peu, ils gèrent l'exception. Sinon même cela, eh bien, au moins maintenant, vous aurez une trace de pile que vous pouvez examiner pour décider ce que l'appelant attendait vraiment et éventuellement l'implémenter (bien que je frissonne de penser à perpétuer un bug de cette façon, j'ai expliqué comment je le ferais dans cette autre réponse).

BTW, pour le reste de l'implémentation, je déléguerais tout aux objets réels Artist et Set transmis via le constructeur cela peut être facilement séparé plus tard.

 3
Author: Old Pro, 2017-05-23 12:30:18

Comment Résoudre Votre Cas Spécifique

ArtistSet est tout simplement un Artiste et un Ensemble:

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

D'un point de vue OO, ce n'est pas une déclaration utile. Un artiste est un type de nom, une "chose" qui a défini des propriétés et des actions (méthodes). Un ensemble est un agrégat de choses-une collection d'éléments uniques. Au lieu de cela, essayez:

ArtistSet est simplement un ensemble d'artistes.

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

Alors, pour votre cas particulier, l'homonyme les méthodes sont sur des interfaces qui ne sont jamais combinées dans le même type, vous n'avez donc pas de conflit et vous pouvez programmer...

De plus, vous n'avez pas besoin de déclarer ArtistSet car vous n'étendez pas réellement Set avec de nouvelles déclarations. Vous instanciez simplement un paramètre de type, vous pouvez donc remplacer toute utilisation par Set<Artist>.

Comment Résoudre le Cas Plus Général

Pour ce conflit, les noms de méthode n'ont même pas besoin d'être homographiques au sens anglais-ils peut être le même mot avec la même signification anglaise, utilisé dans différents contextes en java. Le conflit se produit si vous avez deux interfaces que vous souhaitez appliquer à un type mais qu'elles contiennent la même déclaration (par exemple signature de méthode) avec des définitions sémantiques/de traitement contradictoires.

Java ne vous permet pas d'implémenter le comportement que vous demandez - vous devez avoir une solution de contournement alternative. Java ne permet pas à une classe de fournir plusieurs implémentations pour la même signature de méthode à partir de plusieurs interfaces différentes (implémentant la même méthode plusieurs fois avec une certaine forme de qualification/alias/annotation à distinguer). Voir Java remplaçant deux interfaces, conflit de noms de méthode, Java - Méthode de collision de nom dans l'implémentation de l'interface

Par exemple Si vous avez ce qui suit

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

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

Ensuite, si vous avez un objet qui est ces deux choses, vous pouvez combiner les deux dans une nouvelle interface (facultative) ou taper:

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 };
}
 3
Author: Glen Best, 2017-05-23 11:44:35

Comment puis-je implémenter une classe qui a deux superinterfaces ayant des méthodes homographiques?

En Java, une classe qui a deux superinterfaces ayant des méthodes homographiques est considérée comme n'ayant qu'une seule implémentation de cette méthode. (Voir leJava Language Specification section 8.4.8 ). Cela permet aux classes d'hériter facilement de plusieurs interfaces qui implémentent toutes la même autre interface et n'implémentent la fonction qu'une seule fois. Cela simplifie également la langue parce que cela élimine le besoin de prise en charge de la syntaxe et de la répartition des méthodes pour distinguer les méthodes homographiques en fonction de l'interface d'où elles proviennent.

Donc, la bonne façon d'implémenter une classe qui a deux superinterfaces ayant des méthodes homographiques est de fournir une seule méthode qui satisfait les contrats des deux superinterfaces.

C# a un moyen de le faire. Comment cela peut-il être fait en Java? N'y a-t-il pas de construction pour cela?

C # définit les interfaces différemment que Java fait et a donc des capacités que Java ne fait pas.

En Java, la construction du langage est définie pour signifier que toutes les interfaces obtiennent la même implémentation unique de méthodes homographiques. Il n'y a pas de construction de langage Java pour créer des comportements alternatifs de fonctions d'interface héritées par multiplication en fonction de la classe de compilation de l'objet. C'était un choix conscient fait par les concepteurs du langage Java.

Sinon, comment peut-on le faire avec reflection / bytecode astuces/etc le plus fiable?

"Cela" ne peut pas être fait avec des astuces de réflexion/bytecode car les informations nécessaires pour décider quelle version d'interface de la méthode homographique choisir ne sont pas nécessairement présentes dans le code source Java. Donné:

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") == ???
}

Selon la spécification du langage Java, une classe implémentant I12 doit le faire de telle manière que C.f1(), C.f2(), et C.f12() renvoie exactement le même résultat lorsqu'il est appelé avec les mêmes arguments. Si C.f12(i, "Hello") renvoie parfois 72 et parfois renvoyé 5 en fonction de la façon dont C.f12() ont été appelés, ce serait un bug sérieux dans le programme et une violation de la spécification du langage.

De plus, si l'auteur de la classe C s'attendait à un comportement cohérent de f12(), il n'y a pas de bytecode ou d'autres informations dans la classe C qui indiquent s'il devrait s'agir du comportement de I1.f(s) ou I2.f(s). Si l'auteur de C.f12() avait à l'esprit C.f("Hello") devrait revenir 5 ou 72, il n'y a aucun moyen de dire en regardant le code.

Bien, donc je ne peux pas en général fournir des comportements différents pour les fonctions homographiques en utilisant des astuces de bytecode, mais j'ai vraiment une classe comme mon exemple de classe TheaterManager. Que dois-je faire pour implémenter ArtistSet.complement()?

La réponse réelleà la question réelle que vous avez posée est de créer votre propre implémentation de remplacement de TheaterManager qui ne nécessite pas de ArtistSet. Vous n'avez pas besoin de changer l'implémentation de la bibliothèque, vous devez écrire la vôtre.

Le la réponse réelleà la autre exemple de question que vous citez est essentiellement "déléguer I12.f() à I2.f()" car aucune fonction qui reçoit un objet I12 ne passe cet objet à une fonction qui attend un objet I1.

Stack Overflow est uniquement pour les questions et réponses d'intérêt général

L'une des raisons énoncées pour rejeter une question ici est que " elle n'est pertinente que pour une situation extraordinairement étroite qui ne s'applique généralement pas à la audience mondiale de l'internet." Parce que nous voulons être utiles, la meilleure façon de traiter des questions aussi étroites est de réviser la question pour qu'elle soit plus largement applicable. Pour cette question, j'ai pris l'approche de répondre à la version largement applicable de la question plutôt que de modifier la question pour supprimer ce qui la rend unique à votre situation.

Dans le monde réel de la programmation commerciale, toute bibliothèque Java qui a une interface cassée comme I12 le ferait ne pas accumuler même des dizaines de clients commerciaux à moins qu'il ne puisse être utilisé en mettant en œuvre I12.f() de l'une des manières suivantes:

  • délégué à I1.f()
  • délégué à I2.f()
  • ne rien faire
  • lancer une exception
  • choisissez l'une des stratégies ci-dessus sur chaque appel fondé sur les valeurs de certains membres de la I12 object

Si des milliers ou même seulement une poignée d'entreprises utilisent cette partie de cette bibliothèque en Java alors vous pouvez être assuré ils ont utilisé l'une de ces solutions. Si la bibliothèque n'est pas utilisée même par une poignée d'entreprises, la question est trop étroite pour le débordement de pile.

OK, TheaterManager était une simplification excessive. Dans le cas réel, il est trop difficile pour moi de remplacer cette classe et je n'aime aucune des solutions pratiques que vous avez décrites. Ne puis-je pas simplement résoudre ce problème avec des astuces JVM sophistiquées?

Cela dépend de ce que vous voulez corriger. Si vous souhaitez corriger votre bibliothèque spécifique en mappant tous les appelle à I12.f(), puis analyse la pile pour déterminer l'appelant et choisir un comportement en fonction de cela. Vous pouvez accéder à la pile via Thread.currentThread().getStackTrace().

Si vous rencontrez un appelant que vous ne reconnaissez pas, vous aurez peut-être du mal à déterminer quelle version ils veulent. Par exemple, vous pouvez être appelé à partir d'un générique (comme c'était le cas dans le autre exemple spécifique que vous avez donné), comme:

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

En Java, les génériques sont implémentés comme des effacements , ce qui signifie tout type les informations sont jetées au moment de la compilation. Il n'y a pas de différence de signature de classe ou de méthode entre a TalentAgent<Artist> et a TalentAgent<Set> et le type formel du paramètre people est juste List. Il n'y a rien dans l'interface de classe ou la méthode de la signature de l'appelant pour vous dire quoi faire en regardant la pile.

Vous devrez donc implémenter plusieurs stratégies, dont l'une serait de décompiler le code de la méthode appelante à la recherche d'indices que l'appelant attend une classe ou l'autre. Il devrait être très sophistiqué pour couvrir toutes les façons dont cela pourrait se produire, car entre autres choses, vous n'avez aucun moyen de savoir à l'avance quelle classe elle attend réellement, seulement qu'elle attend une classe qui implémente l'une des interfaces.

Il existe des utilitaires de bytecode open source matures et extrêmement sophistiqués, y compris un qui génère automatiquement un proxy pour une classe donnée à l'exécution (écrit bien avant qu'il n'y ait un support pour cela dans Java le fait qu'il n'y ait pas d'utilitaire open source pour gérer ce cas en dit long sur le rapport entre l'effort et l'utilité dans la poursuite de cette approche.

 1
Author: Old Pro, 2020-06-20 09:12:55

D'accord, après de nombreuses recherches, j'ai une autre idée pour m'adapter pleinement à la situation. Puisque vous ne pouvez pas modifier directement leur code... vous pouvez forcer les modifications vous-même.

AVERTISSEMENT: L'exemple de code ci-dessous est très simplifié. Mon intention est de montrer la méthode générale de la façon dont cela pourrait être fait, pas de produire du code source fonctionnel pour le faire (puisque c'est un projet en soi).

Le problème est que les méthodes sont homographiques. Donc, pour le résoudre, nous pouvons renommez simplement les méthodes. Simple, non? Nous pouvons utiliser leInstrument package pour y parvenir. Comme vous le verrez dans la documentation liée, il vous permet de créer un "agent" qui peut directement modifier les classes au fur et à mesure qu'elles sont chargées ou les modifier à nouveau même si elles ont déjà été chargées.

Essentiellement, cela vous oblige à faire deux classes:

  • Une classe d'agent qui préprocède et recharge les classes; et,
  • Un ClassFileTransformer la mise en œuvre qui spécifie les modifications que vous souhaitez apporter.

La classe d'agent doit avoir une méthode premain() ou agentmain() définie, selon que vous souhaitez qu'elle commence son traitement au démarrage de la JVM ou après son exécution. Des exemples de ceci sont dans la documentation du paquet ci-dessus. Ces méthodes vous donnent accès à un Instrumenation exemple, qui vous permettra d'enregistrer votre ClassFileTransformer. Donc cela pourrait ressembler à quelque chose comme ceci:

InterfaceFixAgent.java

public class InterfaceFixAgent {

    public static void premain(String agentArgs, Instrumentation inst) {

        //Register an ArtistTransformer
        inst.addTransformer(new ArtistTransformer());

        //In case the Artist interface or its subclasses 
        //have already been loaded by the JVM
        try {
            for(Class<?> clazz : inst.getAllLoadedClasses()) {
                if(Artist.class.isAssignableFrom(clazz)) {
                    inst.retransformClasses(clazz);
                }
            }
        }
        catch(UnmodifiableClassException e) {
            //TODO logging
            e.printStackTrace();
        }
    }
}

Artisttransformateur.java

public class ArtistTransformer implements ClassFileTransformer {

    private static final byte[] BYTES_TO_REPLACE = "complement".getBytes();
    private static final byte[] BYTES_TO_INSERT = "compliment".getBytes();

    @Override
    public byte[] transform(ClassLoader loader, String className,
                            Class<?> classBeingRedefined, ProtectionDomain protectionDomain,
                            byte[] classfileBuffer) throws IllegalClassFormatException {

        if(Artist.class.isAssignableFrom(classBeingRedefined)) {
            //Loop through the classfileBuffer, find sequences of bytes
            //which match BYTES_TO_REPLACE, replace with BYTES_TO_INSERT
        }
        else return classfileBuffer;
    }

Ceci est, bien sûr, simplifié. Il remplacera le mot "complément" par "compliment" dans n'importe quelle classe qui extends ou implements Artist, vous aurez donc très probablement besoin de le conditionner davantage (par exemple, si Artist.class.isAssignableFrom(classBeingRedefined) && Set.class.isAssignableFrom(classBeingRedefined), vous ne voulez évidemment pas remplacer chaque instance de "complément" par "compliment", car le "complément" pour Set est parfaitement légitime).

Donc, maintenant, nous avons corrigé l'interface Artist et ses implémentations. La faute de frappe a disparu, les méthodes ont deux noms différents, il n'y a donc pas d'homographie. Cela nous permet d'avoir deux implémentations différentes dans notre classe CommunityTheatre maintenant, chacune implémentant/remplaçant correctement les méthodes du ArtistSet.

Malheureusement, nous avons maintenant créé un autre problème (peut-être encore plus grand). Nous venons de casser toutes les références précédemment légitimes à complement() des classes implémentant Artist. De corrigez cela, nous devrons créer un autre ClassFileTransformer qui remplace ces appels par notre nouveau nom de méthode.

C'est un peu plus difficile, mais pas impossible. Essentiellement, la nouvelle ClassFileTransformer (disons que nous appelons le OldComplementTransformer) devra effectuer les étapes suivantes:

  1. Trouve la même chaîne d'octets que précédemment (celle représentant l'ancien nom de méthode, "complement");
  2. Obtenez les octets avant qui représentent la référence de l'objet appelant le méthode;
  3. Convertir ces octets en un Object;
  4. Vérifier pour voir si Object est un Artist; et,
  5. Si c'est le cas, remplacez ces octets par le nouveau nom de la méthode.

Une fois que vous avez fait ce deuxième transformateur, vous pouvez modifier le InterfaceFixAgent pour l'accueillir. (J'ai également simplifié l'appel retransformClasses(), car dans l'exemple ci-dessus, nous effectuons la vérification nécessaire dans le transformateur lui-même.)

InterfaceFixAgent.java (modifié )

public class InterfaceFixAgent {

    public static void premain(String agentArgs, Instrumentation inst) {

        //Register our transformers
        inst.addTransformer(new ArtistTransformer());
        inst.addTransformer(new OldComplementTransformer());

        //Retransform the classes that have already been loaded
        try {
            inst.retransformClasses(inst.getAllLoadedClasses());
        }
        catch(UnmodifiableClassException e) {
            //TODO logging
            e.printStackTrace();
        }
    }
}

Et maintenant... notre programme est bon pour aller. Ce ne serait certainement pas facile à coder, et ce sera un enfer pour l'assurance qualité et le test. Mais c'est certainement robuste, et il résout le problème. (Techniquement, je suppose que évite le problème en le supprimant, mais... Je prendrai ce que je peux obtenir.)

D'autres façons de résoudre le problème:

Les deux vous permettraient de manipuler directement des octets en mémoire. Une solution pourrait certainement être conçue autour de ceux-ci, mais je pense que ce serait beaucoup plus difficile et beaucoup moins sûr. Je suis donc allé avec l'itinéraire ci-dessus.

Je pense que cette solution pourrait même être rendue plus générique dans une bibliothèque incroyablement utile pour intégrer des bases de code. Spécifiez quelle interface et quelle méthode vous devez refactoriser dans une variable, un argument de ligne de commande ou une configuration fichier, et qu'elle lâche. La bibliothèque qui réconcilie les interfaces en conflit dans Java lors de l'exécution. (Bien sûr, je pense que ce serait toujours mieux pour tout le monde s'ils venaient de corriger le bogue dans Java 8.)

 1
Author: asteri, 2013-07-18 19:26:01

Voici ce que je ferais pour supprimer l'ambiguïté:

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;
    }
}

Si je transmettais cet objet au service des ressources humaines, je lui donnerais en fait la valeur renvoyée par asSet() pour embaucher/licencier l'ensemble du groupe.

Si je transmettais cet objet au Théâtre pour une représentation, je lui donnerais en fait la valeur renvoyée par asArtist() pour être traité comme un talent.

Cela fonctionne tant que VOUS avez le contrôle de parler directement aux différents composants...

Mais je me rends compte que votre problème est qu'un seul fournisseur tiers a créé un composant, TheaterManager, qui attend un objet pour ces deux fonctions et il ne connaîtra pas les méthodes asSet et asArtist. Le problème n'est pas avec les fournisseurs qui ont créé Set et Artist, c'est le fournisseur qui les a combinés au lieu d'utiliser un modèle de visiteur ou simplement de spécifier une interface qui refléterait les méthodes asSet et asArtist que j'ai faites ci-dessus. Si vous pouvez convaincre votre fournisseur " C " de réparer cette interface, votre monde sera un beaucoup plus heureux.

Bonne chance!

 0
Author: William Price, 2013-05-19 02:24:52

Chien, j'ai un fort sentiment que vous laissez de côté certains détails qui sont cruciaux pour la solution. Cela arrive souvent sur SO parce que

  • les gens doivent laisser de côté beaucoup de détails pour obtenir la question à une taille et une portée raisonnables,
  • les gens ne comprennent pas complètement le problème et la solution (c'est pourquoi ils demandent de l'aide), ils ne peuvent donc pas être sûrs des détails qui sont importants et des détails qui ne le sont pas, et
  • la raison pour laquelle la personne ne peut pas résoudre le problème sur leur propre parce qu'ils ne comprennent pas l'importance de ce détail, qui est la même raison qu'ils ont laissée.

J'ai dit dans une autre réponse ce que je ferais à propos d'ArtistSet. Mais en gardant ce qui précède à l'esprit, je vais vous donner une autre solution à un problème légèrement différent. Disons que j'avais du code d'un mauvais fournisseur:

package com.bad;

public interface IAlpha {
    public String getName();
    // Sort Alphabetically by Name
    public int compareTo(IAlpha other);
}

C'est mauvais car vous devez déclarer une fonction renvoyant un Comparator<IAlpha> pour implémenter la stratégie de tri, mais peu importe. Maintenant, je reçois le code d'un pire entreprise:

package com.worse;
import com.bad.IAlpha;

// an Alpha ordered by name length
public interface ISybil extends IAlpha, Comparable<IAlpha> {}

C'est pire, parce que c'est totalement faux, en ce qu'il remplace le comportement incompatibilité. Un ISybil ordres lui-même par la longueur de ce nom, mais un IAlpha ordonne par ordre alphabétique, sauf un ISybil est un IAlpha. Ils ont été induits en erreur par l'anti-modèle d'IAlpha alors qu'ils auraient pu et dû faire quelque chose comme:

public interface ISybil extends IAlpha {
  public Comparator<IAlpha> getLengthComparator();
}

Cependant, cette situation est encore bien meilleure qu'ArtistSet car ici le comportement attendu est documenter. Il n'y a pas de confusion sur ce que ISybil.compareTo() devrait faire. Je créerais donc des classes comme suit. Une classe Sybil qui implémente compareTo () en tant que com.pire attend et délègue tout le reste:

package com.hack;

import com.bad.IAlpha;
import com.worse.ISybil;

public class Sybil implements ISybil {

    private final Alpha delegate;

    public Sybil(Alpha delegate) { this.delegate = delegate; }
    public Alpha getAlpha() {   return delegate; }
    public String getName() { return delegate.getName(); }
    public int compareTo(IAlpha other) {
        return delegate.getName().length() - other.getName().length();
    }

}

Et une classe Alpha qui fonctionne exactement comme com.bad a dit qu'il devrait:

package com.hack;
import com.bad.IAlpha;

public class Alpha implements IAlpha {
    private String name;
    private final Sybil sybil;
    public Alpha(String name) { 
        this.name = name;
        this.sybil = new Sybil(this);
    }

    // Sort Alphabetically
    public int compareTo(IAlpha other) {
        return name.compareTo(other.getName());
    }

    public String getName() { return name; }
    public Sybil getSybil() { return sybil; }
}

Notez que j'ai inclus des méthodes de conversion de type: Alpha.getSybil () et Sybil.getAlpha(). C'est pour que je puisse créer mes propres wrappers autour de n'importe quelle com.pire les méthodes du fournisseur qui prennent ou renvoient des Sybils je peux donc éviter de polluer mon code ou le code de tout autre fournisseur avec com.le pire de la rupture. Donc, si com.pire avait:

public ISybil breakage(ISybil broken);

Je pourrais écrire une fonction

public Alpha safeDelegateBreakage(Alpha alpha) {
  return breakage(alpha.getSybil).getAlpha();
}

Et en finir, sauf que je me plaindrais toujours avec véhémence à com.pire et poliment à com.mauvais.

 -1
Author: Old Pro, 2013-05-18 19:06:33