Низьке зчеплення і міцна згуртованість


11

Звичайно, це залежить від ситуації. Але коли об'єкт або система нижнього важеля спілкуються з системою вищого рівня, чи слід віддавати перевагу зворотним викликам або подіям, аби зберігати вказівник на об'єкт вищого рівня?

Наприклад, у нас є worldклас, який має змінну члена vector<monster> monsters. Коли monsterклас спілкується з клавішею world, чи повинен я вважати за краще використовувати функцію зворотного виклику, чи повинен я мати покажчик на worldклас всередині monsterкласу?


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

Відповіді:


10

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

  1. Через функцію зворотного дзвінка.
  2. Через систему подій.
  3. Через інтерфейс.

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

У C ++ я рідко використовую зворотні дзвінки:

  1. C ++ не має хорошої підтримки для зворотних викликів, які зберігають свій thisпокажчик, тому важко використовувати зворотні виклики в об'єктно-орієнтованому коді.

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

У цьому випадку я, мабуть, зробив би інтерфейс. У своєму запитанні ви насправді не уточнюєте, що monsterнасправді потрібно спілкуватися world. Здогадуючись, я б зробив щось на кшталт:

class IWorld {
public:
  virtual Monster* getNearbyMonster(const Position & position) = 0;
  virtual Item*    getItemAt(const Position & position) = 0;
};

class Monster {
public:
  void update(IWorld * world) {
    // Do stuff...
  }
}

class World : public IWorld {
public:
  virtual Monster* getNearbyMonster(const Position & position) {
    // ...
  }

  virtual Item*    getItemAt(const Position & position) {
    // ...
  }

  // Lots of other stuff that Monster should not have access to...
}

Ідея тут полягає в тому, що ви вводите лише IWorld(що є шаленим ім'ям) голий мінімум, до якого Monsterпотрібно отримати доступ. Його погляд на світ повинен бути максимально вузьким.


1
+1 Делегати (зворотні звороти) зазвичай стають все більш численними з часом. Надання інтерфейсу монстрам, щоб вони могли потрапити на речі - це гарний спосіб перейти на мою думку.
Майкл Коулман

12

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

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

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


+1 Надання обмеженому способу закликати монстра до батька - це приємний середній шлях.
Майкл Коулман

7

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

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

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


3

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

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

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


2

Очевидно, ви не підете без подій, але, перш ніж навіть почати писати (і проектувати) систему подій, вам слід задати справжнє запитання: чому монстр спілкуватиметься зі світовим класом? Чи справді це повинно бути?

Візьмемо "класичну" ситуацію, монстр атакує гравця.

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

void Monster::attack(LivingCreature l)
{
  // Call to combat system
}

Але світ (який вже знає монстра) не повинен знати монстра. Власне, монстр може ігнорувати саме існування класу Світ, що, мабуть, краще.

Те ж саме, коли монстр рухається (я дозволяю підсистемам отримати істоту і обробляти обчислення / наміри для переміщення, монстр - це просто пакет даних, але багато людей скажуть, що це не справжній OOP).

Моя думка: події (або зворотний дзвінок), звичайно, чудові, але вони не єдина відповідь на кожну проблему, з якою ви зіткнетесь.


1

Кожного разу, коли я можу, я намагаюся обмежити спілкування між об'єктами моделлю "запит і відповідь". Існує мається на увазі часткове впорядкування об'єктів у моїй програмі таким чином, що між будь-якими двома об'єктами A і B може бути спосіб, щоб A прямо чи опосередковано викликав метод B або B, щоб прямо чи опосередковано викликати метод A , але А і В ніколи не можна взаємно називати методи один одного. Іноді, звичайно, хочеться мати зворотній зв'язок з абонентом того чи іншого методу. Мені подобається це робити пару способів, і жоден з них не є зворотним.

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

Інший спосіб - викликати взаємний дочірній предмет. Тобто, якщо A викликає метод на B, а B потрібно передавати деяку інформацію A, B називає метод на C, де A і B можуть обидва викликати C, але C не може викликати A або B. відповідальний за отримання інформації від C після того, як B повертає контроль до А. Зауважте, що це насправді принципово не відрізняється від першого запропонованого мною способу. Об'єкт A все ще може отримати інформацію лише із поверненого значення; жоден з методів об'єкта A не викликається B або C. Варіантом цього фокусу є передача C як параметра методу, але обмеження щодо відношення C до A і B все ще застосовуються.

Тепер важливим питанням є те, чому я наполягаю робити такі дії. Є три основні причини:

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

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

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


0

Особисто? Я просто використовую сингл.

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

Чи буде у вас колись одразу два світи? Можливо! Можливо, і будете. Але якщо ви не можете придумати цю ситуацію прямо зараз, ви, мабуть, не зробите.

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

Для цього потрібно трохи дисциплінувати, щоб прибирати речі, коли вони стають безладними (це "коли", а не "якщо"), але немає ніякого способу запобігти забрудненню коду - або у вас є проблема спагетті, або тисячі -проблеми з абстрагуванням. Принаймні таким чином ви не пишете велику кількість непотрібного коду.

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


2
Я б, напевно, висловив це трохи більш лаконічно: "Не бійтеся рефактора".
Тетрад

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

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