Разве это нормально иметь объекты, которые приводятся сами, даже если это загрязняет API их подклассов?

33

У меня есть базовый класс 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, имеет смысл использовать наследование, а не инкапсуляцию. Что я делаю хорошо? Есть ли лучший способ решить эту проблему?

нарушитель закона
источник
12
Все 3 класса теперь должны знать друг о друге. Добавление Sub3 повлекло бы за собой множество изменений кода, а добавление Sub10 было бы совершенно болезненным
Дэн Пичельман,
15
Было бы очень полезно, если бы вы дали нам реальный код. Бывают ситуации, когда уместно принимать решения на основе определенного класса чего-либо, но невозможно сказать, оправданы ли вы тем, что делаете с такими надуманными примерами. Для чего бы это ни стоило, что вы хотите, это посетитель или помеченный союз .
Доваль
1
Извините, я думал, что будет проще привести упрощенные примеры. Может быть, я опубликую новый вопрос с более широкой областью того, что я хочу сделать.
код
12
Вы просто переопределяете приведение и instanceof, таким образом, требует много печатания, подвержено ошибкам и затрудняет добавление дополнительных подклассов.
user253751
5
Если Sub1и Sub2не могут быть использованы взаимозаменяемо, то почему вы относитесь к ним как таковой? Почему бы не отслеживать ваши «сэндвичницы» и «инопланетян» по отдельности?
Питер Витвоет

Ответы:

27

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

Например , я мог бы иметь Animalкласс с Catи Dogкомпонентами. Если я хочу иметь возможность распечатать их или показать их в графическом интерфейсе, это может быть излишним для меня добавить renderToGUI(...)и sendToPrinter(...)в базовый класс.

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

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

public abstract class Base {
  ...
  abstract void visit( BaseVisitor visitor );
}

public class Sub1 extends Base {
  ...
  void visit(BaseVisitor visitor) { visitor.onSub1(this); }
}

public class Sub2 extends Base {
  ...
  void visit(BaseVisitor visitor) { visitor.onSub2(this); }
}

public interface BaseVisitor {
   void onSub1(Sub1 that);
   void onSub2(Sub2 that);
}

Теперь ваш код становится

public class ActOnBase implements BaseVisitor {
    void onSub1(Sub1 that) {
       that.makeASandwich(getRandomIngredients())
    }

    void onSub2(Sub2 that) {
       that.contactAliens(getFrequency());
    }
}

BaseVisitor visitor = new ActOnBase();
for (Base base : bases) {
    base.visit(visitor);
}

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

Вы также можете извлечь циклическую логику: например, приведенный выше фрагмент может стать

BaseVisitor.applyToArray(bases, new ActOnBase() );

Немного дальнейшего массажа и использования лямбд и потоковой передачи в Java 8 позволит вам

bases.stream()
     .forEach( BaseVisitor.forEach(
       Sub1 that -> that.makeASandwich(getRandomIngredients()),
       Sub2 that -> that.contactAliens(getFrequency())
     ));

Какая IMO выглядит очень аккуратно и лаконично, насколько это возможно.

Вот более полный пример Java 8:

public static abstract class Base {
    abstract void visit( BaseVisitor visitor );
}

public static class Sub1 extends Base {
    void visit(BaseVisitor visitor) { visitor.onSub1(this); }

    void makeASandwich() {
        System.out.println("making a sandwich");
    }
}

public static class Sub2 extends Base {
    void visit(BaseVisitor visitor) { visitor.onSub2(this); }

    void contactAliens() {
        System.out.println("contacting aliens");
    }
}

public interface BaseVisitor {
    void onSub1(Sub1 that);
    void onSub2(Sub2 that);

    static Consumer<Base> forEach(Consumer<Sub1> sub1, Consumer<Sub2> sub2) {

        return base -> {
            BaseVisitor baseVisitor = new BaseVisitor() {

                @Override
                public void onSub1(Sub1 that) {
                    sub1.accept(that);
                }

                @Override
                public void onSub2(Sub2 that) {
                    sub2.accept(that);
                }
            };
            base.visit(baseVisitor);
        };
    }
}

Collection<Base> bases = Arrays.asList(new Sub1(), new Sub2());

bases.stream()
     .forEach(BaseVisitor.forEach(
             Sub1::makeASandwich,
             Sub2::contactAliens));
Майкл Андерсон
источник
+1: Подобные вещи обычно используются в языках, которые имеют первоклассную поддержку для типов сумм и сопоставления с образцом, и это допустимый дизайн в Java, хотя синтаксически он очень уродлив.
Mankarse
@Mankarse Небольшой синтаксический сахар может сделать его очень уродливым - я обновил его на примере.
Майкл Андерсон
Допустим, гипотетически ОП меняет свое мнение и решает добавить Sub3. У посетителя нет такой же проблемы, как instanceof? Теперь вам нужно добавить onSub3к посетителю, и он сломается, если вы забудете.
Radiodef
1
@Radiodef Преимущество использования шаблона посетителя перед непосредственным включением типа заключается в том, что после добавления onSub3в интерфейс посетителя вы получаете ошибку компиляции в каждом месте, где вы создаете нового посетителя, который еще не был обновлен. Напротив, переключение типа в лучшем случае приведет к ошибке времени выполнения, что может быть сложнее для запуска.
Майкл Андерсон
1
@FiveNine, если вы рады добавить этот полный пример в конец ответа, который может помочь другим.
Майкл Андерсон
83

С моей точки зрения: ваш дизайн не так .

В переводе на естественный язык вы говорите следующее:

Учитывая, что есть animals, есть catsи fish. animalsимеют свойства, которые являются общими для catsи fish. Но этого недостаточно: есть некоторые свойства, которые отличаются catот других fish, поэтому вам необходимо создать подкласс.

Теперь у вас есть проблема, что вы забыли смоделировать движение . Хорошо. Это относительно просто:

for(Animal a : animals){
   if (a instanceof Fish) swim();
   if (a instanceof Cat) walk();
}

Но это неправильный дизайн. Правильный путь будет:

for(Animal a : animals){
    animal.move()
}

Где moveбыло бы общее поведение, реализуемое по-разному для каждого животного.

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

Это значит: ваш дизайн сломан.

Моя рекомендация: рефакторинг Base, Sub1и Sub2.

Томас Джанк
источник
8
Если бы вы предложили реальный код, я мог бы дать рекомендации.
Томас Джанк
9
@codebreaker Если вы согласны с тем, что ваш пример не был удачным, я рекомендую принять этот ответ, а затем написать новый вопрос. Не пересматривайте вопрос, чтобы привести другой пример, иначе существующие ответы не будут иметь смысла.
Moby Disk
16
@ Codebreaker Я думаю, что это "решает" проблему, отвечая на реальный вопрос. Реальный вопрос заключается в следующем: как мне решить мою проблему? Рефакторинг вашего кода правильно. Рефакторинг так что вы .move()или .attack()и правильно абстрактной thatчасти - вместо того .swim(), .fly(), .slide(), .hop(), .jump(), .squirm(), .phone_home()и т.д. Как вы рефакторинг правильно? Неизвестно без лучших примеров ... но все же, как правило, правильный ответ - если пример кода и более подробная информация не предлагают иное.
WernerCD
11
@codebreaker Если ваша иерархия классов приводит вас к подобным ситуациям, то по определению это не имеет смысла
Патрик Коллинз
7
@codebreaker В связи с последним комментарием Crisfole, есть еще один способ взглянуть на него, который может помочь. Например, в movevs fly/ run/ etc это можно объяснить одним предложением: вы должны указывать объекту, что делать, а не как это делать. С точки зрения средств доступа, как вы упоминаете, это больше похоже на реальный случай в комментарии здесь, вы должны задавать вопросы объекта, а не проверять его состояние.
Изката
9

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

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

Поэтому вместо списка Baseработаем над списком действий

for (RandomAction action : actions)
   action.act(context);

где

interface RandomAction {
    void act(Context context);
} 

interface Context {
    Ingredients getRandomIngredients();
    double getFrequency();
}

Вы можете, при необходимости, заставить Base реализовать метод для возврата действия или любым другим способом, который вам необходим для выбора действия из экземпляров в вашем базовом списке (поскольку вы говорите, что не можете использовать полиморфизм, поэтому, вероятно, действие, которое нужно предпринять это не функция класса, а какое-то другое свойство баз, иначе вы бы просто дали Base метод act (Context))

Пит Киркхэм
источник
2
Вы понимаете, что мои абсурдные методы предназначены для того, чтобы показать различия в том, что классы могут делать после того, как их подкласс известен (и что они не имеют ничего общего друг с другом), но я думаю, что наличие Contextобъекта только ухудшает ситуацию. С таким же успехом я мог бы просто передавать Objects в методы, приводить их и надеяться, что они правильного типа.
код
1
@codebreaker, вам действительно нужно прояснить свой вопрос - в большинстве систем есть веская причина для вызова ряда функций, не зависящих от того, что они делают; например, для обработки правил для события или выполнения шага в симуляции. Обычно такие системы имеют контекст, в котором происходят действия. Если 'getRandomIngredients' и 'getFrequency' не связаны, тогда они не должны быть в одном и том же объекте, и вам нужен еще другой подход, например, чтобы действия захватывали источник ингредиентов или частоту.
Пит Киркхам
1
Вы правы, и я не думаю, что смогу спасти этот вопрос. Я закончил тем, что выбрал плохой пример, поэтому я, вероятно, просто опубликую другой. GetFrequency () и getIngredients () были просто заполнителями. Я, наверное, должен был просто поставить «...» в качестве аргументов, чтобы сделать более очевидным, что я не знаю, что это такое.
код
Это ИМО правильный ответ. Поймите, что Contextне должен быть новый класс или даже интерфейс. Обычно контекст является просто вызывающей стороной, поэтому вызовы выполняются в форме action.act(this). Если getFrequency()и getRandomIngredients()являются статическими методами, вам может даже не понадобиться контекст. Вот как я бы реализовал, скажем, очередь на работу, которая очень похожа на вашу проблему.
Вопрос
Кроме того, это в основном идентично решению для посетителей. Разница в том, что вы реализуете методы Visitor :: OnSub1 () / Visitor :: OnSub2 () в виде Sub1 :: act () / Sub2 :: act ()
QuestionC
4

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

interface SandwichCook
{
    public void makeASandwich(String[] ingredients);
}

interface AlienRadioSignalAwarable
{
    public void contactAliens(int frequency);

}

Тогда ваши занятия будут выглядеть так:

class Sub1 extends Base implements SandwichCook
{
    public void makeASandwich(String[] ingredients)
    {
        //some code here
    }
}

class Sub2 extends Base implements AlienRadioSignalAwarable
{
    public void contactAliens(int frequency)
    {
        //some code here
    }
}

И ваш цикл станет:

for (Base base : bases) {
    if (base instanceof SandwichCook) {
        base.makeASandwich(getRandomIngredients());
    } else if (base instanceof AlienRadioSignalAwarable) {
        base.contactAliens(getFrequency());
    }
}

Два основных преимущества этого подхода:

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

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

Раду Мурзеа
источник
3
Это на самом деле не решает проблему. Вы все еще нуждаетесь в приведении цикла. (Если вы этого не сделаете, то вам также не понадобится приведение в примере OP).
Taemyr
2

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

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

  1. Фред знает, что он содержит единственные ссылки на это BunchOfThings, и никакой внешней ссылки на то, что BunchOfThingsего внутренние органы не существуют нигде во вселенной. Если никто не имеет ссылки на его BunchOfThingsили его внутреннее устройство, никто больше не сможет изменить его, поэтому ограничение будет выполнено.

  2. Ни то BunchOfThings, ни какое-либо из его внутренних органов, на которые существуют внешние ссылки, не могут быть изменены каким-либо образом. Если абсолютно никто не может изменить a BunchOfThings, то ограничение будет выполнено.

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

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

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

Supercat
источник
0

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

Поэтому либо:


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

if (base.canMakeASandwich()) {
    base.makeASandwich(getRandomIngredients());
    // ... etc.
} else { // can't make sandwiches, must be able to contact aliens
    base.contactAliens(getFrequency());
    // ... etc.
}

Затем один или оба подкласса переопределяют canMakeASandwich(), и только один реализует каждый из makeASandwich()и contactAliens().


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

if (base instanceof SandwichMaker) {
    ((SandwichMaker)base).makeASandwich(getRandomIngredients());
    // ... etc.
} else { // can't make sandwiches, must be able to contact aliens
    ((AlienContacter)base).contactAliens(getFrequency());
    // ... etc.
}

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

try {
    ((SandwichMaker)base).makeASandwich(getRandomIngredients());
} catch (ClassCastException e) {
    ((AlienContacter)base).contactAliens(getFrequency());
}

Лично мне обычно не нравится последний стиль отлова ожидаемого исключения из-за риска ненадлежащего отлова ClassCastExceptionисхода getRandomIngredientsили makeASandwich, но YMMV.

Стив Джессоп
источник
2
Поймать ClassCastException - это только что. Вот и вся цель instanceof.
Radiodef
@Radiodef: правда, но одна из целей <состоит в том, чтобы избежать ловли ArrayIndexOutOfBoundsException, и некоторые люди решают сделать это тоже. Без учета вкуса ;-) В сущности, моя «проблема» в том, что я работаю в Python, где обычно предпочитают отлавливать любое исключение, а не заранее проверять, возникнет ли исключение, независимо от того, насколько простым будет предварительный тест. , Возможно, такую ​​возможность просто не стоит упоминать в Java.
Стив Джессоп
@SteveJessop »Проще просить прощения, чем разрешения« это лозунг;)
Томас Джанк
0

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

  1. Мы можем охватить все случаи в базовом классе.
  2. Ни один иностранный производный класс не должен был бы добавлять новый случай, никогда.
  3. Мы можем жить с политикой, для которой призыв, чтобы быть под контролем базового класса.
  4. Но мы знаем из нашей науки, что политика того, что делать в производном классе для метода, не входящего в базовый класс, является политикой производного класса, а не базового класса.

Если 4, то имеем: 5. Политика производного класса всегда находится под тем же политическим контролем, что и политика базового класса.

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

Но вот в чем дело. Если они все ваши, вы можете заменить if абстракцией, которая является вызовом виртуального метода (даже если это бессмысленный ответ), и избавиться от if и самостоятельного приведения. Поэтому не делай этого. Лучший дизайн доступен.

Джошуа
источник
-1

Поместите абстрактный метод в базовый класс, который выполняет одно в Sub1, а другой - в Sub2, и вызовите этот абстрактный метод в ваших смешанных циклах.

class Sub1 : Base {
    @Override void doAThing() {
        this.makeASandwich(getRandomIngredients());
    }
}

class Sub2 : Base {
    @Override void doAThing() {
        this.contactAliens(getFrequency());
    }
}

for (Base base : bases) {
    base.doAThing();
}

Обратите внимание, что для этого могут потребоваться другие изменения в зависимости от того, как определены getRandomIngredients и getFrequency. Но, честно говоря, внутри класса, который связывается с инопланетянами, вероятно, лучшее место для определения getFrequency.

Кстати, ваши методы asSub1 и asSub2 - определенно плохая практика. Если вы собираетесь сделать это, сделайте это путем кастования. Нет ничего, что эти методы дают вам, что кастинг не дает.

Random832
источник
2
Ваш ответ говорит о том, что вы не полностью прочитали вопрос: полиморфизм здесь не поможет. Есть несколько методов для каждого из Sub1 и Sub2, это всего лишь примеры, и «doAThing» на самом деле ничего не значит. Это не соответствует ничему, что я буду делать. Кроме того, что касается вашего последнего предложения: как насчет гарантированной безопасности типов?
код
@codebreaker - это точно соответствует тому, что вы делаете в цикле в примере. Если это не имеет смысла, не пишите цикл. Если он не имеет смысла как метод, он не имеет смысла как тело цикла.
Random832
1
И ваши методы «гарантируют» безопасность типов точно так же, как это делает приведение типов: генерируя исключение, если объект имеет неправильный тип.
Random832
Я понимаю, что выбрал плохой пример. Проблема в том, что большинство методов в подклассах больше похоже на геттеры, чем на мутативные методы. Эти классы сами ничего не делают, просто предоставляют данные, в основном. Полиморфизм мне там не поможет. Я имею дело с производителями, а не с потребителями. Кроме того: создание исключения - это гарантия времени выполнения, которая не так хороша, как время компиляции.
код
1
Моя точка зрения заключается в том, что ваш код обеспечивает только гарантию выполнения. Ничто не мешает кому-то не проверить isSub2 перед вызовом asSub2.
Random832
-2

Вы можете вставить оба makeASandwichи contactAliensв базовый класс, а затем реализовать их с помощью фиктивных реализаций. Поскольку возвращаемые типы / аргументы не могут быть преобразованы в общий базовый класс.

class Sub1 extends Base{
    Sandwich makeASandwich(Ingredients i){
        //Normal implementation
    }
    boolean contactAliens(Frequency onFrequency){
        return false;
    }
 }
class Sub2 extends Base{
    Sandwich makeASandwich(Ingredients i){
        return null;
    }
    boolean contactAliens(Frequency onFrequency){
       // normal implementation
    }
 }

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

Подписать
источник
1
Я действительно думал об этом, но это нарушает принцип подстановки Лискова и с ним не так приятно работать, например, когда вы набираете «sub1». и приходит автозаполнение, вы видите makeASandwich, но не связываетесь с Alien.
код
-5

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

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

Вот к чему сводятся все последующие комментарии. Механизм самолитья практически не упоминается.

Майк Накис
источник
13
Это вполне законно, если вы спроектировали себя в коробку, конечно. Это проблема многих вопросов такого типа. Люди принимают решения в других областях дизайна, которые вынуждают эти хакерские решения, когда реальное решение состоит в том, чтобы выяснить, почему вы вообще оказались в этой ситуации. Приличный дизайн не доставит вас сюда. В настоящее время я смотрю на приложение 200K SLOC и нигде не вижу, что нам нужно для этого. Так что это не просто то, что люди читают в книге. Не попадание в подобные ситуации - это просто конечный результат следования передовой практике.
Данк
3
@ Dunk Я только что показал, что потребность в таком механизме существует, например, в java.lang.reflection.Memberиерархии. Создатели java runtime попали в такую ​​ситуацию, я не думаю, что вы сказали бы, что они спроектировали себя в коробку, или вините их за то, что они не следовали каким-то лучшим практикам? Итак, я хочу подчеркнуть, что, как только у вас возникнет ситуация такого типа, этот механизм станет хорошим способом улучшить ситуацию.
Майк Накис
6
@MikeNakis Создатели Java приняли множество плохих решений, так что одно это мало что говорит. В любом случае, использование посетителя было бы лучше, чем проведение специального теста, а затем и приведения.
Доваль
3
Кроме того, пример ошибочен. Существует Memberинтерфейс, но он служит только для проверки отборочного доступа. Как правило, вы получаете список методов или свойств и используете их напрямую (без их смешивания, поэтому ничего не List<Member>появляется), поэтому вам не нужно приводить. Обратите внимание, что Classне предоставляет getMembers()метод. Конечно, невежественный программист может создать List<Member>, но это будет иметь такой же смысл, как создание List<Object>и добавление некоторых Integerили Stringк ним.
SJuan76
5
@MikeNakis «Многие люди делают это» и «Знаменитый человек делает это» не настоящие аргументы. Антипаттерны - это и плохие идеи, и широко используемые по определению, но эти оправдания оправдывают их использование. Укажите реальные причины использования того или иного дизайна. Моя проблема с ad-hoc кастингом заключается в том, что каждый пользователь вашего API должен получать кастинг правильно каждый раз, когда он это делает. Посетитель должен быть правильным только один раз. Сокрытие приведения за некоторыми методами и возврат nullвместо исключения не делает его намного лучше. При прочих равных, посетитель безопаснее при выполнении той же работы.
Доваль