Кастинг
Це майже напевно стане повною мірою до підходу цитованої книги, але один із способів краще відповідати провайдеру - це сприйняти мислення, що лежить в одній центральній області кодової бази, використовуючи 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.