Как реализовать интерфейсы с гомографическими методами в Java?

На английском языке пара гомографов - это два слова, которые имеют одинаковое правописание, но разные значения.

В разработке программного обеспечения пара гомографических методов - это два метода с одним и тем же именем, но с разными требованиями. Посмотрим на надуманный пример, чтобы сделать вопрос максимально ясным:

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

Как я могу реализовать I12? С# имеет способ сделать это, но Java нет. Таким образом, единственный способ - это взломать. Как это можно сделать с помощью трюков reflection/bytecode/etc наиболее надежно (т.е. Это не должно быть идеальным решением, я просто хочу, чтобы тот работал лучше всего)?


Обратите внимание, что какой-то существующий массив массивных фрагментов с закрытым исходным кодом, который я не могу законно развернуть, требует параметра типа I12 и делегирует I12 как код, который имеет I1 в качестве параметра, так и код, который имеет I2 в качестве параметра. Поэтому в основном мне нужно создать экземпляр I12, который знает, когда он должен действовать как I1, и когда он должен действовать как I2, который, я считаю, может быть сделан на байт-код во время выполнения непосредственного вызывающего абонента. Мы можем предположить, что вызывающие не используют никакого отражения, потому что это простой код. Проблема в том, что автор I12 не ожидал, что Java слияния f с обоих интерфейсов, так что теперь я должен придумать лучший взломать проблему. Ничто не вызывает I12.f (очевидно, если автор написал код, который фактически называет I12.f, он бы заметил проблему перед ее продажей).

Обратите внимание, что я действительно ищу ответ на этот вопрос, а не как изменить структуру кода, который я не могу изменить. Я ищу лучшее эвристическое возможное или точное решение, если оно существует. См. "Ответ на серый" для допустимого примера (я уверен, что есть более надежные решения).


Здесь - конкретный пример того, как может возникнуть проблема гомографических методов в двух интерфейсах. И вот еще один конкретный пример:

У меня есть следующие 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();
}

HRDepartment использует Set для представления сотрудников. Он использует сложный процесс для декодирования, который сотрудники нанимают/стреляют:

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

Вселенная a 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 - это просто Artist и a Set:

/** 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 с одной и той же сигнатурой внутри одного класса. Artist.complement является гомографом Set.complement.

Ответы

Ответ 1

Новая идея, какая-то грязная...

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

Ответ 2

Как решить конкретный случай

ArtistSet - это просто Исполнитель и набор:

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

С точки зрения ОО, это не полезное объявление. Художник - это тип существительного, "вещь", которая определила свойства и действия (методы). Набор - совокупность вещей - коллекция уникальных элементов. Вместо этого попробуйте:

ArtistSet - это просто набор исполнителей.

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

Затем для вашего конкретного случая методы омонимии находятся на интерфейсах, которые никогда не объединяются в одном типе, поэтому у вас нет столкновения и можно запрограммировать прочь...

Кроме того, вам не нужно объявлять ArtistSet, потому что вы фактически не расширяете Set с любыми новыми объявлениями. Вы просто создаете параметр типа, поэтому вы можете заменить все использование Set<Artist>.

Как решить более общий случай

Для этого столкновения имена методов не обязательно должны быть гомографическими в смысле английского языка - они могут быть одним и тем же словом с одним и тем же английским значением, используемым в разных контекстах в java. Clash возникает, если у вас есть два интерфейса, которые вы хотите применить к типу, но они содержат одно и то же объявление (например, подпись метода) с противоречивыми определениями семантики/обработки.

Java не позволяет вам выполнять поведение, которое вы запрашиваете, - вы должны иметь альтернативный подход.. Java не позволяет классу предоставлять несколько реализаций для одной и той же сигнатуры метода из нескольких разных интерфейсы (один и тот же метод несколько раз с некоторой формой квалификации/псевдонима/аннотации для различения). См. Java, переопределяющий два интерфейса, столкновение имен методов, Java - столкновение имен методов в реализации интерфейса

например. Если у вас есть следующие

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

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

Затем, если у вас есть объект, который является обоим из этих вещей, вы можете объединить два в новом интерфейсе (необязательно) или ввести:

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

Несмотря на доблестную попытку Грей Кемми, я бы сказал, что проблема, как вы заявили, не разрешима. Как правило, с учетом ArtistSet вы не можете знать, ожидал ли код, вызывающий его, Artist или Set.

Кроме того, даже если бы вы могли, в соответствии с вашими комментариями по другим другим ответам, у вас действительно есть требование передать ArtistSet функции поставщика, что означает, что функция не предоставила компилятору или людям какую-либо подсказку как к чему он ожидает. Вам совершенно не повезло с любым технически правильным ответом.

Как практический вопрос программирования для выполнения работы, я бы сделал следующее (в этом порядке):

  • Распечатайте отчет об ошибке с тем, кто создал интерфейс, требующий ArtistSet, и тот, кто сгенерировал интерфейс ArtistSet.
  • Отправьте запрос поддержки поставщику, предоставляющему функцию, требующую ArtistSet, и спросите их, что они ожидают поведения complement().
  • Реализовать функцию 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();
    }
}

Выбрасывая исключение, у вас есть шанс получить трассировку стека, которая дает вам представление о том, какое из двух типов поведения ожидает ожидающий. С удачей никто не называет эту функцию, поэтому поставщик дошел до кода доставки с этой проблемой. С меньшей удачей, но все же некоторые, они обрабатывают исключение. Если даже не это, ну, по крайней мере, теперь у вас будет трассировка стека, которую вы можете просмотреть, чтобы решить, что вызывающий абонент действительно ожидал и, возможно, реализовал (хотя я содрогаюсь, чтобы думать о увековечивании ошибки таким образом, я объяснил, как я сделал бы это в этом другом ответе).

Кстати, для остальной части реализации я делегировал все фактическим Artist и Set объектам, переданным через конструктор, чтобы это можно было легко разделить позже.

Ответ 4

Как я могу реализовать класс, который имеет два суперинтерфейса с гомографическими методами?

В Java класс, который имеет два суперинтерфейса, имеющих гомографические методы, считается имеющим только одну реализацию этого метода. (См. Раздел Java Language Specification раздел 8.4.8). Это позволяет классам удобно наследовать от нескольких интерфейсов, которые реализуют один и тот же другой интерфейс и только реализуют эту функцию один раз. Это также упрощает язык, потому что это устраняет необходимость в синтаксисе и методе диспетчеризации поддержки для различения гомографических методов, основанных на том, с какого интерфейса они пришли.

Таким образом, правильный способ реализации класса, который имеет два суперинтерфейса, имеющих гомографические методы, состоит в том, чтобы предоставить единственный метод, который удовлетворяет контрактам обоих суперинтерфейсов.

У С# есть способ сделать это. Как это можно сделать на Java? Нет ли для этого конструкции?

С# определяет интерфейсы по-разному, чем Java, и поэтому обладает возможностями, которых нет у Java.

В Java конструкция языка определяется как означающая, что все интерфейсы получают одну и ту же единую реализацию гомографических методов. Не существует Java-языка для создания альтернативного поведения многонаследованных функций интерфейса, основанного на классе времени компиляции объекта. Это был сознательный выбор, сделанный разработчиками языка Java.

Если нет, как это можно сделать с помощью трюков отражения/байт-кода/etc наиболее надежно?

"Это" не может быть сделано с помощью трюков отражения/байткода, потому что информация, необходимая для решения, какая версия интерфейса для выбранного гомографического метода не обязательно присутствует в исходном коде 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, должен делать это таким образом, чтобы C.f1(), C.f2() и C.f12() возвращали тот же самый результат при вызове с теми же аргументами. Если C.f12(i, "Hello") иногда возвращалось 72 и иногда возвращалось 5 в зависимости от того, как были вызваны C.f12(), это было бы серьезной ошибкой в ​​программе и нарушением спецификации языка.

Кроме того, если автор класса C ожидал какого-либо согласованного поведения из f12(), в классе C нет байт-кода или другой информации, указывающей, должно ли оно быть поведение I1.f(s) или I2.f(s). Если автор C.f12() имел в виду C.f("Hello"), должен возвращать 5 или 72, нет способа сказать, глядя на код.

Прекрасно, поэтому я не могу в целом обеспечить разные типы поведения для гомографических функций, используя приемы байт-кода, но у меня действительно есть класс, подобный моему примеру class TheaterManager. Что делать, чтобы реализовать ArtistSet.complement()?

Фактический ответ на фактический вопрос, который вы попросили, - создать свою собственную заменяющую реализацию TheaterManager, для которой не требуется ArtistSet. Вам не нужно менять реализацию библиотеки, вам нужно написать свой собственный.

Фактический ответ на другой пример вопроса, который вы цитируете, в основном "делегат I12.f() до I2.f()", потому что никакая функция, которая получает объект I12, продолжает передавать этот объект функции, ожидающей объект I1.

Переполнение стека только для вопросов и ответов, представляющих общий интерес

Одна из заявленных причин отклонить вопрос здесь заключается в том, что "это относится только к чрезвычайно узкой ситуации, которая обычно не применима к мировой аудитории Интернета". Поскольку мы хотим быть полезными, предпочтительный способ решения таких узких вопросов состоит в том, чтобы пересмотреть этот вопрос, чтобы он был более широко применимым. По этому вопросу я принял подход, чтобы ответить на широко применимую версию вопроса, а не на фактическое редактирование вопроса, чтобы удалить то, что делает его уникальным для вашей ситуации.

В реальном мире коммерческого программирования любая библиотека Java, имеющая сломанный интерфейс, такой как I12, не будет накапливать даже десятки коммерческих клиентов, если она не может быть использована при реализации I12.f() одним из следующих способов:

  • делегат I1.f()
  • делегат I2.f()
  • ничего не делать
  • выполнить исключение
  • выберите одну из вышеперечисленных стратегий для каждого вызова, основываясь на значениях некоторых членов объекта I12

Если тысячи или даже несколько компаний используют эту часть этой библиотеки на Java, вы можете быть уверены, что использовали одно из этих решений. Если библиотека не используется даже несколькими компаниями, вопрос слишком узкий для.

ОК, 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, generics реализованы как стирания, что означает, что вся информация о типе отбрасывается во время компиляции. Различия между символами TalentAgent<Artist> и TalentAgent<Set> не существуют, а формальный тип параметра people равен List. Нет ничего в интерфейсе класса или сигнатуре метода вызывающего, чтобы рассказать вам, что делать, глядя на стек.

Таким образом, вам нужно будет реализовать несколько стратегий, один из которых будет декомпилировать код вызывающего метода, который ищет ключи, которые вызывающий абонент ожидает от одного класса. Это должно быть очень сложным, чтобы охватить все способы, которыми это может случиться, потому что, помимо прочего, вы не можете заранее знать, какой класс он действительно ожидает, только то, что он ожидает класс, который реализует один из интерфейсов.

Существуют зрелые и чрезвычайно сложные утилиты bytecode с открытым исходным кодом, в том числе такие, которые автоматически генерируют прокси для данного класса во время выполнения (написаны задолго до того, как была поддержка этого на языке Java), поэтому тот факт, что нет утилита с открытым исходным кодом для обработки этого случая говорит о том, как соотносятся усилия и полезность в реализации этого подхода.

Ответ 5

Хорошо, после долгих исследований у меня есть еще одна идея, чтобы полностью учесть ситуацию. Поскольку вы не можете напрямую изменить свой код... вы можете самостоятельно внести изменения.

ОТКАЗ ОТ ОТВЕТСТВЕННОСТИ: Примерный код, приведенный ниже, очень упрощен. Мое намерение состоит в том, чтобы показать общий метод того, как это можно сделать, а не создавать исполняемый исходный код для этого (так как сам проект).

Проблема в том, что методы являются гомографическими. Поэтому, чтобы решить эту проблему, мы можем просто переименовать методы. Простой, не так ли? Для этого мы можем использовать Инструментальный пакет. Как вы увидите в связанной документации, это позволяет вам создать "агент", который может непосредственно изменять классы по мере их загрузки или изменения их, даже если они уже были загружены.

По сути, это требует от вас создания двух классов:

  • Класс агента, который предварительно обрабатывает и перезагружает классы; и,
  • A ClassFileTransformer реализация, которая определяет изменения, которые вы хотите внести.

Класс агента должен иметь либо метод premain(), либо agentmain(), определенный, исходя из того, хотите ли вы, чтобы он начал свою обработку при запуске JVM или после того, как он уже запущен. Примеры этого приведены в приведенной выше документации пакета. Эти методы предоставляют вам доступ к экземпляру Instrumenation, который позволит вам зарегистрировать ваш ClassFileTransformer. Таким образом, это может выглядеть примерно так:

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

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

Это, конечно, упрощается. Он заменит слово "дополнение" на "комплимент" в любом классе, который extends или implements Artist, поэтому вам, скорее всего, потребуется дополнительно его условно (например, если Artist.class.isAssignableFrom(classBeingRedefined) && Set.class.isAssignableFrom(classBeingRedefined), вы, очевидно, не хотите замените каждый экземпляр "дополнения" на "комплимент", так как "дополнение" для Set совершенно законно).

Итак, теперь мы скорректировали интерфейс Artist и его реализации. Опечатка ушла, методы имеют два разных имени, поэтому гомографии нет. Это позволяет нам иметь две разные реализации в нашем классе CommunityTheatre, каждый из которых будет правильно реализовывать/переопределять методы из ArtistSet.

К сожалению, теперь мы создали еще одну (возможно, еще большую) проблему. Мы только что сломали все ранее легитимные ссылки на complement() из классов, реализующих Artist. Чтобы исправить это, нам нужно будет создать еще один ClassFileTransformer, который заменяет эти вызовы нашим новым именем метода.

Это несколько сложнее, но не невозможно. По существу, новый ClassFileTransformer (скажем, мы называем его OldComplementTransformer) должен выполнить следующие шаги:

  • Найдите ту же строку байтов, что и раньше (тот, который представляет старое имя метода, "дополнение" );
  • Получить байты перед этим, которые представляют ссылку на объект, вызывающую метод;
  • Преобразуйте эти байты в Object;
  • Убедитесь, что Object является Artist; и,
  • Если это так, замените эти байты новым именем метода.

После того, как вы сделали этот второй трансформатор, вы можете изменить InterfaceFixAgent, чтобы разместить его. (Я также упростил вызов retransformClasses(), так как в приведенном выше примере мы выполняем необходимую проверку внутри самого трансформатора.)

InterfaceFixAgent.java (изменено)

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

А теперь... наша программа хороша. Конечно, было бы непросто закодировать, и это будет полный ад для QA и теста. Но это, безусловно, здорово, и это решает проблему. (Технически я полагаю, что это позволяет избежать проблемы, удалив ее, но... я возьму то, что смогу получить.)

Другие способы решения этой проблемы:

Оба из них позволят вам напрямую манипулировать байтами в памяти. Разумеется, решение может быть спроектировано вокруг них, но я считаю, что это будет намного сложнее и безопаснее. Поэтому я отправился с маршрутом выше.

Я думаю, что это решение можно было бы даже сделать более универсальным в невероятно полезную библиотеку для интеграции баз кода. Укажите, какой интерфейс и какой метод вам нужно реорганизовать в переменной, аргумент командной строки или файл конфигурации и освободить ее. Библиотека, которая примиряет конфликтующие интерфейсы в Java во время выполнения. (Конечно, я думаю, что все равно было бы лучше для всех, если бы они исправили ошибку на Java 8.)

Ответ 6

Вот что я сделал бы, чтобы устранить двусмысленность:

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(), чтобы нанять/уволить всю группу.

Если бы я передавал этот объект Театру для исполнения, я бы фактически дал ему значение, возвращенное из asArtist(), чтобы его рассматривали как талант.

Это работает до тех пор, пока вы контролируете прямое обращение к различным компонентам...

Но я понимаю, что ваша проблема - это один сторонний поставщик, создавший компонент TheaterManager, который ожидает один объект для обеих этих функций, и он не будет знать о методах asSet и asArtist, Проблема не в том, что производители, создавшие Set и Artist, это поставщик, который объединил их вместо использования шаблона посетителя или просто указав интерфейс, который будет отражать методы asSet и asArtist, которые я сделал выше, Если вы можете убедить своего поставщика "C" исправить этот интерфейс, ваш мир будет намного счастливее.

Удачи!

Ответ 7

Собака, у меня есть сильное чувство, что вы оставляете некоторые детали, которые имеют решающее значение для решения. Это часто происходит на SO, потому что

  • людям нужно оставить много деталей, чтобы получить вопрос с разумным размером и объемом,
  • люди не полностью понимают проблему и решение (именно поэтому они просят о помощи), поэтому они не могут быть уверены, какие детали важны, а какие нет, и
  • причина, по которой человек не может решить проблему самостоятельно, состоит в том, что они не понимают важности этой детали, что по той же причине они ее оставили.

Я сказал в другом ответе, что я буду делать с ArtistSet. Но, сохраняя это, я дам вам другое решение немного другой проблемы. Допустим, у меня был код от плохого продавца:

package com.bad;

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

Это плохо, потому что вы должны объявить функцию, возвращающую Comparator<IAlpha> для реализации стратегии сортировки, но что угодно. Теперь я получаю код от худшей компании:

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

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

Это хуже, потому что это совершенно неверно, поскольку оно переопределяет поведение несовместимо. ISybil упорядочивает себя по длине имени, но IAlpha упорядочивает себя по алфавиту, за исключением ISybil - IAlpha. Они были введены в заблуждение анти-образцом IAlpha, когда они могли и должны были сделать что-то вроде:

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

Однако, эта ситуация по-прежнему намного лучше, чем ArtistSet, потому что здесь ожидаемое поведение документировано. Нет никакой путаницы в отношении того, что должен делать ISybil.compareTo(). Поэтому я бы создал классы следующим образом. Класс Sybil, который реализует compareTo(), как com.worse, ожидает и делегирует все остальное:

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

}

и класс Alpha, который работает точно так же, как com.bad сказал, что он должен:

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

Обратите внимание, что я включил методы преобразования типов: Alpha.getSybil() и Sybil.getAlpha(). Это значит, что я могу создать свои собственные оболочки вокруг любых методов com.worse vendor, которые берут или возвращают Sybils, поэтому я могу избежать загрязнения моего кода или любого другого кода поставщика с помощью com.worse breakage. Так что если бы у com.worse были:

public ISybil breakage(ISybil broken);

Я мог бы написать функцию

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

и покончить с этим, за исключением того, что я все равно буду жаловаться громко, чтобы согласиться и вежливо согласиться на com.bad.