Виртуальные обработчики событий из нескольких классов: множественное наследование или композиция?
Моя команда написала несколько классов C++, которые реализуют обработку событий через чисто виртуальные обратные вызовы - например, когда сообщение получено от другого процесса, базовый класс, который обрабатывает обмен сообщениями IPC, вызывает свою собственную чисто виртуальную функцию, а производный класс обрабатывает событие в переопределение этой функции. Базовый класс знает, что событие произошло; производный класс знает, что с ним делать.
Теперь я хочу объединить функции, предоставляемые этими базовыми классами, в класс более высокого уровня, поэтому, например, когда сообщение приходит из другого процесса, мой новый класс может затем переслать его по сетевому соединению, используя аналогичный управляемый событиями сетевой класс., Похоже, у меня есть два варианта:
(1) состав: производные классы от каждого из базовых классов обработки событий и добавление объектов этих производных классов в мой новый класс в качестве членов, или:
(2) множественное наследование: сделать мой новый класс производным от всех базовых классов для обработки событий.
Я пробовал оба (1) и (2), и я не удовлетворен моей реализацией либо.
Есть дополнительная сложность: некоторые базовые классы были написаны с использованием методов инициализации и завершения вместо использования конструкторов и деструкторов, и, конечно, эти методы имеют одинаковые имена в каждом классе. Таким образом, множественное наследование вызывает неоднозначность имени функции. Разрешимый с using
декларации и / или явная область видимости, но не самая удобная в обращении вещь, которую я когда-либо видел.
Даже без этой проблемы, использование множественного наследования и переопределение каждой чистой виртуальной функции из каждого из нескольких базовых классов сделает мой новый класс очень большим, граничащим с "God Object"-ness. Поскольку требования изменяются (читай: "по мере добавления требований"), это не будет хорошо масштабироваться.
С другой стороны, использование отдельных производных классов и добавление их в качестве членов моего нового класса означает, что мне нужно написать множество методов для каждого производного класса для обмена информацией между ними. Это очень похоже на "геттеры и сеттеры" - не так уж плохо, но есть много "получить эту информацию из этого класса и передать ее этому", что выглядит неэффективно - много дополнительных методов, много дополнительных операций чтения и записи, и классы должны много знать о логике друг друга, что кажется неправильным. Я думаю, что полноценная модель публикации и подписки была бы излишней, но я пока не нашел простой альтернативы.
Там также много дублирования данных, если я использую композицию. Например, если состояние моего класса зависит от того, работает ли его сетевое соединение, я должен либо иметь флаг состояния в каждом классе, затронутом этим, либо каждый класс должен запрашивать у сетевого класса свое состояние каждый раз, когда решение требует быть сделано. Если бы у меня был только один класс с множественным наследованием, я мог бы просто использовать флаг, к которому мог бы получить доступ любой код в моем классе.
Итак, множественное наследование, композиция или, может быть, что-то еще целиком? Существует ли общее эмпирическое правило о том, как лучше всего подойти к такого рода вещам?
2 ответа
Из вашего описания я думаю, что вы выбрали подход стиля "шаблонный метод", при котором база работает, а затем вызывает чистый виртуальный объект, который реализует производный класс, а не подход "интерфейс обратного вызова", который почти такой же, за исключением того, что Чисто виртуальный метод находится на совершенно отдельном интерфейсе, который передается в "базу" в качестве параметра для конструктора. Лично я предпочитаю последнее, так как считаю, что оно гораздо более гибкое, когда приходит время объединять объекты и создавать объекты более высокого уровня.
Я склонен идти на композицию с классом composing, реализующим интерфейсы обратного вызова, которые требуются для составных объектов, и затем потенциально снова сочинять в похожем стиле на более высоком уровне.
Затем вы можете решить, уместно ли сочинять, если составляющий объект реализует интерфейсы обратного вызова и передает их "составленным" объектам в их конструкторах, ИЛИ вы можете реализовать интерфейс обратного вызова в своем собственном объекте, возможно, с более простым и точным обратным вызовом. интерфейс, который реализует ваш создающий объект, и создает "базовый объект" и "объект реализации обратного вызова"...
Лично я бы не стал использовать интерфейс "абстрактной обработки событий", так как предпочел бы, чтобы мой код был явным и понятным, даже если это приводит к тому, что он немного менее универсален.
Я не совсем понимаю, чего пытается достичь ваш новый класс, но похоже, что вы фактически должны предоставить новую реализацию где-то для всех этих абстрактных классов событий.
Лично я бы пухл по составу. Множественное наследование быстро становится кошмаром, особенно когда все должно измениться, а композиция сохраняет существующее разделение интересов.
Вы заявляете, что каждый производный объект должен будет взаимодействовать с сетевым классом, но можете ли вы попытаться свести это к минимуму. Например, каждый производный объект события несет полную ответственность за упаковку информации о событии в некоторый типовой пакет, а затем этот пакет передается в сетевой класс для выполнения функции отправки?
Не зная точно, что делает ваш новый класс, трудно комментировать или предлагать лучшие шаблоны, но чем больше я кодирую, тем больше я учусь соглашаться со старой поговоркой "предпочтение композиции над наследованием"