Как избежать «менеджеров» в моем коде

26

В настоящее время я перепроектирую свою Entity System для C ++, и у меня много менеджеров. В моем дизайне у меня есть эти классы, чтобы связать мою библиотеку. Я слышал много плохих вещей, когда речь шла о классах «менеджера», возможно, я не правильно назвал свои классы. Однако я понятия не имею, как еще их назвать.

Большинство менеджеров в моей библиотеке состоят из этих классов (хотя они немного различаются):

  • Контейнер - контейнер для объектов в менеджере
  • Атрибуты - атрибуты для объектов в менеджере

В моем новом дизайне для моей библиотеки у меня есть эти конкретные классы, чтобы связать мою библиотеку вместе.

  • ComponentManager - управляет компонентами в Entity System

    • ComponentContainer
    • ComponentAttributes
    • Сцена * - ссылка на сцену (см. Ниже)
  • SystemManager - управляет системами в Entity System

    • SystemContainer
    • Сцена * - ссылка на сцену (см. Ниже)
  • EntityManager - управляет объектами в Entity System

    • EntityPool - пул сущностей
    • EntityAttributes - атрибуты сущности (это будет доступно только для классов ComponentContainer и System)
    • Сцена * - ссылка на сцену (см. Ниже)
  • Сцена - связывает всех менеджеров вместе

    • ComponentManager
    • SystemManager
    • EntityManager

Я думал просто поместить все контейнеры / пулы в сам класс Scene.

т.е.

Вместо этого:

Scene scene; // create a Scene

// NOTE:
// I technically could wrap this line in a createEntity() call in the Scene class
Entity entity = scene.getEntityManager().getPool().create();

Было бы так:

Scene scene; // create a Scene

Entity entity = scene.getEntityPool().create();

Но я не уверен. Если бы я делал последнее, это означало бы, что у меня было бы много объектов и методов, объявленных внутри моего класса Scene.

ЗАМЕТКИ:

  1. Система сущностей - это просто дизайн, который используется для игр. Он состоит из 3 основных частей: компонентов, объектов и систем. Компоненты - это просто данные, которые могут быть «добавлены» к объектам, чтобы объекты были различимыми. Сущность представлена ​​целым числом. Системы содержат логику для сущности с конкретными компонентами.
  2. Причина, по которой я меняю свой дизайн для своей библиотеки, заключается в том, что я думаю, что он может быть изменен довольно сильно, мне не нравится ощущение / поток к нему в данный момент.
miguel.martin
источник
Не могли бы вы рассказать о плохих вещах, которые вы слышали о менеджерах и как они относятся к вам?
MrFox
@MrFox В основном я слышал, что упомянул Данк, и у меня есть много классов * Manager в моей библиотеке, от которых я хочу избавиться.
miguel.martin
Это может быть полезным , а также: medium.com/@wrong.about/...
Zapadlo
Вы выделяете полезную основу из рабочего приложения. Почти невозможно написать полезную среду для воображаемых приложений.
Кевин Клайн

Ответы:

19

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

Как говорит Данк, классы Manager называются так, потому что они «управляют» вещами. «управлять» - это как «данные» или «делать», это абстрактное слово, которое может содержать почти все.

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

Что я изменил, чтобы исправить это, так это изменить словарный запас, который я использую в коде. Я полностью избегаю универсальных слов (если это не универсальный код, но редко, когда вы не создаете универсальные библиотеки). Я избегаю называть любой тип «Менеджер» или «Объект».

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

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

Klaim
источник
Если пара dynamic_castубивает вашу производительность, тогда используйте статическую систему типов - вот для чего она. Это действительно специализированный, а не общий контекст, когда нужно свернуть свой RTTI, как это сделал LLVM. Даже тогда они этого не делают.
DeadMG
@DeadMG Я не говорил об этой части в частности, но большинство высокопроизводительных игр не могут позволить себе, например, динамическое распределение с помощью новых или даже других вещей, которые хороши для программирования в целом. Это конкретные звери для конкретного оборудования, которые вы не можете обобщить, поэтому «это зависит» - более общий ответ, в частности, в C ++. (Кстати, никто из разработчиков игр не использует динамическое приведение, это бесполезно, пока у вас нет плагинов, и даже тогда это редко).
Klaim
@DeadMG Я не использую dynamic_cast и не использую альтернативу dynamic_cast. Я реализовал это в библиотеке, но я не мог возиться с этим. template <typename T> class Class { ... };на самом деле для назначения идентификаторов для различных классов (а именно, пользовательских компонентов и пользовательских систем). Назначение идентификатора используется для хранения компонентов / систем в векторе, поскольку карта может не обеспечивать производительность, необходимую для игры.
miguel.martin
Ну, на самом деле я также не реализовал альтернативу dynamic_cast, просто поддельный type_id. Но все же я не хотел, чтобы type_id был достигнут.
miguel.martin
«найдите правильное слово, соответствующее реальной ответственности вашего типа» - хотя я полностью с этим согласен, часто нет ни единого слова (если таковое имеется), с которым сообщество разработчиков согласилось бы для определенного типа ответственности.
bytedev
23

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

Давайте начнем с ObjectFactory . Во-первых, указатели на функции. Это без сохранения состояния, единственный полезный способ создания объекта без сохранения состояния - newи вам не нужна фабрика для этого. Создание объекта - это то, что полезно, а указатели функций для этой задачи не нужны. И, во-вторых, каждый объект должен знать, как уничтожить себя , а не искать точный создатель для его уничтожения. Кроме того, вы должны возвращать умный указатель, а не необработанный указатель, для исключения и безопасности ресурсов вашего пользователя. Весь ваш класс ClassRegistryData просто std::function<std::unique_ptr<Base, std::function<void(Base*)>>()>, но с вышеупомянутыми дырами. Это не должно существовать вообще.

Затем у нас есть AllocatorFunctor - уже есть стандартные интерфейсы распределителя - не говоря уже о том, что они являются просто оболочками delete obj;и return new T;- что опять-таки уже предоставлено, и они не будут взаимодействовать с какими-либо существующими или пользовательскими распределителями памяти, которые существуют.

И ClassRegistry - это просто, std::map<std::string, std::function<std::unique_ptr<Base, std::function<void(Base*)>>()>>но вы написали класс для него вместо использования существующей функциональности. Это то же самое, но совершенно ненужное глобальное изменяемое состояние с кучей дурацких макросов.

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

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

    template<typename T, typename Y>
    bool isSameType(const X& x, const Y& y) { return typeid(x) == typeid(y); }
    template <class Type, class T>
    bool isType(const T& obj)
    {
            return dynamic_cast<const Type*>(std::addressof(obj));
    }

Проблема решена, но без каких-либо двухсот предыдущих макросов и прочего. Это также помечает ваш IdentifiableArray как не что иное, как std::vectorвы, но вы должны использовать подсчет ссылок, даже если было бы лучше уникальное владение или отсутствие владения.

О, и я упоминал, что Types содержит кучу абсолютно бесполезных typedefs? Вы буквально не получаете никаких преимуществ по сравнению с непосредственным использованием необработанных типов и теряете значительную часть читабельности.

Далее идет ComponentContainer . Вы не можете выбрать владельца, у вас не может быть отдельных контейнеров для производных классов различных компонентов, вы не можете использовать собственную семантику времени жизни. Удалить без раскаяния.

Теперь, ComponentFilter может стоить немного, если вы много рефакторинг. Что-то типа

class ComponentFilter {
    std::unordered_map<std::type_info, unsigned> types;
public:
    template<typename T> void SetTypeRequirement(unsigned count = 1) {
        types[typeid(T)] = count;
    }
    void clear() { types.clear(); }
    template<typename Iterator> bool matches(Iterator begin, Iterator end) {
        std::for_each(begin, end, [this](const std::iterator_traits<Iterator>::value_type& t) {
            types[typeid(t)]--;
        });
        return types.empty();
    }
};

Это не касается классов, производных от тех, с которыми вы пытаетесь работать, но и ваших тоже.

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

Как бы вы избежали менеджеров в этом коде? Удалите в основном все это.

DeadMG
источник
14
Мне потребовалось некоторое время, чтобы выучить, но самые надежные, не содержащие ошибок фрагменты кода - это те, которых там нет .
Такро
1
Причина, по которой я не использовал std :: type_info, заключается в том, что я пытался избежать RTTI. Я упомянул рамки была направлена на игры, которые в основном причина , я не использую typeidили dynamic_cast. Спасибо за отзыв, я ценю это.
miguel.martin
Основана ли эта критика на ваших знаниях игровых движков системы компонент-сущность или это общий обзор кода C ++?
День
1
@ Я думаю больше о дизайне, чем о чем-либо еще.
miguel.martin
10

Проблема с классами Manager заключается в том, что менеджер может делать все что угодно. Вы не можете прочитать имя класса и знать, что делает класс. EntityManager .... Я знаю, что он что-то делает с сущностями, но кто знает, что. SystemManager ... да, он управляет системой. Это очень полезно.

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

Замочить
источник
1
+1, и они не только могут делать что-либо, на достаточно длительном графике. Люди рассматривают дескриптор «Менеджер» как приглашение к созданию классов «кухонная раковина / швейцарский армейский нож».
Эрик Дитрих
@Erik - Ах да, я должен был добавить, что хорошее имя класса важно не только потому, что оно говорит кому-то, что может делать класс; но в равной степени имя класса также говорит, что класс не должен делать.
Данк
3

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

Я полагаю, вы могли бы использовать Database, как ComponentDatabase. Или вы могли бы просто использовать Components, Systemsи Entities(это то, что я лично использую, и в основном только потому, что мне нравятся краткие идентификаторы, хотя, по общему признанию, они передают даже меньше информации, возможно, чем «Менеджер»).

Одна часть, которую я нахожу немного неуклюжей, это:

Entity entity = scene.getEntityManager().getPool().create();

Это очень много вещей, getпрежде чем вы сможете создать сущность, и кажется, что дизайн немного утечет детали реализации, если вам нужно знать о пулах сущностей и «менеджерах» для их создания. Я думаю, что такие вещи, как «пулы» и «менеджеры» (если вы придерживаетесь этого имени) должны быть деталями реализации ECS, а не чем-то, что доступно клиенту.

Но не знаю, я видел некоторые очень воображения и общие виды использования Manager, Handler, Adapterи названия такого рода, но я думаю , что это разумно простительно использовать случай , когда вещь под названием «Менеджер» фактически отвечает за «управление» ( "контроль? "," обработка? ") время жизни объектов, ответственность за создание и уничтожение и обеспечение доступа к ним по отдельности. Это самое простое и интуитивно понятное использование «Менеджера», по крайней мере, для меня.

Тем не менее, одна общая стратегия, чтобы избежать «Менеджер» на ваше имя, это просто взять часть «Менеджер» и добавить -sв конце. :-D Например, допустим, у вас есть ThreadManagerи он отвечает за создание потоков, предоставление информации о них, доступ к ним и т. Д., Но он не объединяет потоки, поэтому мы не можем его вызвать ThreadPool. Ну, в таком случае, просто позвони Threads. Это то, что я делаю в любом случае, когда я не воображаем.

Я немного напомнил, когда аналог «Windows Explorer» назывался «Диспетчер файлов», например:

введите описание изображения здесь

И я на самом деле думаю, что «Диспетчер файлов» в этом случае более описательный, чем «Проводник Windows», даже если есть какое-то более подходящее имя, которое не включает «Диспетчер». Поэтому, по крайней мере, пожалуйста, не переусердствуйте со своими именами и начните называть такие вещи, как "ComponentExplorer". «ComponentManager», по крайней мере, дает мне разумное представление о том, что эта штука делает, когда кто-то привык работать с двигателями ECS.


источник
Согласовано. Если класс используется для типичных управленческих обязанностей (создание («найм»), уничтожение («увольнение»), надзор и «сотрудник» / более высокий интерфейс), тогда имя Managerподходит. У класса могут быть проблемы с дизайном , но имя спот-.
Джастин Time 2 восстановило Моника