Что является хорошим способом представить отношения многих ко многим между двумя классами?

10

Допустим, у меня есть два типа объектов, A и B. Отношения между ними многие-ко-многим, но ни один из них не является владельцем другого.

И экземпляры A и B должны знать о соединении; это не только один путь.

Итак, мы можем сделать это:

class A
{
    ...

    private: std::vector<B *> Bs;
}

class B
{
    private: std::vector<A *> As;
}

У меня вопрос: куда мне поместить функции для создания и уничтожения соединений?

Должно ли это быть A :: Attach (B), которое затем обновляет векторы A :: Bs и B :: As?

Или это должно быть B :: Attach (A), что кажется одинаково разумным.

Ни один из тех, кто чувствует себя хорошо. Если я перестану работать с кодом и вернусь через неделю, я уверен, что не смогу вспомнить, нужно ли мне делать A.Attach (B) или B.Attach (A).

Возможно, это должна быть такая функция:

CreateConnection(A, B);

Но создание глобальной функции также нежелательно, учитывая, что эта функция предназначена специально для работы только с классами A и B.

Другой вопрос: если я часто сталкиваюсь с этой проблемой / требованием, могу ли я как-то найти общее решение? Возможно, класс TwoWayConnection, который я могу извлечь или использовать в классах, которые разделяют этот тип отношений?

Каковы некоторые хорошие способы справиться с этой ситуацией ... Я знаю, как справиться с ситуацией «один-ко-многим» владеет D), но этот хитрее.

Изменить: просто чтобы сделать его более явным, этот вопрос не включает в себя вопросы собственности. И A, и B принадлежат некоторому другому объекту Z, а Z решает все вопросы, связанные с владением. Меня интересует только то, как создать / удалить связи «многие ко многим» между А и В.

Дмитрий Шуралев
источник
Я бы создал Z :: Attach (A, B), поэтому, если Z владеет двумя типами объектов, ему кажется, что он создает соединение. В моем (не очень хорошем) примере Z будет репозиторием для A и B. Я не вижу другого пути без практического примера.
Мачадо
1
Чтобы быть более точным, A и B могут иметь разных владельцев. Возможно, A принадлежит Z, тогда как B принадлежит X, который, в свою очередь, принадлежит Z. Как вы можете видеть, он становится действительно запутанным, пытаясь управлять ссылкой в ​​другом месте. A и B должны иметь возможность быстрого доступа к списку пар, с которыми он связан.
Дмитрий Шуралев
Если вам интересно узнать настоящие имена А и Б в моей ситуации, они Pointerи есть GestureRecognizer. Указатели принадлежат и управляются классом InputManager. GestureRecognizer принадлежат экземплярам Widget, которые в свою очередь принадлежат экземпляру Screen, который принадлежит экземпляру приложения. Указатели присваиваются GestureRecognizer, чтобы они могли передавать им необработанные входные данные, но GestureRecognizers должны знать, сколько указателей в настоящее время связано с ними (чтобы различать жесты 1 палец против 2 пальцев и т. Д.).
Дмитрий Шуралев
Теперь, когда я больше об этом думаю, мне кажется, что я просто пытаюсь сделать распознавание жестов на основе фреймов (а не указателей). Так что я мог бы также просто передавать события жестам покадрово, а не указателю за разом. Хм.
Дмитрий Шуралев

Ответы:

2

Одним из способов является добавление открытого Attach()метода, а также защищенного AttachWithoutReciprocating()метода для каждого класса. Заведите Aи Bобщих друзей, чтобы их Attach()методы могли вызывать другие AttachWithoutReciprocating():

A::Attach(B &b) {
    Bs.push_back(&b);
    b.AttachWithoutReciprocating(*this);
}

A::AttachWithoutReciprocating(B &b) {
    Bs.push_back(&b);
}

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

Я уверен, что вы могли бы обернуть это поведение в MutuallyAttachableклассе, который унаследован Aи Bунаследован, что позволит вам избежать повторения и набирать бонусные очки в Судный день. Но даже простой подход «реализуй это в обоих местах» сделает работу.

Калеб
источник
1
Это звучит точно так же, как решение, о котором я думал в глубине души (то есть поместить Attach () в A и B). Мне также действительно нравится более СУХОЕ решение (мне очень не нравится дублирование кода), в котором есть класс MutuallyAttachable, который можно извлекать из любого случая, когда это необходимо (я предвижу довольно часто). Просто увидев то же самое решение, предложенное кем-то другим, я чувствую себя гораздо увереннее в этом, спасибо!
Дмитрий Шуралев
Принимая это, потому что IMO - это лучшее решение, предлагаемое до сих пор. Спасибо! Я собираюсь реализовать этот класс MutuallyAttachable сейчас и сообщать здесь, если у меня возникнут какие-либо непредвиденные проблемы (некоторые трудности могут также возникнуть из-за множественного наследования, с которым у меня пока нет особого опыта).
Дмитрий Шуралев
Все сложилось гладко. Однако, в соответствии с моим последним комментарием к исходному вопросу, я начинаю понимать, что мне не нужны эти функции для того, что я изначально предполагал. Вместо того, чтобы отслеживать эти двусторонние соединения, я просто передам каждую пару в качестве параметра функции, которая нуждается в этом. Тем не менее, это может пригодиться позже.
Дмитрий Шуралев
Вот MutuallyAttachableкласс, который я написал для этой задачи, если кто-то хочет использовать его повторно: goo.gl/VY9RB (Pastebin) Редактировать: Этот код можно улучшить, сделав его шаблоном класса.
Дмитрий Шуралев
1
Я разместил MutuallyAttachable<T, U>шаблон класса в Gist. gist.github.com/3308058
Дмитрий Шуралев
5

Возможно ли, чтобы сами отношения имели дополнительные свойства?

Если так, то это должен быть отдельный класс.

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

Стивен А. Лоу
источник
Если это отдельный класс, как A узнает связанные экземпляры B, а B узнает связанные экземпляры A? В этот момент и А, и В должны были бы иметь отношение 1-ко-многим с С, не так ли?
Дмитрий Шуралев
1
A и B оба будут нуждаться в ссылке на коллекцию экземпляров C; C служит той же цели, что и «таблица-мост» в реляционном моделировании
Стивен А. Лоу,
1

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

Одним из кандидатов на перемещение ассоциации является код, который создает ассоциацию в первую очередь. Может быть, вместо того, чтобы звонить, attach()он просто хранит список std::pair<A, B>. Если есть несколько мест, создающих ассоциации, его можно абстрагировать в один класс.

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

Другим распространенным кандидатом на перенос ассоциации является код, который часто вызывает методы Aили Bобъекты. Например, вместо вызова a->foo(), который внутренне что-то делает со всеми связанными Bобъектами, он уже знает ассоциации и вызывает a->foo(b)для каждого. Опять же, если его называют несколько мест, его можно абстрагировать в один класс.

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

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

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

Карл Билефельдт
источник
0

Если бы я реализовывал это, я бы поместил Attach как в A, так и в B. Внутри метода Attach я бы вызвал Attach для переданного объекта. Таким образом, вы можете вызвать либо A.Attach (B), либо B.Attach ( А). Мой синтаксис может быть неправильным, я не использовал c ++ годами:

class A {
  private: std::vector<B *> Bs;

  void Attach (B* obj) {
    // Only add obj if it's not in the vector
    if (std::find(Bs.begin(), Bs.end(), obj) == Bs.end()) {
      //Add obj to the vector
      Bs.push_back(obj);

      // Attach this class to obj
      obj->Attach(this);
    }
  }    
}

class B {
  private: std::vector<A *> As;

  void Attach (A* obj) {
    // Only add obj if it's not in the vector
    if (std::find(As.begin(), As.end(), obj) == As.end()) {
      //Add obj to the vector
      As.push_back(obj);

      // Attach this class to obj
      obj->Attach(this);
    }
  }    
}

Альтернативным методом было бы создание 3-го класса, C, который управляет статическим списком пар AB.

briddums
источник
Просто вопрос относительно вашего альтернативного предложения о наличии третьего класса C для управления списком пар AB: как A и B узнают своих членов пары? Нужно ли каждому A и B ссылка на (один) C, а затем попросить C найти подходящие пары? B :: Foo () {std :: vector <A *> As = C.GetAs (this); / * Сделай что-нибудь с As ... * /} Или ты предполагал что-то еще? Потому что это кажется ужасно запутанным.
Дмитрий Шуралев