Принцип поділу інтерфейсу: Що робити, якщо інтерфейси мають значне перекриття?


9

З розробки програмного забезпечення Agile, принципів, моделей та практик: Нова міжнародна версія Pearson :

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

Дядько Боб, розповідає про випадок, коли є незначне перекриття.

Що робити, якщо є значне перекриття?

Скажіть, у нас є

Class UiInterface1;
Class UiInterface2;
Class UiInterface3;

Class UiIterface : public UiInterface1, public UiInterface2, public UiInterface3{};

Що робити, якщо між UiInterface1та між ними є значне перекриття UiInterface2?


Коли я стикаюся з сильно перекриваються інтерфейсами, я створюю батьківський інтерфейс, який групує загальні методи, а потім успадковує цей звичайний для створення спеціалізацій. АЛЕ! Якщо ви ніколи не хочете, щоб хтось використовував загальний інтерфейс без спеціалізації, то вам насправді потрібно піти на дублювання коду, тому що якщо ви введете спільний інтерфейс для батьків, люди можуть використовувати цей.
Енді

Питання для мене трохи розпливчасте, можна відповісти на багато різних рішень залежно від випадків. Чому виросло перекриття?
Артур Гавлічек

Відповіді:


1

Кастинг

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

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

Наприклад, може здатися дивним розробити такі функції клієнта:

// Returns the absolute position of an entity as the sum
// of its own position and the position of its ancestors.
// `position` and `parenting` parameters should point to the 
// same object.
Vec2i abs_position(IPosition* position, IParenting* parenting)
{
     const Vec2i xy = position->xy();
     auto parent = parenting->parent();
     if (parent)
     {
         // If the entity has a parent, return the sum of the
         // parent position and the entity's local position.
         return xy + abs_position(dynamic_cast<IPosition*>(parent),
                                  dynamic_cast<IParenting*>(parent));
     }
     return xy;
}

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

Змішування відповідальності та кастингу

Розробляючи інтерфейси з абсолютно дистильованою, ультрасингулярною відповідальністю, часто спокушається або прийняти якийсь інтерфейс пониження, або консолідувати інтерфейси для виконання декількох обов'язків (і, отже, ступати як на ISP, так і на SRP).

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

// Returns the absolute position of an entity as the sum
// of its own position and the position of its ancestors.
// `obj` should implement `IPosition` and optionally `IParenting`.
Vec2i abs_position(Object* obj)
{
     // `Object::query_interface` returns nullptr if the interface is
     // not provided by the entity. `Object` is an abstract base class
     // inherited by all entities using this interface query system.
     IPosition* position = obj->query_interface<IPosition>();
     assert(position && "obj does not implement IPosition!");
     const Vec2i xy = position->xy();

     IParenting* parenting = obj->query_interface<IParenting>();
     if (parenting && parenting->parent()->query_interface<IPosition>())
     {
         // If the entity implements IParenting and has a parent, 
         // return the sum of the parent position and the entity's 
         // local position.
         return xy + abs_position(parenting->parent());
     }
     return xy;
}

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

З цим, спокуса проектувати інтерфейси, що перекриваються, часто пом'якшується до абсолютного мінімуму. Це дозволяє створювати інтерфейси з дуже особливими обов'язками (іноді лише однією функцією члена всередині), що ви можете змішувати і співставляти все, що вам подобається, не турбуючись про ISP, і отримуючи гнучкість введення псевдо-качки під час виконання в C ++ (хоча, звичайно, з компроміс покарань за час виконання запитів на об'єкти, щоб перевірити, чи підтримують вони певний інтерфейс). Частина виконання може бути важливою в, скажімо, налаштуваннях з набором програмного забезпечення, де функції не матимуть заздалегідь інформацію про час збирання плагінів, які реалізують ці інтерфейси.

Шаблони

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

// Returns the absolute position of an entity as the sum
// of its own position and the position of its ancestors.
// `obj` should have `position` and `parent` methods.
template <class Entity>
Vec2i abs_position(Entity& obj)
{
     const Vec2i xy = obj.xy();
     if (obj.parent())
     {
         // If the entity has a parent, return the sum of the parent 
         // position and the entity's local position.
         return xy + abs_position(obj.parent());
     }
     return xy;
}

... звичайно, у такому випадку parentметоду доведеться повернути той самий Entityтип, і в цьому випадку ми, мабуть, хочемо уникати інтерфейсів прямо (оскільки вони часто хочуть втратити інформацію про тип на користь роботи з базовими покажчиками).

Субстанційно-складова система

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

Зауважте, що це рішення у стилі COM є повністю там, наскільки розроблено інструментарій GUI, і ECS було б ще більше, так що це не те, що буде забезпечено великими ресурсами. Однак це однозначно дозволить вам пом’якшити спокуси проектувати інтерфейси, які мають відповідальність, що перекривається, до абсолютного мінімуму, часто роблячи це непроблемним.

Прагматичний підхід

Альтернативою, звичайно, є трохи розслабити захист або розробити інтерфейси на детальному рівні, а потім почати успадковувати їх, щоб створити більш грубі інтерфейси, які ви використовуєте, як, IPositionPlusParentingщо випливає з обох IPositionтаIParenting(сподіваюся, з кращим ім'ям, ніж це). З чистими інтерфейсами він не повинен порушувати ISP так само, як ті монолітні глибокоієрархічні підходи, які зазвичай застосовуються (Qt, MFC тощо), де документація часто відчуває необхідність приховувати невідповідні члени з огляду на надмірний рівень порушення ISP з тими видами конструкцій), тому прагматичний підхід може просто сприйняти деяке перекриття тут і там. Однак такий підхід у стилі COM уникає необхідності створювати консолідовані інтерфейси для кожної комбінації, яку ви коли-небудь використовуватимете. Занепокоєння щодо "самодостатності" в таких випадках повністю усувається, і це часто усуває кінцеве джерело спокуси спроектувати інтерфейси, які мають дублюючі обов'язки, які хочуть боротися як з SRP, так і з ISP.


11

Це виклик судового рішення, який вам належить здійснити в кожному конкретному випадку.

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

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

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

Розглянемо наступний код:

public interface A
{
    void X();
    void Y();
}

public class Foo
{
     public void ConsumeX(A a)
     {
         a.X();
     }
}

Тепер у нас ситуація, коли, якщо ми хочемо передати новий об’єкт в ConsumeX, він повинен реалізувати X () і Y (), щоб відповідати договору.

Тож чи слід зараз змінити код, щоб виглядати наступним прикладом?

public interface A
{
    void X();
    void Y();
}

public interface B
{
    void X();
}

public class Foo
{
     public void ConsumeX(B b)
     {
         b.X();
     }
}

ISP пропонує нам це зробити, тому ми повинні схилятися до цього рішення. Але без контексту важко бути впевненим. Чи ймовірно, що ми продовжимо A і B? Чи ймовірно, що вони продовжать самостійно? Чи є ймовірність, що B коли-небудь реалізує методи, яких A не потребує? (Якщо ні, ми можемо зробити A похід від B.)

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

Чому? Тому що згодом легко змінити свою думку. Коли вам потрібен цей новий клас, просто створіть новий інтерфейс та реалізуйте обидва у своєму старому класі.


1
"Перш за все, пам’ятайте, що принципи ТОЛІ - це просто ... принципи. Це не правила. Вони не срібна куля. Вони просто принципи. Це не відбирати їх важливості, завжди слід схилятися" у бік слідування за ними. Але другий, коли вони вводять рівень болю, ви повинні занурити їх, поки не знадобляться. " Це має бути на першій сторінці кожної книги дизайну / принципів дизайну. Він повинен з’являтися також на кожні 50 сторінок як нагадування.
Крістіан Родрігес
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.