Как не нарушить принцип подстановки Лисков с классом, реализующим несколько интерфейсов?

Учитывая следующий класс:

class Example implements Interface1, Interface2 {
    ...
}

Когда я создаю экземпляр класса, используя Interface1:

Interface1 example = new Example();

...тогда я могу вызывать только методы Interface1, а не методы Interface2, если только я не приведу:

((Interface2) example).someInterface2Method();

Конечно, чтобы сделать эту среду выполнения безопасной, я должен также обернуть это проверкой instanceof:

if (example instanceof Interface2) {
    ((Interface2) example).someInterface2Method();
}

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

Нарушает ли подход instanceof/cast LSP, когда я опрашиваю экземпляр среды выполнения, чтобы определить его реализации?

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


person jml    schedule 14.01.2019    source источник
comment
Я бы не стал добавлять чек. Если бы мне нужно было использовать оба интерфейса в одной области, я бы сделал время компиляции типа Example, а не Interface1.   -  person duffymo    schedule 14.01.2019
comment
Вам определенно не нужно бросать в этой ситуации. Это всегда должно быть последним средством. Вероятно, более 90% слепков — просто результат плохого дизайна.   -  person Michael    schedule 14.01.2019
comment
создайте третий интерфейс, который расширяет те 2, которые вы упомянули, и используйте первый во всем коде. или используйте дженерики, чтобы быть более снисходительными: public <T extends Interfac1 & Interface2> void doSomething(T t)   -  person Lino    schedule 14.01.2019
comment
Вы не можете использовать Example example = new Example();?   -  person Andy Turner    schedule 14.01.2019
comment
Учитывая, что приведения типов являются разумной функцией языка Java, мы не можем ответить на эти вопросы, не зная назначения класса и интерфейсов и того, почему вы хотите выполнять приведения типов.   -  person Matt Timmermans    schedule 14.01.2019
comment
Если вам нужно получить доступ к методам всех реализованных интерфейсов, для этого и нужен реализующий класс.   -  person Sarthak Mittal    schedule 15.01.2019
comment
Ceylon — это язык, подобный Java, который поддерживает первоклассные типы пересечения и объединения. На Цейлоне вы можете просто объявить переменную следующим образом: Interface1 & Interface2 example = new Example();. Зацените, на Цейлоне много интересного!   -  person Lii    schedule 29.01.2019


Ответы (8)


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

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

Если у вас есть веская причина для того, чтобы какой-то код требовал что-то, что является одновременно Interface1 и Interface2, тогда обязательно сделайте комбинированную версию, которая расширяет оба. Если вы изо всех сил пытаетесь придумать подходящее имя для этого (нет, не FooAndBar), то это показатель того, что ваш дизайн неверен.

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

Мой любимый и наиболее часто используемый шаблон дизайна — это шаблон декоратора. Таким образом, большинство моих классов будут когда-либо реализовывать только один интерфейс (за исключением более общих интерфейсов, таких как Comparable). Я бы сказал, что если ваши классы часто/всегда реализуют более одного интерфейса, то это запах кода.


Если вы создаете экземпляр объекта и используете его в той же области, вам следует просто написать

Example example = new Example();

Просто чтобы было ясно (я не уверен, что это то, что вы предлагали), ни при ни при каких обстоятельствах вы не должны никогда писать что-то вроде этого:

Interface1 example = new Example();
if (example instanceof Interface2) {
    ((Interface2) example).someInterface2Method();
}
person Michael    schedule 14.01.2019
comment
Это интересное замечание, которое вы делаете, когда говорите, что оно бесполезно по отдельности - Interface2 в этом примере фактически относится к интерфейсу, который я назвал HasParameters, который имеет методы getParams() и setParams(), поскольку не все реализации Interface1 на самом деле нужны для работы с параметрами. Я пытался не нарушать принцип разделения интерфейса, имея множество реализаций с пустыми методами getParams() и setParams(), но, как вы говорите, Interface2 сам по себе довольно бесполезен... - person jml; 14.01.2019
comment
Почему только некоторые реализации заботятся о параметрах? Это кажется странным. - person Michael; 14.01.2019
comment
@jml, тогда, возможно, спецификация Interface1 с указанными вами методами будет лучше (interface Interface2 extends Interface1 { /* get and set params */ } - person Lino; 14.01.2019
comment
@Michael - некоторым реализациям необходимо предоставить больше параметров контекста для выполнения операции из Interface1 ... Так что я мог бы просто добавить параметр params в интерфейс, но не подумал, что было бы хорошо иметь много вызовов с ноль для этого параметра - person jml; 14.01.2019
comment
@jml Тогда то, что предлагает Лино, вероятно, то, что вы хотите. Request и ParametizedRequest лучше, чем Request и HasParameters. - person Michael; 14.01.2019
comment
@Michael, моя причина не использовать реализацию, т. Е. «Пример примера = новый пример ()», заключается в том, что я хочу запрограммировать интерфейс, а не реализацию, и я создаю свои экземпляры через класс Factory - person jml; 14.01.2019
comment
Я рассмотрю подход Request и ParameterizedRequest. Большое спасибо за ваши ответы - person jml; 14.01.2019
comment
@jml HasParameters выглядит не как интерфейс, а как атрибут. Возможно, на самом деле вам нужно добавить методы к Request, такие как hasParameters(), getParameters() и setParameters(). С Java 8 вы даже можете иметь эти default до false, emptyList() и throw OperationNotSupportedException соответственно, если вы не хотите реализовывать их все время. Классы Collection в Java делают это постоянно. - person jbx; 14.01.2019
comment
С Java 8 вы можете включить свои getParams() и setParams() в Interface1 с реализациями по умолчанию, такими как public default void setParams(Params p) {} и public default Params getParams() { return null; }. - person Stephen P; 14.01.2019

Ваш класс может прекрасно реализовать несколько интерфейсов и не нарушает никаких принципов ООП. Напротив, он следует принципу разделения интерфейса.

Это сбивает с толку, почему у вас может возникнуть ситуация, когда ожидается, что что-то типа Interface1 предоставит someInterface2Method(). Вот где ваша конструкция неверна.

Подумайте об этом немного по-другому: представьте, что у вас есть другой метод, void method1(Interface1 interface1). Нельзя ожидать, что interface1 также будет экземпляром Interface2. Если бы это было так, тип аргумента должен был бы быть другим. Пример, который вы показали, именно таков, имея переменную типа Interface1, но ожидая, что она также будет иметь тип Interface2.

Если вы хотите иметь возможность вызывать оба метода, тип вашей переменной example должен быть установлен на Example. Таким образом, вы полностью избегаете instanceof и приведения типов.

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

В целом (хотя и не всегда) instanceof проверки и приведения типов часто указывают на некоторые недостатки проектирования ООП. Иногда дизайн подходил для остальной части программы, но у вас был бы небольшой случай, когда проще ввести приведение, чем реорганизовать все. Но, если возможно, вы всегда должны стремиться избегать этого сначала, как часть вашего дизайна.

person jbx    schedule 14.01.2019

У вас есть два разных варианта (держу пари, их гораздо больше).

Первый — создать свой собственный interface, который расширяет два других:

interface Interface3 extends Interface1, Interface2 {}

А затем используйте это во всем коде:

public void doSomething(Interface3 interface3){
    ...
}

Другой способ (и, на мой взгляд, лучший) - использовать дженерики для каждого метода:

public <T extends Interface1 & Interface2> void doSomething(T t){
    ...
}

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

person Lino    schedule 14.01.2019
comment
А затем используйте это во всем коде. Существенным недостатком этого является то, что вы должны заставить Example (и любые другие классы) реализовать этот класс. - person Andy Turner; 14.01.2019
comment
@ Энди, я согласен, и поэтому второй подход более гибкий и, вероятно, может быть предпочтительнее. - person Lino; 14.01.2019
comment
Сначала стоит указать предпочтительный вариант; или, по крайней мере, заявив Мой предпочтительный способ или что-то подобное, что является гораздо более сильным одобрением, чем наоборот. - person Andy Turner; 14.01.2019
comment
Пока вы предоставляете правильные технические решения проблемы, будьте осторожны с внедрением обходных путей, когда основная проблема связана с архитектурой. В конце концов вы можете столкнуться с еще большим количеством проблем, связанных с неправильным дизайном. - person Vincent Savard; 14.01.2019

Основная проблема

Немного подправив ваш пример, чтобы я мог решить основную проблему:

public void DoTheThing(Interface1 example)
{
    if (example instanceof Interface2) 
    {
        ((Interface2) example).someInterface2Method();
    }
}

Итак, вы определили метод DoTheThing(Interface1 example). Это в основном говорит: «Чтобы сделать это, мне нужен объект Interface1».

Но затем в теле вашего метода оказывается, что вам действительно нужен объект Interface2. Тогда почему вы не запросили его в параметрах метода? Совершенно очевидно, что вы должны были попросить Interface2

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

Нет неотъемлемого требования, согласно которому Interface1 и Interface2 должны быть реализованы на одном и том же объекте. Вы не можете знать (и не полагаться на предположение), что это так.

Если вы не определите неотъемлемое требование и не примените его.

interface InterfaceBoth extends Interface1, Interface2 {}

public void DoTheThing(InterfaceBoth example)
{
    example.someInterface2Method();
}

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

Вы (и компилятор) знаете, что этот метод всегда будет доступен, и нет никаких шансов, что он не сработает.

Примечание. Вы могли бы использовать Example вместо создания интерфейса InterfaceBoth, но тогда вы сможете использовать только объекты типа Example, а не любой другой класс, реализующий оба интерфейса. Я предполагаю, что вы заинтересованы в обработке любого класса, который реализует оба интерфейса, а не только Example.

Дальнейшее рассмотрение проблемы.

Посмотрите на этот код:

ICarrot myObject = new Superman();

Если вы предполагаете, что этот код компилируется, что вы можете сказать мне о классе Superman? Что он явно реализует интерфейс ICarrot. Это все, что вы можете мне сказать. Вы понятия не имеете, реализует ли Superman интерфейс IShovel или нет.

Итак, если я попытаюсь сделать это:

myObject.SomeMethodThatIsFromSupermanButNotFromICarrot();

или это:

myObject.SomeMethodThatIsFromIShovelButNotFromICarrot();

Стоит ли вам удивляться, если я скажу вам, что этот код компилируется? Вы должны это сделать, потому что этот код не компилируется.

Вы можете сказать: «Но я знаю, что это объект Superman, у которого есть этот метод!». Но тогда вы забудете, что вы только сказали компилятору, что это переменная ICarrot, а не переменная Superman.

Вы можете сказать: «Но я знаю, что это объект Superman, который реализует интерфейс IShovel!». Но тогда вы забудете, что вы сказали компилятору только переменную ICarrot, а не переменную Superman или IShovel.

Зная это, давайте вернемся к вашему коду.

Interface1 example = new Example();

Все, что вы сказали, это то, что у вас есть переменная Interface1.

if (example instanceof Interface2) {
    ((Interface2) example).someInterface2Method();
}

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

Вы можете сказать: «Но я знаю, что помещаю объект Example, компилятор тоже должен это знать!» но вы бы упустили тот момент, что если бы это был параметр метода, у вас не было бы возможности узнать, что отправляют вызывающие объекты вашего метода.

public void DoTheThing(Interface1 example)
{
    if (example instanceof Interface2) 
    {
        ((Interface2) example).someInterface2Method();
    }
}

Когда другие вызывающие объекты вызывают этот метод, компилятор останавливает их только в том случае, если переданный объект не реализует Interface1. Компилятор не собирается останавливать кого-либо от передачи объекта класса, который реализует Interface1, но не реализует Interface2.

person Flater    schedule 15.01.2019
comment
Первые ~ 50% вашего ответа очень многословно объясняют то, что ОП уже понимает. Я знаю, что у меня может быть интерфейс-оболочка, который расширяет оба интерфейса. - person Michael; 15.01.2019
comment
@Michael: Это относится только к небольшому, если вы не определите ... абзац, а не основную проблему. Этот второй абзац добавлен не только для того, чтобы предложить решение, но и для того, чтобы объяснить, как он идеологически отличается от исходной ситуации OP, чтобы дополнительно прояснить, почему неявные ожидания OP не признаются компилятором. Понимание того, что вы можете это сделать (это то, что знает ОП, вы правы в этом), не равнозначно пониманию того, почему вы должны это делать и почему компилятор ожидает от вас этого и отказывается принять иное код. - person Flater; 15.01.2019

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

Реализация 2 (или более) интерфейсов в классе — это нормально. Решение о том, какой интерфейс использовать в качестве типа данных, полностью зависит от контекста кода, который будет его использовать.

Кастинг — это хорошо, особенно при смене контекста.

class Payment implements Expirable, Limited {
 /* ... */
}

class PaymentProcessor {
    // Using payment here because i'm working with payments.
    public void process(Payment payment) {
        boolean expired = expirationChecker.check(payment);
        boolean pastLimit = limitChecker.check(payment);

        if (!expired && !pastLimit) {
          acceptPayment(payment);
        }
    }
}

class ExpirationChecker {
    // This the `Expirable` world, so i'm  using Expirable here
    public boolean check(Expirable expirable) {
        // code
    }
}

class LimitChecker {
    // This class is about checking limits, thats why im using `Limited` here
    public boolean check(Limited limited) {
        // code
    }
}
person sweet suman    schedule 14.01.2019
comment
ЛСВ? Вы имеете в виду ЛСП? - person Peter Mortensen; 15.01.2019
comment
Это, конечно, опечатка, извините за это. - person sweet suman; 15.01.2019

Как правило, многие интерфейсы, специфичные для клиента, подходят и являются частью принципа разделения интерфейса ( «Я» в SOLID). Некоторые более конкретные моменты на техническом уровне уже упоминались в других ответах.

В частности, вы можете зайти слишком далеко с этой сегрегацией, создав такой класс, как

class Person implements FirstNameProvider, LastNameProvider, AgeProvider ... {
    @Override String getFirstName() {...}
    @Override String getLastName() {...}
    @Override int getAge() {...}
    ...
}

Или, наоборот, что у вас слишком мощный класс реализации, как в

class Application implements DatabaseReader, DataProcessor, UserInteraction, Visualizer {
    ...
}

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

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

Что касается вашего примера: если всем вашим клиентам всегда нужны функции Interface1 и Interface2 одновременно, вам следует рассмотреть возможность определения

interface Combined extends Interface1, Interface2 { }

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

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

class Example {
    Interface1 getInterface1() { ... }
    Interface2 getInterface2() { ... }
}

Это выглядит немного странно в этом Example (sic!), но в зависимости от сложности реализации Interface1 и Interface2 действительно может иметь смысл разделить их.


Отредактировано в ответ на комментарий:

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

interface DatabaseReader { String read(); }
interface DatabaseWriter { void write(String s); }

class Database {
    DatabaseConnection connection = create();
    DatabaseReader reader = createReader(connection);
    DatabaseReader writer = createWriter(connection);

    DatabaseReader getReader() { return reader; }
    DatabaseReader getWriter() { return writer; }
}

Клиент по-прежнему будет полагаться на интерфейсы. Такие методы, как

void create(DatabaseWriter writer) { ... }
void read  (DatabaseReader reader) { ... }
void update(DatabaseReader reader, DatabaseWriter writer) { ... }

затем можно было бы вызвать с помощью

create(database.getWriter());
read  (database.getReader());
update(database.getReader(), database.getWriter());

соответственно.

person Marco13    schedule 14.01.2019
comment
Это правильный ответ. Интерфейс построен для контракта, который необходимо выполнить клиентскому коду. Если клиентский код разумно предполагает возможность вызова метода someInterface1Method И someInterface2Method, то это новый контракт. Из википедии о интернет-провайдере: ни один клиент не должен зависеть от методов, которые он не использует. и клиенты должны будут знать только те методы, которые их интересуют. - person Xtros; 15.01.2019
comment
Я не уверен, что согласен с последним пунктом. Если вы решили, что концептуально не можете создать такой интерфейс, как Combined, потому что два интерфейса не связаны между собой, то объединение двух объектов в один композиционный объект ничем не лучше. Если они концептуально не связаны, потребляющий метод должен принимать только два параметра. - person Michael; 15.01.2019
comment
@Michael Я колебался с последним пунктом, потому что это может показаться странным или вызвать недоразумения. Для ясности: понимаете ли вы, что Example-объект должен быть передан потребляющему методу, которому нужны оба интерфейса? Я имел в виду не это. (Если бы это можно было понять таким образом, я бы попытался сделать это яснее...) - person Marco13; 16.01.2019
comment
Да, что-то в этом роде. Для чего еще вы бы его использовали? - person Michael; 16.01.2019
comment
@Michael Майкл, я добавил «редактировать». Если вы думаете, что это слишком надуманно или надумано, я бы предпочел пропустить (изначально) последний абзац и «редактировать»… - person Marco13; 17.01.2019

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

Ниже показаны итеративные изменения решения для соответствия принципам SOLID.

Требование

Чтобы создать ответ для веб-службы, к объекту ответа добавляются пары ключ + объект. Необходимо добавить множество различных пар ключ + объект, каждая из которых может иметь уникальную обработку, необходимую для преобразования данных из источника в формат, требуемый в ответе.

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

Таким образом, в итерации решения 1 был создан следующий интерфейс:

Итерация решения 1

ResponseObjectProvider<T, S> {
    void addObject(T targetObject, S sourceObject, String targetKey);
}

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

Это здорово, поскольку у нас есть общий интерфейс, который действует как контракт для этой распространенной практики добавления объектов ответа.

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

Здесь есть варианты, первый — добавить реализацию существующего интерфейса следующим образом:

public class GetIdentifierResponseObjectProvider<T extends Map, S extends Map> implements ResponseObjectProvider<T, S> {
  public void addObject(final T targetObject, final S sourceObject, final String targetKey) {
     targetObject.put(targetKey, sourceObject.get("identifier"));
  }
}

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

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

Итерация решения 2

ResponseObjectProvider<T, S, U> {
    void addObject(T targetObject, S sourceObject, String targetKey);
    void setParams(U params);
    U getParams();
}

Этот интерфейс подходит для обоих сценариев использования; реализации, требующие дополнительных параметров для выполнения операции addObject, и реализации, не требующие

Однако, учитывая последний из сценариев использования, реализации, не требующие дополнительных параметров, нарушат принцип разделения интерфейса SOLID, поскольку эти реализации переопределяют методы getParams и setParams, но не реализуют их. например:

public class GetObjectBySourceKeyResponseObjectProvider<T extends Map, S extends Map, U extends String> implements ResponseObjectProvider<T, S, U> {
    public void addObject(final T targetObject, final S sourceObject, final String targetKey) {
        targetObject.put(targetKey, sourceObject.get(U));
    }

    public void setParams(U params) {
        //unimplemented method
    }

    U getParams() {
        //unimplemented method
    }

}

Итерация решения 3

Чтобы исправить проблему разделения интерфейса, методы интерфейса getParams и setParams были перемещены в новый интерфейс:

public interface ParametersProvider<T> {
    void setParams(T params);
    T getParams();
}

Реализации, которым требуются параметры, теперь могут реализовывать интерфейс ParametersProvider:

public class GetObjectBySourceKeyResponseObjectProvider<T extends Map, S extends Map, U extends String> implements ResponseObjectProvider<T, S>, ParametersProvider<U>

  private String params;
  public void setParams(U params) {
      this.params = params;
  }

  public U getParams() {
    return this.params;
  }

  public void addObject(final T targetObject, final S sourceObject, final String targetKey) {
     targetObject.put(targetKey, sourceObject.get(params));
  }
}

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

ResponseObjectProvider responseObjectProvider = new  GetObjectBySourceKeyResponseObjectProvider<>();

Тогда для экземпляра будет доступен метод addObject, но НЕ методы getParams и setParams интерфейса ParametersProvider... Для их вызова требуется приведение, и для безопасности также должна быть выполнена проверка instanceof:

if(responseObjectProvider instanceof ParametersProvider) {
      ((ParametersProvider)responseObjectProvider).setParams("identifier");
}

Это не только нежелательно, но и нарушает принцип подстановки Лискова: «если S является подтипом T, то объекты типа T в программе могут быть заменены объектами типа S без изменения каких-либо желаемых свойств этого объекта. программа"

т. е. если мы заменили реализацию ResponseObjectProvider, которая также реализует ParametersProvider, реализацией, которая не реализует ParametersProvider, то это могло бы изменить некоторые желаемые свойства программы... Кроме того, клиент должен знать, какая реализация находится в данный момент. использовать для вызова правильных методов

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

Итерация решения 4 – окончательное решение

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

Решение состоит в том, чтобы иметь два отдельных интерфейса, ParameterisedResponseObjectProvider и ResponseObjectProvider.

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

Новый интерфейс был впервые реализован как расширение ResponseObjectProvider:

public interface ParameterisedResponseObjectProvider<T,S,U> extends ResponseObjectProvider<T, S> {
    void setParams(U params);   
    U getParams();
}

Однако у этого все еще была проблема с использованием, когда вызывающему клиенту сначала нужно было вызвать setParams перед вызовом addObject, а также сделать код менее читаемым.

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

public interface ResponseObjectProvider<T, S> {
    void addObject(T targetObject, S sourceObject, String targetKey);   
}


public interface ParameterisedResponseObjectProvider<T,S,U> {
    void addObject(T targetObject, S sourceObject, String targetKey, U params);
}

Это решение устраняет нарушения принципов разделения интерфейса и замены Лисков, а также улучшает использование для вызова клиентов и улучшает читаемость кода.

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

person jml    schedule 16.01.2019

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

Рассмотрим, например, базовый интерфейс последовательности/перечисления и следующие варианты поведения:

  1. Создайте перечислитель, который может считывать объекты, если другой итератор еще не создан.

  2. Создайте перечислитель, который может считывать объекты, даже если другой итератор уже создан и используется.

  3. Сообщите, сколько элементов в последовательности

  4. Сообщите значение N-го элемента в последовательности

  5. Скопируйте диапазон элементов из объекта в массив этого типа.

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

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

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

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

person supercat    schedule 14.01.2019