Чи добре мати об’єкти, які створюють себе, навіть якщо це забруднює API їхніх підкласів?


33

У мене є базовий клас, Base. Він має два підкласи Sub1та Sub2. Кожен підклас має деякі додаткові методи. Наприклад, Sub1має Sandwich makeASandwich(Ingredients... ingredients)і Sub2має boolean contactAliens(Frequency onFrequency).

Оскільки ці методи приймають різні параметри і роблять абсолютно різні речі, вони абсолютно несумісні, і я не можу просто використати поліморфізм для вирішення цієї проблеми.

Baseзабезпечує більшу частину функціональності, і у мене є велика колекція Baseоб'єктів. Однак усі Baseоб’єкти є або a, Sub1або 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екземпляр насправді є базовим, має сенс використовувати спадкування, а не інкапсуляцію. Це те, що я роблю добре? Чи є кращий спосіб вирішити цю проблему?


12
Усі 3 класи тепер мають знати один про одного. Додавання Sub3 призведе до багатьох змін коду, а додавання Sub10 буде прямо болісним
Ден Пішельман

15
Це дуже допомогло б, якби ви дали нам реальний код. Існують ситуації, коли доцільно приймати рішення, виходячи з певного класу чогось, але неможливо сказати, чи виправдані ви, що робите з такими надуманими прикладами. Що б то не було, те, що ви хочете, - це Союз відвідувачів або тегів .
Довал

1
Вибачте, я подумав, що це спростить приклади спрощених прикладів. Можливо, я опублікую нове запитання з більш широким спектром того, що хочу зробити.
кодовий прорив

12
Ви просто доповнюєте кастинг і instanceof, таким чином, вимагаючи багато введення тексту, схильний до помилок і ускладнює додавання більше підкласів.
користувач253751

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())
     ));

Який ІМО майже настільки акуратний і лаконічний, як ви можете отримати.

Ось більш повний приклад 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 Трохи синтаксичний цукор може зробити це дуже далеко не потворним - я оновив його на прикладі.
Майкл Андерсон

Скажімо, гіпотетично ОП змінює свою думку і вирішує додати 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
Якщо ви запропонуєте фактичний код, я можу дати рекомендації.
Thomas Junk

9
@codebreaker Якщо ви погоджуєтесь, що ваш приклад виявився непоганим, рекомендую прийняти цю відповідь і написати нове запитання. Не переглядайте питання, щоб мати інший приклад, інакше відповіді не мають сенсу.
Диск Moby

16
@codebreaker Я думаю, що це "вирішує" проблему, відповідаючи на реальне запитання. Справжнє питання: Як вирішити свою проблему? Рефакторинг коду належним чином. Рефакторинг так що ви .move()або .attack()й правильно абстрактної thatчастини - замість того .swim(), .fly(), .slide(), .hop(), .jump(), .squirm(), .phone_home()і т.д. Як ви рефакторинг правильно? Невідомо без кращих прикладів ... але все-таки правильна відповідь - якщо тільки приклад коду та більше деталей не підкаже інше.
WernerCD

11
@codebreaker Якщо ієрархія вашого класу призводить вас до подібних ситуацій, то, за визначенням, це не має сенсу
Патрік Коллінз

7
@codebreaker Що стосується останнього коментаря Крісфола, є ще один спосіб поглянути на це, який може допомогти. Наприклад, за допомогою movevs fly/ run/ тощо, що можна пояснити в одному реченні як: Ви повинні говорити об'єкту, що робити, а не як це зробити. Що стосується аксесуарів, то, як ви згадуєте, більше нагадує реальний випадок у коментарі тут, вам слід задавати питання об’єкту, а не перевіряти його стан.
Ізката

9

Трохи важко уявити обставину, коли у вас є група речей і хочете, щоб вони або робили бутерброд, або контактували з прибульцями. У більшості випадків, коли ви знайдете таке кастинг, ви будете працювати з одним типом - наприклад, у кланге ви фільтруєте набір вузлів для декларацій, де 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об'єкта просто погіршує питання. Я, можливо, просто переходжу Objectдо методів, кидаючи їх, і сподіваюся, що вони потрібного типу.
кодовий прорив

1
@codebreaker вам дійсно потрібно уточнити своє запитання - у більшості систем буде поважна причина викликати купу функцій, що відповідають їх дії; наприклад, обробити правила щодо події або виконати крок у моделюванні. Зазвичай такі системи мають контекст, в якому відбуваються дії. Якщо 'getRandomIngredients' і 'getFrequency' не пов’язані між собою, тоді вони не повинні знаходитися в одному об’єкті, і вам потрібен ще інший підхід, наприклад, щоб дії фіксували джерело інгредієнтів або частоту.
Піт Кіркхем

1
Ви маєте рацію, і я не думаю, що зможу врятувати це питання. Я в кінцевому підсумку вибрав поганий приклад, тож, мабуть, просто опублікую ще один. GetFrequency () та getIngredients () були просто заповнювачами. Я, мабуть, повинен був просто поставити "..." як аргументи, щоб зробити більш очевидним, що я не знаю, що вони є.
кодовий прорив

Це правильна відповідь ІМО. Зрозумійте, що Contextне потрібно бути новим класом чи навіть інтерфейсом. Зазвичай контекст є лише тим, хто телефонує, тому дзвінки у формі action.act(this). Якщо getFrequency()і getRandomIngredients()є статичними методами, можливо, вам навіть не знадобиться контекст. Ось як я би реалізував, скажімо, чергу на роботу, яка у вашій проблемі звучить дуже жахливо.
QuestionC

Крім того, це здебільшого ідентичне рішення щодо схеми відвідувачів. Різниця полягає в тому, що ви реалізуєте методи 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-петля стане:

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

Дві основні переваги цього підходу:

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

PS: Вибачте за назви інтерфейсів, я не міг придумати нічого крутішого в той конкретний момент: D.


3
Це фактично не вирішує проблему. Вам ще потрібен ролик у циклі for. (Якщо ви цього не зробите, вам також не потрібен амплуа в прикладі ОП).
Taemyr

2

Цей підхід може бути сприятливим у випадках, коли майже будь-який тип в сім'ї буде або безпосередньо застосований як реалізація деякого інтерфейсу, який відповідає певному критерію, або може бути використаний для створення реалізації цього інтерфейсу. Вбудовані типи колекцій IMHO отримали б користь від цього шаблону, але оскільки вони цього не роблять для прикладу, я вигадаю інтерфейс колекції BunchOfThings<T>.

Деякі реалізації BunchOfThingsфайлів є змінними; деякі - ні. У багатьох випадках об’єкт, який Фред може захотіти вмістити в себе щось, що може використовуватись як, BunchOfThingsі знає, що нічого, крім Фреда, не зможе змінити. Цю вимогу можна задовольнити двома способами:

  1. Фред знає, що він має єдині посилання на це BunchOfThings, і жодної зовнішньої посилання на це, BunchOfThingsякщо його внутрішні не існують ніде у Всесвіті. Якщо ніхто інший не має посилання на його BunchOfThingsабо його внутрішні, ніхто більше не зможе його змінити, тому обмеження буде задоволено.

  2. Ні те BunchOfThings, ні будь-яке його внутрішнє середовище, на яке існують зовнішні посилання, не можуть бути змінені будь-якими способами. Якщо абсолютно ніхто не зможе змінити BunchOfThings, тоді обмеження буде задоволено.

Одним із способів задоволення обмеження буде безумовна копія будь-якого отриманого об'єкта (рекурсивна обробка будь-яких вкладених компонентів). Іншим було б перевірити, чи обіцяє отриманий об’єкт незмінність, а якщо ні, зробити його копію та зробити так само з будь-якими вкладеними компонентами. Альтернативою, яка може бути чистішою за другу і швидшою за першу, є запропонувати AsImmutableметод, який просить об'єкт зробити непорушну копію себе (використовуючи AsImmutableбудь-які вкладені компоненти, які його підтримують).

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

Зауважте, що хоча asXXметоди можуть повертати трохи різні типи, їх реальна роль полягає в тому, щоб повернені об'єкти задовольняли потреби програми.


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 - це лише ew. Ось і вся мета екземпляра.
Radiodef

@Radiodef: Це правда, але одна мета <- уникнути лову ArrayIndexOutOfBoundsException, і деякі люди вирішують це зробити також. Ніякого обліку смаку ;-) В основному моя "проблема" тут полягає в тому, що я працюю в Python, де зазвичай кращим стилем є те, що вилов будь-якого винятку є кращим для попереднього тестування, чи буде виняток, незалежно від того, наскільки простим буде попередній тест. . Можливо, цю можливість просто не варто згадувати в Java.
Стів Джессоп

@SteveJessop »Простіше просити пробачення, ніж дозволу« - це гасло;)
Thomas Junk

0

Тут ми маємо цікавий випадок базового класу, який спускається до власних похідних класів. Ми добре знаємо, що це зазвичай погано, але якщо ми хочемо сказати, що ми знайшли вагому причину, давайте подивимось і подивимося, які обмеження в цьому можуть бути:

  1. Ми можемо охопити всі випадки в базовому класі.
  2. Жоден іноземний похідний клас не повинен був би додавати новий випадок ніколи.
  3. Ми можемо жити з тією політикою, для якої вимагаємо перейти під контроль базового класу.
  4. Але ми знаємо з нашої науки, що політика того, що робити у похідному класі для методу, який не є базовим, є політикою похідного класу, а не базовим класом.

Якщо 4, то маємо: 5. Політика похідного класу завжди знаходиться під тим же політичним контролем, що і політика базового класу.

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

Але ось у чому річ. Якщо вони всі ваші, ви можете замінити if на абстракцію, яка є викликом віртуального методу (навіть якщо це нісенітниця), і позбутися від if і self-cast. Тому не робіть цього. Є кращий дизайн.


-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, безумовно, є поганою практикою. Якщо ви збираєтеся це зробити, зробіть це за допомогою кастингу. Ці методи не дають вам того, що кастинг не робить.


2
Ваша відповідь говорить про те, що ви не прочитали повністю питання: поліморфізм просто не вирішить його. Існує кілька методів для кожного з Sub1 і Sub2, це лише приклади, і "doAThing" насправді нічого не означає. Це не відповідає нічого, що я б робив. Крім того, що стосується останнього речення: як щодо гарантованої безпеки типу?
кодовий прорив

@codebreaker - Це точно відповідає тому, що ви робите в циклі в прикладі. Якщо це не має сенсу, не пишіть цикл. Якщо це не має сенсу як методу, він не має сенсу як тіло циклу.
Випадково832

1
І ваші методи "гарантують" безпеку типу, як і кастинг: викидаючи виняток, якщо об'єкт неправильного типу.
Випадково832

Я усвідомлюю, що вибрав поганий приклад. Проблема полягає в тому, що більшість методів у підкласах більше схожі на гетерів, ніж на мутаційні. Ці заняття не роблять справи самі, просто надають дані, в основному. Поліморфізм мені там не допоможе. Я маю справу з виробниками, а не споживачами. Також: виняток - це гарантія виконання, що не так добре, як час компіляції.
кодовий прорив

1
Моя думка, ваш код надає лише гарантію виконання. Ніщо не заважає комусь не вдатися перевірити isSub2 перед тим, як викликати asSub2.
Випадково832

-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, але не контактує з Alienens.
codebreaker

-5

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

Я кілька разів використовував той самий механізм, і з впевненістю можу сказати, що час виконання Java також міг би зробити те саме, щонайменше в одному місці, про яке я можу подумати, покращуючи таким чином продуктивність, зручність і читабельність коду використовує його.

Візьмемо для прикладу java.lang.reflect.Member, що є основою java.lang.reflect.Fieldі java.lang.reflect.Method. (Фактична ієрархія трохи складніша за це, але це не має значення.) Поля та методи значно відрізняються від тварин: у одного є значення, яке можна отримати або встановити, а в іншого немає такого поняття, але його можна використовувати кілька параметрів, і воно може повернути значення. Отже, поля та методи обидва учасники, але те, що ви можете зробити з ними, приблизно настільки ж відрізняється одне від одного, як робити бутерброди проти контакту з прибульцями.

Тепер, коли пишемо код, який використовує відображення, ми дуже часто маємо Memberв руках, і ми знаємо, що це або а, Methodабо Field(або, рідко, щось інше), і все ж нам доводиться робити все нудне, instanceofщоб точно з'ясувати. що це таке, і тоді ми мусимо це робити, щоб отримати належне посилання на нього. (І це не тільки нудно, але й не дуже добре.) MethodКлас міг би дуже легко реалізувати схему, яку ви описуєте, тим самим полегшивши життя тисячі програмістів.

Звичайно, ця методика є життєздатною лише у невеликих, чітко визначених ієрархіях тісно зв'язаних класів, у яких ви (і завжди матимете) контроль рівня джерела: ви не хочете робити таке, якщо ваша ієрархія класів є можуть бути подовжені людьми, які не мають свободи перетворити базовий клас.

Ось, як те, що я зробив, відрізняється від того, що ви зробили:

  • Базовий клас забезпечує реалізацію за замовчуванням для всього asDerivedClass()сімейства методів, кожен з яких повертається null.

  • Кожен похідний клас лише переосмислює один із asDerivedClass()методів, повертаючись thisзамість null. Це не відміняє нікого з решти, і не хоче нічого про них знати. Отже, не IllegalStateExceptionкидаються.

  • Базовий клас також забезпечує finalреалізацію для всього isDerivedClass()сімейства методів, кодованих таким чином: return asDerivedClass() != null; Таким чином, кількість методів, які потрібно перекрити похідними класами, зводиться до мінімуму.

  • Я не використовував @Deprecatedцей механізм, тому що не думав про це. Тепер, коли ви дали мені ідею, я буду використовувати її для використання, дякую!

C # має вбудований відповідний механізм за допомогою використання asключового слова. В C # ви можете сказати, DerivedClass derivedInstance = baseInstance as DerivedClassі ви отримаєте посилання на a, DerivedClassякщо ви baseInstanceбули цього класу, або nullякщо він не був. Це (теоретично) працює краще, ніж isслідує за кидком, ( isце, мабуть, краще називається ключовим словом C # для instanceof), але спеціальний механізм, який ми instanceofпрацювали вручну, виконує навіть краще: пара операцій 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 потрапили в подібний тип ситуації, я не думаю, ви б сказали, що вони створили себе в коробку, або звинувачуєте їх у тому, що вони не дотримуються якихось кращих практик? Отже, суть, яку я зауважу тут, полягає в тому, що коли у вас виникнути такі ситуації, цей механізм - це хороший спосіб поліпшити речі.
Майк Накіс

6
@MikeNakis Творці Java прийняли багато поганих рішень, так що самі по собі не говорять багато. У будь-якому випадку, користування відвідувачем було б краще, ніж робити спеціальний тест, а потім - акторський склад.
Довал

3
Крім того, приклад є хибним. Існує Memberінтерфейс, але він служить тільки для перевірки відбіркового доступу. Як правило, ви отримуєте перелік методів або властивостей і використовуєте це безпосередньо (без їх змішування, тому не List<Member>з'являється), тому вам не потрібно робити це. Зверніть увагу, що метод Classне передбачає getMembers(). Звичайно, нерозумний програміст може створити List<Member>, але це мало б сенс, як зробити його a List<Object>і додати деякі Integerабо Stringдо нього.
SJuan76

5
@MikeNakis "Дуже багато людей це робить" та "Знаменита людина робить це" - не справжні аргументи. Анти-візерунки є і поганими ідеями, і широко використовуються за визначенням, але ці виправдання виправдовували б їх використання. Наведіть реальні причини використання того чи іншого дизайну. Моя проблема при спеціальному кастингу полягає в тому, що кожен користувач вашого API повинен отримувати право кастингу кожен раз, коли він це робить. Відвідувач повинен бути правильним лише один раз. Приховування кастингу за деякими методами та повернення nullзамість винятку не робить його набагато кращим. За інших рівних, відвідувач безпечніше, виконуючи ту саму роботу.
Довал
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.