Jak uniknąć „menedżerów” w moim kodzie

26

Obecnie projektuję mój system Entity dla C ++ i mam wielu menedżerów. W moim projekcie mam te klasy, aby związać moją bibliotekę. Słyszałem wiele złych rzeczy, jeśli chodzi o zajęcia „menedżerskie”, być może nie nazywam odpowiednio moich zajęć. Nie mam jednak pojęcia, jak inaczej je nazwać.

Większość menedżerów w mojej bibliotece składa się z tych klas (choć nieco się różni):

  • Kontener - pojemnik na obiekty w menedżerze
  • Atrybuty - atrybuty obiektów w menedżerze

W nowym projekcie mojej biblioteki mam te konkretne klasy, aby związać moją bibliotekę.

  • ComponentManager - zarządza komponentami w systemie encji

    • ComponentContainer
    • ComponentAttributes
    • Scena * - odniesienie do Sceny (patrz poniżej)
  • SystemManager - zarządza systemami w systemie Entity

    • SystemContainer
    • Scena * - odniesienie do Sceny (patrz poniżej)
  • EntityManager - zarządza jednostkami w Systemie Entity

    • EntityPool - pula encji
    • EntityAttributes - atrybuty bytu (będzie to dostępne tylko dla klas ComponentContainer i System)
    • Scena * - odniesienie do Sceny (patrz poniżej)
  • Scena - łączy wszystkich menedżerów razem

    • ComponentManager
    • Menadżer systemu
    • EntityManager

Myślałem o umieszczeniu wszystkich kontenerów / basenów w samej klasie Scene.

to znaczy

Zamiast tego:

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();

Byłoby to:

Scene scene; // create a Scene

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

Ale nie jestem pewien. Gdybym miał zrobić to drugie, oznaczałoby to, że miałbym wiele obiektów i metod zadeklarowanych w mojej klasie Scene.

UWAGI:

  1. System encji to po prostu projekt wykorzystywany w grach. Składa się z 3 głównych części: komponentów, bytów i systemów. Składniki to po prostu dane, które mogą być „dodawane” do bytów, aby były one odróżniające. Obiekt jest reprezentowany przez liczbę całkowitą. Systemy zawierają logikę jednostki z określonymi komponentami.
  2. Powodem, dla którego zmieniam projekt mojej biblioteki, jest to, że myślę, że można ją bardzo zmienić, w tej chwili nie lubię tego stylu.
miguel.martin
źródło
Czy mógłbyś wyjaśnić złe rzeczy, które słyszałeś o menedżerach i jak odnoszą się one do ciebie?
MrFox
@MrFox Zasadniczo słyszałem o tym, o czym wspomniał Dunk, i mam w swojej bibliotece wiele * klas menedżerskich, których chcę się pozbyć.
miguel.martin
Może to również być pomocne: medium.com/@wrong.about/…
Zapadlo,
Rozróżniasz użyteczną strukturę z działającej aplikacji. Prawie niemożliwe jest napisanie przydatnej struktury dla wyimaginowanych aplikacji.
kevin cline

Odpowiedzi:

19

DeadMG zajmuje się specyfiką twojego kodu, ale wydaje mi się, że brakuje mu wyjaśnienia. Nie zgadzam się również z niektórymi jego zaleceniami, które nie mają zastosowania w określonych kontekstach, takich jak na przykład tworzenie gier wideo o wysokiej wydajności. Ale na całym świecie ma rację, że większość twojego kodu nie jest teraz przydatna.

Jak mówi Dunk, klasy menedżerów są tak nazywane, ponieważ „zarządzają” rzeczami. „zarządzanie” jest jak „dane” lub „zrób”, to abstrakcyjne słowo, które może zawierać prawie wszystko.

Około 7 lat temu byłem głównie w tym samym miejscu, co ty, i zacząłem myśleć, że w moim sposobie myślenia było coś nie tak, ponieważ tyle wysiłku włożono w kodowanie, aby nic nie zrobić.

To, co zmieniłem, aby to naprawić, to zmiana słownictwa używanego w kodzie. Całkowicie unikam słów ogólnych (chyba że jest to ogólny kod, ale jest to rzadkie, gdy nie tworzysz bibliotek ogólnych). I unikać, aby wymienić każdy rodzaj „Manager” lub „obiekt”.

Wpływ jest bezpośredni w kodzie: zmusza cię do znalezienia odpowiedniego słowa odpowiadającego rzeczywistej odpowiedzialności twojego typu. Jeśli uważasz, że ten typ ma kilka rzeczy (prowadź indeks książek, utrzymuj je przy życiu, twórz / niszcz książki), potrzebujesz różnych typów, z których każdy będzie miał jedną odpowiedzialność, a następnie połączysz je w kodzie, który ich używa. Czasami potrzebuję fabryki, a czasem nie. Czasami potrzebuję rejestru, więc konfiguruję go (używając standardowych kontenerów i inteligentnych wskaźników). Czasami potrzebuję systemu, który składa się z kilku różnych podsystemów, więc odseparowuję wszystko tak bardzo, jak tylko mogę, aby każda część zrobiła coś pożytecznego.

Nigdy nie nazywaj typu „menedżerem” i upewnij się, że wszystkie typy mają jedną unikalną rolę, to moja rekomendacja. Czasami może być trudno znaleźć nazwiska, ale jest to jedna z najtrudniejszych rzeczy do zrobienia w programowaniu

Klaim
źródło
Jeśli kilka dynamic_castosób zabija Twoją wydajność, użyj systemu typu statycznego - po to jest. To naprawdę wyspecjalizowany kontekst, a nie ogólny, aby stworzyć własne RTTI, tak jak zrobił to LLVM. Nawet wtedy nie robią tego.
DeadMG
@DeadMG Nie mówiłem w szczególności o tej części, ale większość wysokowydajnych gier nie może sobie pozwolić na, na przykład, dynamiczne alokacje dzięki nowym lub nawet innym rzeczom, które są ogólnie przydatne w programowaniu. Są to specyficzne bestie dla konkretnego sprzętu, którego nie można uogólnić, dlatego „to zależy” jest bardziej ogólną odpowiedzią, szczególnie w przypadku C ++. (Nawiasem mówiąc, nikt w grze nie używa dynamicznej obsady, nigdy nie jest użyteczny, dopóki nie masz wtyczek, a nawet wtedy jest to rzadkie).
Klaim
@DeadMG Nie używam dynamic_cast, ani nie używam alternatywy dla dynamic_cast. Zaimplementowałem to w bibliotece, ale nie przeszkadzało mi to. template <typename T> class Class { ... };służy do przypisywania identyfikatorów różnym klasom (mianowicie niestandardowym komponentom i niestandardowym systemom). Przypisanie identyfikatora służy do przechowywania komponentów / systemów w wektorze, ponieważ mapa może nie zapewniać wydajności, której potrzebuję do gry.
miguel.martin
Cóż, właściwie nie wdrożyłem alternatywy dla dynamic_cast, tylko fałszywy typ_id. Ale nadal nie chciałem, co osiągnął typ_id.
miguel.martin
„znajdź właściwe słowo odpowiadające rzeczywistej odpowiedzialności twojego typu” - chociaż całkowicie się z tym zgadzam, często nie ma ani jednego słowa (jeśli jakieś istnieje), które społeczność programistów zgodziła się na określony rodzaj odpowiedzialności.
bytedev
23

Cóż, przeczytałem część kodu, do którego linkujesz, i twój post, a moje szczere podsumowanie jest takie, że większość z nich jest w zasadzie całkowicie bezwartościowa. Przepraszam. To znaczy, masz cały ten kod, ale niczego nie osiągnąłeś . W ogóle. Będę musiał tu zagłębić się, więc proszę o wyrozumiałość.

Zacznijmy od ObjectFactory . Po pierwsze, wskaźniki funkcji. Są bezstanowe, jedynym przydatnym bezstanowym sposobem na stworzenie obiektu jest newi nie potrzebujesz do tego fabryki. Stanowe tworzenie obiektu jest przydatne, a wskaźniki funkcji nie są przydatne do tego zadania. A po drugie, każdy obiekt musi wiedzieć, w jaki sposób, aby zniszczyć sobie , nie mając do stwierdzenia, że dokładna tworząc instancję, aby je zniszczyć. Ponadto należy zwrócić inteligentny wskaźnik, a nie surowy wskaźnik, dla wyjątku i bezpieczeństwa zasobów użytkownika. Twoja cała klasa ClassRegistryData jest sprawiedliwa std::function<std::unique_ptr<Base, std::function<void(Base*)>>()>, ale z wyżej wspomnianymi dziurami. To wcale nie musi istnieć.

Następnie mamy AllocatorFunctor - są już dostępne standardowe interfejsy alokatora - Nie wspominając, że są one tylko opakowaniami delete obj;i return new T;- które znowu są już dostarczone i nie będą oddziaływać z żadnymi stanowymi lub niestandardowymi alokatorami pamięci, które istnieją.

A ClassRegistry jest po prostu, std::map<std::string, std::function<std::unique_ptr<Base, std::function<void(Base*)>>()>>ale napisałeś dla niej klasę zamiast korzystać z istniejącej funkcjonalności. To ten sam, ale całkowicie niepotrzebnie globalny, zmienny stan z garścią gównianych makr.

Mówię tutaj o tym, że stworzyłeś pewne klasy, w których możesz zastąpić całość funkcjonalnością zbudowaną z klas Standardowych, a następnie pogorszysz je, czyniąc je globalnym stanem zmiennym i angażując kilka makr, które są najgorsze, co możesz zrobić.

Następnie mamy możliwość zidentyfikowania . Jest to bardzo podobna historia - std::type_infojuż teraz wykonuje zadanie identyfikacji każdego typu jako wartości czasu działania i nie wymaga nadmiernej płyty kotłowej ze strony użytkownika.

    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));
    }

Problem rozwiązany, ale bez żadnej z dwóch poprzednich linii makr i tak dalej. Oznacza to również, że twoja IdentifiableArray jest niczym, ale std::vectormusisz użyć liczenia referencji, nawet jeśli unikatowa własność lub brak własności byłyby lepsze.

Aha, i czy wspomniałem, że Typy zawierają mnóstwo absolutnie bezużytecznych typedefów? Dosłownie nie zyskujesz żadnej przewagi nad bezpośrednim użyciem typów surowych i tracisz dużą część czytelności.

Następny jest ComponentContainer . Nie możesz wybrać własności, nie możesz mieć oddzielnych kontenerów dla pochodnych klas różnych komponentów, nie możesz używać własnej semantyki. Usuń bez wyrzutów sumienia.

Teraz ComponentFilter może być trochę wart, jeśli bardzo go zrestrukturyzowałeś. Coś jak

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();
    }
};

Nie dotyczy to klas wywodzących się z tych, z którymi próbujesz sobie poradzić, ale twoje też nie.

Reszta to właściwie ta sama historia. Nie ma tu nic, czego nie można byłoby zrobić znacznie lepiej bez korzystania z biblioteki.

Jak uniknąłbyś menedżerów w tym kodzie? Usuń zasadniczo wszystko.

DeadMG
źródło
14
Zajęło mi to trochę czasu, ale najbardziej niezawodnych i wolnych od błędów fragmentów kodu nie ma .
Tacroy
1
Powodem, dla którego nie użyłem std :: type_info jest to, że starałem się unikać RTTI. Wspomniałem, że framework był przeznaczony do gier, co jest zasadniczo powodem, dla którego nie używam typeidlub dynamic_cast. Dziękuję za opinie, doceniam to.
miguel.martin
Czy krytycy opierają się na twojej wiedzy o silnikach gier systemowych z komponentami, czy też jest to ogólna recenzja kodu C ++?
Den
1
@Den Myślę, że bardziej problem projektowy niż cokolwiek innego.
miguel.martin
10

Problem z klasami Menedżera polega na tym, że menedżer może zrobić wszystko. Nie możesz odczytać nazwy klasy i wiedzieć, co robi klasa. EntityManager .... Wiem, że robi coś z Entami, ale kto wie co. SystemManager ... tak, zarządza systemem. To bardzo pomocne.

Domyślam się, że twoje zajęcia menedżerskie prawdopodobnie robią wiele rzeczy. Założę się, że jeśli podzielisz te rzeczy na ściśle powiązane elementy funkcjonalne, prawdopodobnie będziesz w stanie wymyślić nazwy klas związane z elementami funkcjonalnymi, aby każdy mógł powiedzieć, co robią, po prostu patrząc na nazwę klasy. To znacznie ułatwi utrzymanie i zrozumienie projektu.

Maczać
źródło
1
+1 i nie tylko mogą zrobić cokolwiek, na wystarczająco długiej linii czasu zrobią to. Ludzie postrzegają deskryptor „Managera” jako zaproszenie do tworzenia klas zlewozmywaka kuchennego / szwajcarskiej armii-noża.
Erik Dietrich,
@Erik - Ach tak, powinienem dodać, że dobra nazwa klasy jest ważna nie tylko dlatego, że mówi komuś, co klasa może zrobić; ale nazwa klasy mówi również, czego klasa nie powinna robić.
Dunk
3

Nie sądzę, Managerżeby w tym kontekście było tak źle, wzruszając ramionami. Jeśli jest jedno miejsce, w którym moim zdaniem Managermożna wybaczyć, byłoby tak w przypadku systemu podmiot-komponent, ponieważ tak naprawdę „ zarządza ” żywotnością komponentów, bytów i systemów. Przynajmniej jest to tutaj bardziej znacząca nazwa, powiedzmy, Factoryco nie ma w tym kontekście tyle sensu, ponieważ ECS naprawdę robi coś więcej niż tylko tworzy rzeczy.

Przypuszczam, że można użyć Database, jak ComponentDatabase. Lub możesz po prostu użyć Components, Systemsi Entities(tego właśnie używam osobiście, głównie dlatego, że lubię zwięzłe identyfikatory, chociaż co prawda przekazuje jeszcze mniej informacji, być może, niż „Menedżer”).

Jedna część, którą uważam za nieco niezdarną, to:

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

To jest wiele rzeczy do zrobienia, getzanim będziesz mógł utworzyć encję. Wygląda na to, że projekt nieco wycieka szczegóły implementacji, jeśli musisz wiedzieć o pulach encji i „menedżerach”, aby je utworzyć. Myślę, że rzeczy takie jak „pule” i „menedżerowie” (jeśli trzymasz się tej nazwy) powinny być szczegółami implementacyjnymi ECS, a nie czymś narażonym na klienta.

Ale wiem, widziałem kilka bardzo niewyszukane i ogólnych zastosowań Manager, Handler, Adapteroraz nazwy tego rodzaju, ale myślę, że jest to stosunkowo wybaczalne przypadek użycia, gdy coś, co nazywa „Manager” jest rzeczywiście odpowiedzialny za „zarządzanie” ( "kontroli? „,” obchodzenie się? ”) żywotności przedmiotów, odpowiadanie za tworzenie i niszczenie oraz zapewnianie dostępu do nich indywidualnie. To jest dla mnie najbardziej proste i intuicyjne użycie „Managera”.

To powiedziawszy, jedną ogólną strategią unikania „Managera” w twoim imieniu jest po prostu wyjęcie części „Managera” i dodanie -sna końcu. :-D Załóżmy na przykład, że masz ThreadManageri jest on odpowiedzialny za tworzenie wątków i dostarczanie informacji o nich oraz uzyskiwanie do nich dostępu i tak dalej, ale nie łączy puli wątków, więc nie możemy tego nazwać ThreadPool. W takim razie po prostu to nazywaj Threads. I tak właśnie robię, kiedy nie mam wyobraźni.

Przypominają mi się czasy, gdy analogiczny odpowiednik „Eksploratora Windows” nazywał się „Menedżerem plików”, tak jak:

wprowadź opis zdjęcia tutaj

I tak naprawdę uważam, że „Menedżer plików” w tym przypadku jest bardziej opisowy niż „Eksplorator Windows”, nawet jeśli istnieje lepsza nazwa, która nie obejmuje „Menedżera”. Więc przynajmniej nie bądź zbyt fantazyjny ze swoimi nazwiskami i zacznij nazywać takie rzeczy jak „ComponentExplorer”. „ComponentManager” pozwala mi przynajmniej zrozumieć, co robi ta osoba, gdy ktoś przywykł do pracy z silnikami ECS.


źródło
Zgoda. Jeśli klasa jest używana do typowych obowiązków kierowniczych (tworzenie („zatrudnianie”), niszczenie („zwalnianie”), nadzór i interfejs „pracownik” / wyższy poziom, nazwa Managerjest odpowiednia. Klasa może mieć problemy z projektowaniem , ale nazwa jest na miejscu
Justin Time 2 Przywróć Monikę