У меня есть базовый класс Base
. У него есть два подкласса, Sub1
и Sub2
. Каждый подкласс имеет несколько дополнительных методов. Например, Sub1
имеет Sandwich makeASandwich(Ingredients... ingredients)
и Sub2
имеет boolean contactAliens(Frequency onFrequency)
.
Поскольку эти методы принимают разные параметры и делают совершенно разные вещи, они абсолютно несовместимы, и я не могу просто использовать полиморфизм для решения этой проблемы.
Base
обеспечивает большую часть функциональности, и у меня есть большая коллекция Base
объектов. Тем не менее, все Base
объекты либо a, Sub1
либо a Sub2
, и иногда мне нужно знать, какие они есть.
Кажется плохой идеей сделать следующее:
for (Base base : bases) {
if (base instanceof Sub1) {
((Sub1) base).makeASandwich(getRandomIngredients());
// ... etc.
} else { // must be Sub2
((Sub2) base).contactAliens(getFrequency());
// ... etc.
}
}
Поэтому я разработал стратегию, чтобы избежать этого без кастинга. Base
Теперь есть эти методы:
boolean isSub1();
Sub1 asSub1();
Sub2 asSub2();
И, конечно же, Sub1
реализует эти методы как
boolean isSub1() { return true; }
Sub1 asSub1(); { return this; }
Sub2 asSub2(); { throw new IllegalStateException(); }
И Sub2
реализует их противоположным образом.
К сожалению, сейчас Sub1
и Sub2
есть эти методы в собственном API. Так что я могу сделать это, например, на Sub1
.
/** no need to use this if object is known to be Sub1 */
@Deprecated
boolean isSub1() { return true; }
/** no need to use this if object is known to be Sub1 */
@Deprecated
Sub1 asSub1(); { return this; }
/** no need to use this if object is known to be Sub1 */
@Deprecated
Sub2 asSub2(); { throw new IllegalStateException(); }
Таким образом, если известно, что объект является только a Base
, эти методы не являются устаревшими и могут использоваться для «приведения» себя к другому типу, чтобы я мог вызывать методы подкласса для него. В некотором смысле это кажется мне элегантным, но, с другой стороны, я немного злоупотребляю устаревшими аннотациями как способом «удалить» методы из класса.
Поскольку Sub1
экземпляр действительно является Base, имеет смысл использовать наследование, а не инкапсуляцию. Что я делаю хорошо? Есть ли лучший способ решить эту проблему?
источник
instanceof
, таким образом, требует много печатания, подвержено ошибкам и затрудняет добавление дополнительных подклассов.Sub1
иSub2
не могут быть использованы взаимозаменяемо, то почему вы относитесь к ним как таковой? Почему бы не отслеживать ваши «сэндвичницы» и «инопланетян» по отдельности?Ответы:
Не всегда имеет смысл добавлять функции в базовый класс, как предложено в некоторых других ответах. Добавление слишком большого количества функций специального случая может привести к связыванию во всех отношениях не связанных между собой компонентов.
Например , я мог бы иметь
Animal
класс сCat
иDog
компонентами. Если я хочу иметь возможность распечатать их или показать их в графическом интерфейсе, это может быть излишним для меня добавитьrenderToGUI(...)
иsendToPrinter(...)
в базовый класс.Подход, который вы используете, используя проверки типов и приведение типов, хрупок, но, по крайней мере, разделяет проблемы.
Однако, если вы обнаружите, что часто делаете эти типы проверок / приведений, то один из вариантов - реализовать для них шаблон посетителя / двойной отправки. Это выглядит примерно так:
Теперь ваш код становится
Основное преимущество заключается в том, что если вы добавите подкласс, вы получите ошибки компиляции, а не пропущенные по умолчанию случаи. Новый класс посетителя также становится хорошей целью для добавления функций в. Например, имеет смысл перейти
getRandomIngredients()
вActOnBase
.Вы также можете извлечь циклическую логику: например, приведенный выше фрагмент может стать
Немного дальнейшего массажа и использования лямбд и потоковой передачи в Java 8 позволит вам
Какая IMO выглядит очень аккуратно и лаконично, насколько это возможно.
Вот более полный пример Java 8:
источник
Sub3
. У посетителя нет такой же проблемы, какinstanceof
? Теперь вам нужно добавитьonSub3
к посетителю, и он сломается, если вы забудете.onSub3
в интерфейс посетителя вы получаете ошибку компиляции в каждом месте, где вы создаете нового посетителя, который еще не был обновлен. Напротив, переключение типа в лучшем случае приведет к ошибке времени выполнения, что может быть сложнее для запуска.С моей точки зрения: ваш дизайн не так .
В переводе на естественный язык вы говорите следующее:
Учитывая, что есть
animals
, естьcats
иfish
.animals
имеют свойства, которые являются общими дляcats
иfish
. Но этого недостаточно: есть некоторые свойства, которые отличаютсяcat
от другихfish
, поэтому вам необходимо создать подкласс.Теперь у вас есть проблема, что вы забыли смоделировать движение . Хорошо. Это относительно просто:
Но это неправильный дизайн. Правильный путь будет:
Где
move
было бы общее поведение, реализуемое по-разному для каждого животного.Это значит: ваш дизайн сломан.
Моя рекомендация: рефакторинг
Base
,Sub1
иSub2
.источник
.move()
или.attack()
и правильно абстрактнойthat
части - вместо того.swim()
,.fly()
,.slide()
,.hop()
,.jump()
,.squirm()
,.phone_home()
и т.д. Как вы рефакторинг правильно? Неизвестно без лучших примеров ... но все же, как правило, правильный ответ - если пример кода и более подробная информация не предлагают иное.move
vsfly
/run
/ etc это можно объяснить одним предложением: вы должны указывать объекту, что делать, а не как это делать. С точки зрения средств доступа, как вы упоминаете, это больше похоже на реальный случай в комментарии здесь, вы должны задавать вопросы объекта, а не проверять его состояние.Немного сложно представить себе ситуацию, когда у вас есть группа вещей, и вы хотите, чтобы они либо делали бутерброд, либо связывались с инопланетянами. В большинстве случаев, когда вы найдете такое приведение, вы будете работать с одним типом - например, в clang вы фильтруете набор узлов для объявлений, где getAsFunction возвращает ненулевое значение, вместо того, чтобы делать что-то другое для каждого узла в списке.
Возможно, вам нужно выполнить последовательность действий, и на самом деле не имеет значения, что объекты, выполняющие действие, связаны между собой.
Поэтому вместо списка
Base
работаем над списком действийгде
Вы можете, при необходимости, заставить Base реализовать метод для возврата действия или любым другим способом, который вам необходим для выбора действия из экземпляров в вашем базовом списке (поскольку вы говорите, что не можете использовать полиморфизм, поэтому, вероятно, действие, которое нужно предпринять это не функция класса, а какое-то другое свойство баз, иначе вы бы просто дали Base метод act (Context))
источник
Context
объекта только ухудшает ситуацию. С таким же успехом я мог бы просто передаватьObject
s в методы, приводить их и надеяться, что они правильного типа.Context
не должен быть новый класс или даже интерфейс. Обычно контекст является просто вызывающей стороной, поэтому вызовы выполняются в формеaction.act(this)
. ЕслиgetFrequency()
иgetRandomIngredients()
являются статическими методами, вам может даже не понадобиться контекст. Вот как я бы реализовал, скажем, очередь на работу, которая очень похожа на вашу проблему.Как насчет того, если ваши подклассы реализуют один или несколько интерфейсов, которые определяют, что они могут делать? Что-то вроде этого:
Тогда ваши занятия будут выглядеть так:
И ваш цикл станет:
Два основных преимущества этого подхода:
PS: извините за названия интерфейсов, я не мог придумать ничего более крутого в этот конкретный момент: D.
источник
Подход может быть хорошим в тех случаях, когда почти любой тип в семействе будет либо непосредственно использоваться в качестве реализации некоторого интерфейса, который соответствует некоторому критерию, либо может использоваться для создания реализации этого интерфейса. Встроенные типы коллекций, по-моему, выиграли бы от этого паттерна, но, поскольку для примера их нет, я придумаю интерфейс коллекции
BunchOfThings<T>
.Некоторые реализации
BunchOfThings
являются изменяемыми; некоторые нет. Во многих случаях объект Фред может захотеть держать что-то, что он может использовать как,BunchOfThings
и знает, что ничто, кроме Фреда, не сможет его изменить. Это требование может быть выполнено двумя способами:Фред знает, что он содержит единственные ссылки на это
BunchOfThings
, и никакой внешней ссылки на то, чтоBunchOfThings
его внутренние органы не существуют нигде во вселенной. Если никто не имеет ссылки на егоBunchOfThings
или его внутреннее устройство, никто больше не сможет изменить его, поэтому ограничение будет выполнено.Ни то
BunchOfThings
, ни какое-либо из его внутренних органов, на которые существуют внешние ссылки, не могут быть изменены каким-либо образом. Если абсолютно никто не может изменить aBunchOfThings
, то ограничение будет выполнено.Одним из способов удовлетворения ограничения является безоговорочное копирование любого полученного объекта (рекурсивная обработка любых вложенных компонентов). Другой - проверить, обещает ли полученный объект неизменность, а если нет, сделать его копию, а также сделать это с любыми вложенными компонентами. Альтернатива, которая склонна быть чище второго и быстрее первого, состоит в том, чтобы предложить
AsImmutable
метод, который просит объект сделать неизменную копию самого себя (используяAsImmutable
любые вложенные компоненты, которые его поддерживают).Также могут быть предусмотрены связанные методы
asDetached
(для использования, когда код получает объект и не знает, захочет ли он его изменить, и в этом случае изменяемый объект должен быть заменен новым изменяемым объектом, но неизменяемый объект можно сохранить как есть),asMutable
(в тех случаях, когда объект знает, что он будет содержать объект, возвращенный ранееasDetached
, т. е. либо неразделенную ссылку на изменяемый объект, либо разделяемую ссылку на изменяемый объект), иasNewMutable
(в случаях, когда код получает внешний объект и знает, что он захочет изменить копию данных в них - если входящие данные являются изменяемыми, нет оснований начинать с создания неизменяемой копии, которая будет немедленно использована для создания изменяемой копии, а затем заброшена).Обратите внимание, что хотя
asXX
методы могут возвращать немного разные типы, их реальная роль заключается в том, чтобы гарантировать, что возвращаемые объекты будут удовлетворять потребностям программы.источник
Игнорируя вопрос о том, у вас хороший дизайн или нет, и предположив, что он хороший или, по крайней мере, приемлемый, я бы хотел рассмотреть возможности подклассов, а не их тип.
Поэтому либо:
Переместите некоторые знания о существовании бутербродов и пришельцев в базовый класс, даже если вы знаете, что некоторые экземпляры базового класса не могут этого сделать. Реализуйте его в базовом классе, чтобы генерировать исключения, и измените код на:
Затем один или оба подкласса переопределяют
canMakeASandwich()
, и только один реализует каждый изmakeASandwich()
иcontactAliens()
.Используйте интерфейсы, а не конкретные подклассы, чтобы определить, какие возможности имеет тип. Оставьте базовый класс в покое и измените код на:
или, возможно (и не стесняйтесь игнорировать эту опцию, если она не соответствует вашему стилю, или в этом отношении любой стиль Java, который вы считаете разумным):
Лично мне обычно не нравится последний стиль отлова ожидаемого исключения из-за риска ненадлежащего отлова
ClassCastException
исходаgetRandomIngredients
илиmakeASandwich
, но YMMV.источник
<
состоит в том, чтобы избежать ловлиArrayIndexOutOfBoundsException
, и некоторые люди решают сделать это тоже. Без учета вкуса ;-) В сущности, моя «проблема» в том, что я работаю в Python, где обычно предпочитают отлавливать любое исключение, а не заранее проверять, возникнет ли исключение, независимо от того, насколько простым будет предварительный тест. , Возможно, такую возможность просто не стоит упоминать в Java.Здесь у нас есть интересный случай базового класса, который понижает себя до своих собственных производных классов. Мы хорошо знаем, что это обычно плохо, но если мы хотим сказать, что нашли вескую причину, давайте посмотрим и посмотрим, какие могут быть ограничения на это:
Если 4, то имеем: 5. Политика производного класса всегда находится под тем же политическим контролем, что и политика базового класса.
2 и 5 прямо подразумевают, что мы можем перечислить все производные классы, что означает, что не должно быть никаких внешних производных классов.
Но вот в чем дело. Если они все ваши, вы можете заменить if абстракцией, которая является вызовом виртуального метода (даже если это бессмысленный ответ), и избавиться от if и самостоятельного приведения. Поэтому не делай этого. Лучший дизайн доступен.
источник
Поместите абстрактный метод в базовый класс, который выполняет одно в Sub1, а другой - в Sub2, и вызовите этот абстрактный метод в ваших смешанных циклах.
Обратите внимание, что для этого могут потребоваться другие изменения в зависимости от того, как определены getRandomIngredients и getFrequency. Но, честно говоря, внутри класса, который связывается с инопланетянами, вероятно, лучшее место для определения getFrequency.
Кстати, ваши методы asSub1 и asSub2 - определенно плохая практика. Если вы собираетесь сделать это, сделайте это путем кастования. Нет ничего, что эти методы дают вам, что кастинг не дает.
источник
Вы можете вставить оба
makeASandwich
иcontactAliens
в базовый класс, а затем реализовать их с помощью фиктивных реализаций. Поскольку возвращаемые типы / аргументы не могут быть преобразованы в общий базовый класс.У такого рода вещей есть очевидные недостатки, например, что это означает для контракта метода и что вы можете и не можете сделать из контекста результата. Вы не можете думать, так как я не смог сделать бутерброд, ингредиенты использовались в попытке и т.д.
источник
То, что вы делаете, совершенно законно. Не обращайте внимания на скептиков, которые просто повторяют догму, потому что они читают ее в некоторых книгах. Догме нет места в технике.
Я использовал один и тот же механизм пару раз, и я могу с уверенностью сказать, что среда выполнения Java могла бы также сделать то же самое, по крайней мере, в одном месте, о котором я могу думать, улучшив таким образом производительность, удобство использования и читаемость кода, который использует это.
Возьмем, к примеру
java.lang.reflect.Member
, что является основойjava.lang.reflect.Field
иjava.lang.reflect.Method
. (Фактическая иерархия немного сложнее, чем эта, но это не имеет значения.) Поля и методы - это совершенно разные животные: у одного есть значение, которое вы можете получить или установить, в то время как у другого такого нет, но его можно вызвать с помощью ряд параметров, и он может возвращать значение. Итак, поля и методы являются членами, но то, что вы можете с ними сделать, примерно так же отличается друг от друга, как приготовление бутербродов и контакт с инопланетянами.Теперь, когда мы пишем код, использующий отражение, мы очень часто имеем
Member
в руках, и мы знаем, что это или a,Method
илиField
(или, редко, что-то еще), и все же мы должны сделать все утомительное,instanceof
чтобы точно выяснить что это такое, и затем мы должны использовать его, чтобы получить правильную ссылку на него. (И это не только утомительно, но и не очень хорошо работает.)Method
Класс мог бы очень легко реализовать описанный вами шаблон, упрощая тем самым жизнь тысячам программистов.Конечно, этот метод применим только в небольших, четко определенных иерархиях тесно связанных классов, которые вы имеете (и всегда будете иметь) на уровне источника: вы не хотите делать это, если ваша иерархия классов подлежит распространению людьми, которые не имеют права проводить рефакторинг базового класса.
Вот что отличается от того, что вы сделали:
Базовый класс обеспечивает реализацию по умолчанию для всего
asDerivedClass()
семейства методов, каждый из которых возвращаетnull
.Каждый производный класс переопределяет только один из
asDerivedClass()
методов, возвращаяthis
вместоnull
. Он не отменяет ничего из остальных и не хочет ничего о них знать. Таким образом, неIllegalStateException
выбрасываются.Базовый класс также предоставляет
final
реализации для всегоisDerivedClass()
семейства методов, закодированных следующим образом:return asDerivedClass() != null;
Таким образом, число методов, которые должны быть переопределены производными классами, сведено к минимуму.Я не использовал
@Deprecated
в этом механизме, потому что я не думал об этом. Теперь, когда вы дали мне идею, я буду использовать ее, спасибо!C # имеет связанный механизм, встроенный через использование
as
ключевого слова. В C # вы можете сказать,DerivedClass derivedInstance = baseInstance as DerivedClass
и вы получите ссылку на,DerivedClass
если выbaseInstance
принадлежали к этому классу, илиnull
если это не так. Это (теоретически) работает лучше, чемis
сопровождаемое приведением (is
по общему признанию ключевое слово C # дляinstanceof
), но пользовательский механизм, который мы создали вручную, работает даже лучше: пара операцийinstanceof
-and-cast в Java, а также посколькуas
оператор C # не выполняет так быстро, как единственный вызов виртуального метода нашего пользовательского подхода.Настоящим я выдвигаю предположение, что эту технику следует объявить шаблоном и найти для нее хорошее имя.
Ну и дела, спасибо за отрицательные голоса!
Краткое изложение противоречий, чтобы избавить вас от проблем с чтением комментариев:
Люди возражают против того, что первоначальный дизайн был неправильным, то есть вы никогда не должны иметь совершенно разные классы, производные от общего базового класса, или что даже если вы это сделаете, код, который использует такую иерархию, никогда не должен иметь базовая ссылка и необходимость выяснить производный класс. Поэтому, говорят они, механизм самолитья, предложенный этим вопросом и моим ответом, который улучшает использование оригинального дизайна, никогда не должен был быть необходим. (Они на самом деле ничего не говорят о самом механизме литья, они только жалуются на природу конструкций, к которым механизм должен применяться.)
Однако, в приведенном выше примере , я бы уже показал , что создатели Java Runtime в самом деле выбрать именно такой дизайн для
java.lang.reflect.Member
,Field
,Method
иерархии, и в комментариях ниже я также показываю , что создатели # выполнения C независимо пришли к эквивалентная конструкция дляSystem.Reflection.MemberInfo
,FieldInfo
,MethodInfo
иерархии. Итак, это два разных сценария реального мира, которые лежат прямо у каждого под носом и имеют наглядно работающие решения, использующие именно такие конструкции.Вот к чему сводятся все последующие комментарии. Механизм самолитья практически не упоминается.
источник
java.lang.reflection.Member
иерархии. Создатели java runtime попали в такую ситуацию, я не думаю, что вы сказали бы, что они спроектировали себя в коробку, или вините их за то, что они не следовали каким-то лучшим практикам? Итак, я хочу подчеркнуть, что, как только у вас возникнет ситуация такого типа, этот механизм станет хорошим способом улучшить ситуацию.Member
интерфейс, но он служит только для проверки отборочного доступа. Как правило, вы получаете список методов или свойств и используете их напрямую (без их смешивания, поэтому ничего неList<Member>
появляется), поэтому вам не нужно приводить. Обратите внимание, чтоClass
не предоставляетgetMembers()
метод. Конечно, невежественный программист может создатьList<Member>
, но это будет иметь такой же смысл, как созданиеList<Object>
и добавление некоторыхInteger
илиString
к ним.null
вместо исключения не делает его намного лучше. При прочих равных, посетитель безопаснее при выполнении той же работы.