Внедрение методов с использованием стандартных методов интерфейсов - противоречивые?

Вступление

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

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

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

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

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

Сочетание этих:

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

Мой экзамен

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

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. Использование нескольких интерфейсов (потому что вы можете реализовать более одного) может быть обходным путем, к сожалению, снова понадобится метод по умолчанию, который является именно тем видом реализации, который я пытался избежать изначально. Ссылка - проблема множественного наследования с возможным решением

Ответ 1

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

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

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

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

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

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

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

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

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

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

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