Два інтерфейси з однаковими підписами


13

Я намагаюся моделювати карткову гру, де карти мають два важливі набори функцій:

Перший - ефект. Це зміни в ігровому стані, які відбуваються під час гри в карту. Інтерфейс для ефекту такий:

boolean isPlayable(Player p, GameState gs);
void play(Player p, GameState gs);

І ви можете вважати карту граючою, якщо і лише якщо ви зможете задовольнити її вартість і всі її наслідки відтворювати. Так:

// in Card class
boolean isPlayable(Player p, GameState gs) {
    if(p.resource < this.cost) return false;
    for(Effect e : this.effects) {
        if(!e.isPlayable(p,gs)) return false;
    }
    return true;
}

Гаразд, поки що, досить просто.

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

boolean isActivatable(Player p, GameState gs);
void activate(Player p, GameState gs);

І я усвідомлюю, що за винятком називати це "активувати" замість "грати", Abilityі Effectмати точно такий же підпис.


Чи погано мати кілька інтерфейсів з однаковою підписом? Чи повинен я просто використовувати один і мати два набори одного інтерфейсу? Як так:

Set<Effect> effects;
Set<Effect> abilities;

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

Дрібний шрифт:

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


Просто слідкуйте за KISS та YAGNI, і вам буде добре.
Бернард

2
Єдина причина, що це питання навіть виникає, це те, що у ваших функцій є доступ до занадто великого стану, завдяки неймовірно широким і нестримним параметрам Player та GameState.
Ларс Віклунд

Відповіді:


18

Просто те, що два інтерфейси мають один і той же контракт, не означає, що вони однакові.

Принцип заміщення Ліскова говорить:

Нехай q (x) - властивість, доказувальна щодо об'єктів x типу T. Тоді q (y) має бути доказовим для об'єктів y типу S, де S є підтипом T.

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

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


2

З Вікіпедії : " Інтерфейс часто використовується для визначення абстрактного типу, який не містить даних, але розкриває поведінку, визначену як методи ". На мою думку, інтерфейс використовується для опису поведінки, тому якщо у вас різні форми поведінки, має сенс мати різні інтерфейси. Читаючи ваше запитання, враження, яке у мене склалося, ви говорите про різні способи поведінки, тому найкращий підхід може бути різним інтерфейсом.

Ще один момент, який ви самі сказали, це те, що якщо одна з цих форм поведінки зміниться. Тоді що відбувається, коли у вас є лише один інтерфейс?


1

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

Однак, якщо вони надзвичайно схожі, можливо, має сенс виводити їх із спільного предка. Поміркуйте уважно: чи є у вас підстави вважати, що "ефекти" та "здібності" завжди обов'язково матимуть однаковий інтерфейс? Якщо ви додасте елемент в effectінтерфейс, чи очікуєте, що вам потрібен той самий елемент, який буде доданий до abilityінтерфейсу?

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


0

Те, що ви бачите, в основному є артефактом обмеженої виразності систем типу.

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

Зараз системи різних типів мають різну ступінь виразності, але завжди знайдеться те, чого неможливо виразити.

Наприклад: два інтерфейси, які мають різну поведінку помилок, можуть мати однаковий підпис у C #, але не у Java (тому що у Java винятки є частиною підпису). Два інтерфейси, поведінка яких відрізняється лише від їх побічних ефектів, можуть мати однаковий підпис у Java, але матимуть різні підписи в Haskell.

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.