Низкое сцепление и жесткое сцепление

11

Конечно, это зависит от ситуации. Но когда объект или система более низкого уровня обмениваются данными с системой более высокого уровня, следует ли отдавать предпочтение обратным вызовам или событиям, а не указателю на объект более высокого уровня?

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

Hidayat
источник
Помимо написания в названии вопроса, оно фактически не сформулировано в форме вопроса. Я думаю, что перефразировка может помочь закрепить то, что вы спрашиваете здесь, поскольку я не думаю, что вы можете получить полезный ответ на этот вопрос в его нынешнем виде. И это также не игровой дизайн вопрос, это вопрос о структуре программирования (не уверен , если это означает , что конструкция тега или не подходит, не могу вспомнить , где мы спустились на «дизайн программного обеспечения» и тегов)
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

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

Часто вы можете полностью избежать двунаправленной ссылки, передавая данные по мере необходимости. Тривиальный рефакторинг - сделать так, чтобы вместо того, чтобы монстр сохранял связь с миром, вы передавали мир, ссылаясь на методы монстра, которым он нужен. Еще лучше - передать интерфейс только для тех кусков мира, в которых монстр остро нуждается, то есть монстр не полагается на конкретную реализацию в мире. Это соответствует принципу разделения интерфейсов и принципу инверсии зависимостей , но не начинает вводить избыточную абстракцию, которую вы иногда можете получить с событиями, сигналами + слотами и т. Д.

В некотором смысле, вы можете утверждать, что использование обратного вызова - это очень специализированный мини-интерфейс, и это нормально. Вы должны решить, можете ли вы более осмысленно достичь своих целей с помощью одного набора методов в объекте интерфейса или нескольких различных методов в различных обратных вызовах.

Kylotan
источник
3

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

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

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

Замочить
источник
2

Очевидно, вы не продвинетесь далеко без событий, но прежде чем даже начать писать (и разрабатывать) систему событий, вы должны задать вам реальный вопрос: почему монстр общается с мировым классом? Должна ли она на самом деле?

Давайте возьмем «классическую» ситуацию, монстр атакует игрока.

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

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

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

То же самое, когда монстр движется (я позволил подсистемам получить существо и обработать для него вычисления / намерения перемещения, монстр был просто пакетом данных, но многие люди сказали бы, что это не настоящий ООП).

Суть в том, что события (или обратный вызов), конечно, великолепны, но они не являются единственным ответом на каждую проблему, с которой вы столкнетесь.

Raveline
источник
1

Всякий раз, когда я могу, я стараюсь ограничить связь между объектами в модели запроса и-ответа. Существует подразумеваемое частичное упорядочение объектов в моей программе, так что между любыми двумя объектами A и B может существовать способ для прямого или косвенного вызова метода B или для B прямого или косвенного вызова метода A , но это никогда не возможно для а и в взаимно вызывать методы друг друга. Иногда, конечно, вы хотите иметь обратную связь с вызывающей стороной метода. Есть несколько способов, которые мне нравятся, и ни один из них не является обратным вызовом.

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

Другой способ - вызвать общий дочерний объект. То есть, если A вызывает метод на B, а B необходимо передать некоторую информацию A, B вызывает метод на C, где A и B могут одновременно вызывать C, но C не может вызывать A или B. Тогда объект A будет ответственный за получение информации от C после того, как B возвращает управление A. Обратите внимание, что это на самом деле принципиально не отличается от первого способа, который я предложил. Объект A все еще может извлечь информацию только из возвращаемого значения; ни один из методов объекта A не вызывается B или C. Разновидностью этого трюка является передача C в качестве параметра методу, но ограничения по отношению C к A и B все еще применяются.

Теперь важный вопрос: почему я настаиваю на том, чтобы делать так? Есть три основные причины:

  • Это держит мои объекты более свободно связаны. Мои объекты могут инкапсулировать другие объекты, но они никогда не будут зависеть от контекста вызывающего, и контекст никогда не будет зависеть от инкапсулированных объектов.
  • Это позволяет легко рассуждать о моём контроле. Приятно иметь возможность предположить, что единственный код, который может изменить внутреннее состояние во selfвремя выполнения метода, - это один метод, а не другой. Это тот же тип рассуждений, который может привести к созданию взаимных исключений на параллельных объектах.
  • Он защищает инварианты инкапсулированных данных моих объектов. Публичные методы могут зависеть от инвариантов, и эти инварианты могут быть нарушены, если один метод может быть вызван извне, в то время как другой уже выполняется.

Я не против всех видов использования обратных вызовов. В соответствии с моей политикой никогда не «вызывать вызывающего», если объект A вызывает метод на B и передает ему обратный вызов, обратный вызов не может изменить внутреннее состояние A, и это включает объекты, инкапсулированные A, и объекты в контексте А. Другими словами, обратный вызов может вызывать методы только для объектов, данных ему посредством B. Обратный вызов, по сути, находится под теми же ограничениями, что и B.

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

Джейк Макартур
источник
0

Лично? Я просто использую синглтон.

Да, хорошо, плохой дизайн, не объектно-ориентированный и т. Д. Знаете что? Я не забочусь . Я пишу игру, а не демонстрацию технологий. Никто не собирается оценивать меня по коду. Цель состоит в том, чтобы сделать игру веселой, и все, что мне мешает, приведет к игре, которая будет менее веселой.

Будете ли вы когда-нибудь запустить два мира одновременно? Может быть! Может быть, вы будете. Но если вы не можете думать об этой ситуации прямо сейчас, вы, вероятно, не будете.

Итак, мое решение: сделать мир синглтоном. Вызовите функции на нем. Быть сделано со всем беспорядком. Вы можете передать дополнительный параметр каждой функции - и не ошибитесь, вот к чему это приведет. Или вы можете просто написать код, который работает.

Чтобы сделать это таким образом, требуется немного дисциплины, чтобы убрать вещи, когда они становятся грязными (это «когда», а не «если»), но нет никакого способа предотвратить беспорядочный код - либо у вас есть проблема со спагетти, либо тысячи проблема абстракции. По крайней мере, таким образом вы не пишете большое количество ненужного кода.

И если вы решите, что вам больше не нужен синглтон, обычно довольно просто избавиться от него. Требуется некоторая работа, проходит миллиард параметров, но это те параметры, которые вам все равно придется обойти.

ZorbaTHut
источник
2
Я бы, наверное, сформулировал это более кратко: «Не бойтесь рефакторинга».
Тетрад
Одиночки злые! Глобалы намного лучше. Все перечисленные вами достоинства синглтона одинаковы для всего мира. Я использую глобальные указатели (на самом деле глобальные функции, возвращающие ссылки) на мою подсистему и инициализирую / уничтожаю их в своей основной функции. Глобальные указатели избегают проблем порядка инициализации, висячих синглетонов во время разрыва, нетривиальной конструкции синглетонов и т. Д. Я повторяю, синглтоны - это зло.
deft_code
@Tetrad, очень согласен. Это один из лучших навыков, которые вы можете иметь.
ZorbaTHut