Реализация методов с использованием методов интерфейсов по умолчанию - противоречиво? - PullRequest
0 голосов
/ 03 сентября 2018

Введение

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

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

Пример моего экзамена:

Обзор классов:

  • Предмет - Абстрактный суперкласс всех предметов
  • Вода - Предмет, который потребляется
  • Камень - Предмет, который не является расходуемым
  • Расходуемые - интерфейс с некоторыми методами для расходных материалов (эти методы должны быть переопределены всеми реализующими классами)

Объединение этих:

Вода является Предметом и реализует Расходуемый; Камень также является Предметом и не реализует Расходуемый.

Мой экзамен

Я хотел бы реализовать метод, который должны реализовывать все Предметы. Поэтому я объявляю подпись в классе Item.

protected abstract boolean isConsumable(); 
//return true if class implements (or rather "is consumable") Consumable and false in case it does not

Быстрое редактирование: я знаю, что instanceof может решить этот конкретный пример - если это возможно, подумайте о более сложном примере, который в первую очередь требует реализации метода. (Спасибо Sp00m и Евгению)

Теперь у меня есть несколько вариантов:

  1. Реализуйте метод вручную в каждом подклассе Item (это определенно невозможно при масштабировании приложения).

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

  1. Реализация метода внутри интерфейса в качестве метода по умолчанию, чтобы классы Consumable уже реализовали метод, который требуется для элемента суперкласса.

Это решение, рекомендованное в другом посте - я вижу преимущества от его реализации следующим образом:

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

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

  1. Введение подклассов вместо интерфейса - например, класса Consumable в качестве абстрактного подкласса Item и суперкласса Water.

Он предлагает возможность записать регистр по умолчанию для метода в Item (пример: isConsumable() //return false) и впоследствии переопределить его в подклассе. Проблема, которая возникает здесь: при масштабировании приложения и введении большего количества подклассов (как класса Consumable) фактические Предметы начнут расширяться более чем на один подкласс. Это может быть не плохо, потому что необходимо делать то же самое с интерфейсами, но это усложняет дерево наследования. Пример: элемент теперь может расширять подкласс ALayer2, который является подклассом ALayer1, который расширяет Item (layer0) .

  1. Представление другого суперкласса (то есть того же слоя, что и Item) - например, класса Consumable в качестве абстрактного класса, который будет еще одним суперклассом Water. Это означает, что вода должна расширять Предмет и Расходник

Эта опция предлагает гибкость. Можно создать целое новое дерево наследования для нового суперкласса, при этом все еще имея возможность видеть фактическое наследование Item. Но недостатком, который я обнаружил, является реализация этой структуры в реальных классах и использование их позже - Пример: как бы я мог сказать: Расходуемый - это Предмет, когда Расходуемый сможет иметь подклассы, которые не предназначены для Предметов , Весь процесс преобразования, возможно, вызовет головную боль - более вероятно, чем структура варианта 3.

Вопрос

Что было бы правильным вариантом для реализации этой структуры?

  • Это одна из моих перечисленных опций?
  • Это их вариация?
  • Или это еще один вариант, о котором я еще не думал?

Я выбрал очень простой пример - при ответе имейте в виду масштабируемость для будущих реализаций. Спасибо за любую помощь заранее.

Редактировать # 1

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

1 Ответ

0 голосов
/ 03 сентября 2018

Мне не хватает варианта 5 (или, возможно, я не правильно прочитал): поставьте метод внутри самого Item.

Если предположить, что расходные материалы можно идентифицировать с помощью интерфейса Consumable, то вот причины, по которым я не могу рекомендовать большинство из перечисленных вами пунктов: Первый (то есть реализовать его в каждом подклассе) слишком велик для чего-то такого простого, как this instanceof Consumable. Второй может быть в порядке, но не будет моим первым выбором. Третий и четвертый я не могу рекомендовать вообще. Если я могу дать только один совет, то, возможно, стоит дважды подумать о наследовании и никогда не использовать промежуточные классы только потому, что в определенный момент они облегчили вашу жизнь. Возможно, это повредит вам в будущем, когда ваша иерархия классов станет более сложной (примечание: я не говорю, что вы вообще не должны использовать промежуточные классы; -)).

Так что бы я сделал для этого конкретного случая? Я предпочел бы реализовать что-то вроде следующего в абстрактном Item классе:

public final boolean isConsumable() {
  return this instanceof Consumable;
}

Но, может быть, я бы даже не поставил такой метод, поскольку он так же хорош, как вначале написать item instanceof Consumable.

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

Относительно вашего редактирования: "Java не допускает множественное наследование" ... ну, с миксинами возможно нечто похожее на множественное наследование. Вы можете реализовать множество интерфейсов, а сами интерфейсы могут расширять и многие другие. При использовании методов по умолчанию у вас есть что-то, что можно использовать повторно: -)

Итак, почему default методы в интерфейсах могут использоваться (или не противоречат самому определению интерфейса):

  • для предоставления простой или наивной реализации, которая уже удовлетворяет большинству случаев использования (где реализующие классы могут предоставлять специфические, специализированные и / или оптимизированные функциональные возможности)
  • когда из всех параметров и контекста ясно, что должен делать метод (а подходящего абстрактного класса нет)
  • для шаблонных методов, т. Е. Когда они вызывают вызовы абстрактных методов для выполнения какой-либо работы, область которой шире. Типичным примером будет Iterable.forEach, который использует абстрактный метод iterator() из Iterable и применяет предоставленное действие к каждому из его элементов.

Спасибо Федерико Перальта Шаффнер за предложения.

Обратная совместимость также указана здесь для полноты, но перечислена отдельно, как и функциональные интерфейсы: Реализация по умолчанию также помогает не нарушать существующий код при добавлении новых функций (либо просто генерируя исключение, чтобы код продолжал компилироваться, либо предоставляя соответствующую реализацию, которая работает для всех реализующих классов). Для функциональных интерфейсов, что является довольно частным случаем интерфейса, методы по умолчанию довольно важны. Функциональные интерфейсы могут быть легко расширены с помощью функциональности, которая сама по себе не требует какой-либо конкретной реализации. Просто рассмотрите Predicate в качестве примера .. вы предоставляете test, но вы также получаете negate, or и and в дополнение (предоставляются как методы по умолчанию). Многие функциональные интерфейсы предоставляют дополнительные контекстные функции с помощью методов по умолчанию.

...