Czy złą praktyką jest umieszczanie wszystkich wyliczeń w jednym pliku i używanie ich w wielu klasach?

12

Jestem początkującym programistą gier, pracuję nad okazjonalnymi grami niezależnymi i od jakiegoś czasu robię coś, co na początku wydawało się złą praktyką, ale naprawdę chcę uzyskać odpowiedź od niektórych doświadczonych programistów tutaj.

Załóżmy, że mam plik o nazwie, w enumList.hktórej deklaruję wszystkie wyliczenia, których chcę używać w mojej grze:

// enumList.h

enum materials_t { WOOD, STONE, ETC };
enum entity_t { PLAYER, MONSTER };
enum map_t { 2D, 3D };
// and so on.

// Tile.h
#include "enumList.h"
#include <vector>

class tile
{
    // stuff
};

Główną ideą jest to, że deklaruję wszystkie wyliczenia w grze w 1 pliku , a następnie importuję ten plik, gdy muszę użyć określonego wyliczenia z niego, zamiast deklarować go w pliku, w którym muszę go użyć. Robię to, ponieważ sprawia, że ​​wszystko jest czyste, mogę uzyskać dostęp do każdego wyliczenia w jednym miejscu zamiast otwierać strony wyłącznie w celu uzyskania dostępu do jednego wyliczenia.

Czy to zła praktyka i czy może w jakikolwiek sposób wpłynąć na wydajność?

Bugster
źródło
1
Struktura źródła nie może wpływać na wydajność - nadal będzie kompilowana do tego samego, bez względu na to, gdzie znajdują się wyliczenia. Tak naprawdę jest to pytanie o to, gdzie najlepiej je umieścić, a dla małych wierszy jeden plik nie brzmi zbyt tabu.
Steffan Donal,
Pytałem w bardziej ekstremalnych przypadkach, że gra może mieć wiele wyliczeń i mogą być całkiem duże, ale dzięki za komentarz
Bugster,
4
Nie wpłynie to na wydajność aplikacji, ale będzie miało negatywny wpływ na czas kompilacji. Na przykład, jeśli dodasz materiał do materials_tplików, które nie zajmują się materiałami, będziesz musiał go odbudować.
Gort the Robot
14
to tak, jakby umieścić wszystkie krzesła w domu w pokoju z krzesłami, więc jeśli chcesz usiąść, wiesz, gdzie iść.
kevin cline
Tak na marginesie, można łatwo zrobić zarówno , umieszczając każdy enum w oddzielnym pliku i mający enumList.hsłużyć jako zbiór #includes dla tych plików. Pozwala to plikom, które potrzebują tylko jednego wyliczenia, aby uzyskać go bezpośrednio, zapewniając jednocześnie osobny pakiet dla wszystkiego, co naprawdę chce wszystkich.
Justin Time - Przywróć Monikę

Odpowiedzi:

35

Naprawdę uważam, że to zła praktyka. Kiedy mierzymy jakość kodu, coś nazywamy „szczegółowością”. Twoja ziarnistość poważnie cierpi, umieszczając wszystkie te wyliczenia w jednym pliku, a tym samym cierpi na łatwość konserwacji.

Miałem jeden plik na wyliczenie, aby szybko go znaleźć i pogrupować kodem behawioralnym określonej funkcji (np. Wyliczenie materiałów w folderze, w którym zachowano zachowanie materiału itp.);

Główną ideą jest to, że deklaruję wszystkie wyliczenia w grze w 1 pliku, a następnie importuję ten plik, gdy muszę użyć określonego wyliczenia z niego, zamiast deklarować go w pliku, w którym muszę go użyć. Robię to, ponieważ sprawia, że ​​wszystko jest czyste, mogę uzyskać dostęp do każdego wyliczenia w jednym miejscu zamiast otwierać strony wyłącznie w celu uzyskania dostępu do jednego wyliczenia.

Możesz myśleć, że jest czysty, ale tak naprawdę nie jest. Łączy rzeczy, które nie pasują do siebie pod względem funkcjonalności i modułów, i zmniejsza modułowość aplikacji. W zależności od wielkości bazy kodu i tego, jak modułowa ma być struktura, może to przekształcić się w większy problem i nieczytelny kod / zależności w innych częściach systemu. Jeśli jednak napiszesz mały, monolityczny system, nie musi to mieć zastosowania. Jednak nie zrobiłbym tego w ten sposób, nawet dla małego monolitycznego układu.

Sokół
źródło
2
+1 za wzmiankę o ziarnistości, wziąłem pod uwagę inne odpowiedzi, ale masz rację.
Bugster,
+1 za pojedynczy plik na wyliczenie. dla jednego łatwiej jest znaleźć.
Mauris
11
Nie wspominając o tym, że dodanie pojedynczej wartości wyliczenia używanej w jednym miejscu spowoduje, że każdy plik w całym projekcie będzie musiał zostać odbudowany.
Gort the Robot
dobra rada. i tak zmieniłeś zdanie .. Zawsze lubiłem chodzić do CompanyNamespace.Enums .... i otrzymywać łatwą listę, ale jeśli struktura kodu jest zdyscyplinowana, twoje podejście jest lepsze
Matt Evans
21

Tak, to zła praktyka, nie z powodu wydajności, ale z powodu łatwości konserwacji.

Sprawia, że ​​rzeczy są „czyste” tylko w OCD „zbierają podobne rzeczy razem”. Ale tak naprawdę nie jest to użyteczny i dobry rodzaj „czystości”.

Jednostki kodu należy pogrupować, aby zmaksymalizować spójność i zminimalizować sprzężenie , co najlepiej osiągnąć, grupując je w zasadniczo niezależne moduły funkcjonalne. Pogrupowanie ich według kryteriów technicznych (takich jak zebranie wszystkich wyliczeń) daje odwrotną sytuację - łączy kod, który nie ma absolutnie żadnej powiązania funkcjonalnego, i umieszcza wyliczenia, których można użyć tylko w jednym miejscu, w innym pliku.

Michael Borgwardt
źródło
3
Prawdziwe; „tylko w OCD„ zbieraj podobne rzeczy razem ”sposób. 100 głosów pozytywnych, gdybym mógł.
Dogweather
Pomógłbym edytować pisownię, gdybym mógł, ale nie popełniłeś wystarczającej liczby błędów, aby przekroczyć próg „edycji” SE. :-P
Dogweather
interesujące jest to, że prawie wszystkie frameworki internetowe wymagają gromadzenia plików według typu, a nie według funkcji.
kevin cline
@kevincline: Nie powiedziałbym, że „prawie wszyscy” - tylko te oparte na konwencjach nad konfiguracją, i zazwyczaj mają też modułową koncepcję, która umożliwia funkcjonalne grupowanie kodu.
Michael Borgwardt
8

Cóż, to tylko dane (a nie zachowanie). Najgorsze, co może / teoretycznie mogłoby się zdarzyć, to włączenie tego samego kodu i skompilowanie go więcej niż jeden raz, tworząc stosunkowo większy program.

Jest po prostu niemożliwe, aby takie włączenie mogło dodać więcej cykli do twoich operacji, biorąc pod uwagę, że nie ma w nich żadnego kodu behawioralnego / proceduralnego (bez pętli, bez ifs itp.).

(Relatywnie) większy program z trudem może wpływać na wydajność (szybkość wykonywania), a w każdym razie jest tylko odległym problemem teoretycznym. Większość (być może wszystkie) kompilatory zarządzają zawiera w sposób, który zapobiega takim problemom.

IMHO, zalety, które zyskujesz dzięki jednemu plikowi (bardziej czytelny i łatwiejszy do zarządzania kod) znacznie przekraczają wszelkie możliwe wady.

AlexBottoni
źródło
5
Bardzo dobrze rozumiesz, że myślę, że wszystkie bardziej popularne odpowiedzi są ignorowane - niezmienne dane w ogóle nie mają żadnego związku.
Michael Shaw,
3
Wydaje mi się, że nigdy nie musiałeś pracować nad projektami, których skompilowanie zająłoby pół godziny lub dłużej. Jeśli masz wszystkie swoje wyliczenia w jednym pliku i zmienisz jedno wyliczenie, czas na długą przerwę. Cholera, nawet jeśli zajęło to tylko minutę, to wciąż za długo.
Dunk
2
Każdy, kto pracuje w module X pod kontrolą wersji, musi uzyskać moduł X i wyliczenie za każdym razem, gdy chce pracować. Co więcej, wydaje mi się to formą łączenia, jeśli za każdym razem, gdy zmieniasz plik enum, twoja zmiana potencjalnie wpływa na każdy moduł w twoim projekcie. Jeśli twój projekt jest na tyle duży, że wszyscy lub większość członków zespołu nie rozumie każdej części projektu, globalne wyliczenie jest dość niebezpieczne. Globalna niezmienna zmienna nie jest nawet tak bliska jak globalna zmienna zmienna, ale nadal nie jest idealna. To powiedziawszy, globalne wyliczenie jest prawdopodobnie w porządku w zespole z <10 członkami.
Brian,
3

Dla mnie wszystko zależy od zakresu twojego projektu. Jeśli jest jeden plik z, powiedzmy 10 strukturami, i są to jedyne używane kiedykolwiek, byłbym całkowicie wygodny, mając jeden plik .h. Jeśli masz kilka różnych rodzajów funkcjonalności, powiedzmy jednostki, ekonomię, budynki itp. Zdecydowanie podzieliłbym to. Utwórz jednostki. H, w którym znajdują się wszystkie struktury, które dotyczą jednostek. Jeśli chcesz coś zrobić z jednostkami gdzieś, musisz dołączyć jednostki. Pewnie, ale jest to również miły „identyfikator”, że coś z jednostkami prawdopodobnie jest zrobione w tym pliku.

Spójrz na to w ten sposób, nie kupujesz supermarketu, ponieważ potrzebujesz puszki coli;)

hoppa
źródło
3

Nie jestem programistą C ++, więc ta odpowiedź będzie bardziej ogólna dla OOA i D:

Zazwyczaj obiekty kodu powinny być pogrupowane pod względem przydatności funkcjonalnej, niekoniecznie pod względem konstrukcji specyficznych dla języka. Kluczowym pytaniem typu „tak-nie”, które należy zawsze zadawać, jest: „czy koder końcowy będzie używał większości lub wszystkich obiektów, które otrzymają podczas korzystania z biblioteki?” Jeśli tak, zgrupuj się. Jeśli nie, rozważ podzielenie obiektów kodu i umieszczenie ich bliżej innych obiektów, które ich potrzebują (zwiększając w ten sposób szansę, że konsument będzie potrzebował wszystkiego, do czego faktycznie ma dostęp).

Podstawową koncepcją jest „wysoka spójność”; elementy kodu (od metod klasy aż po klasy w przestrzeni nazw lub bibliotece DLL i same biblioteki DLL) powinny być zorganizowane w taki sposób, aby koder mógł zawierać wszystko, czego potrzebuje, i nic, czego nie ma. To sprawia, że ​​ogólny projekt jest bardziej tolerancyjny na zmiany; rzeczy, które muszą się zmienić, mogą pozostać bez wpływu na inne obiekty kodu, które nie musiały się zmieniać. W wielu okolicznościach sprawia to, że aplikacja jest bardziej wydajna pod względem pamięci; biblioteka DLL jest ładowana w całości do pamięci, niezależnie od tego, ile z tych instrukcji kiedykolwiek zostało wykonanych przez proces. Projektowanie aplikacji „lean” wymaga zatem zwrócenia uwagi na ilość kodu wczytywanego do pamięci.

Ta koncepcja ma zastosowanie na praktycznie wszystkich poziomach organizacji kodu, z różnym stopniem wpływu na łatwość konserwacji, wydajność pamięci, wydajność, szybkość kompilacji itp. Jeśli koder musi odwoływać się do nagłówka / biblioteki DLL o raczej monolitycznym rozmiarze tylko w celu uzyskania dostępu do jednego obiektu, który nie zależy od żadnego innego obiektu w tej bibliotece DLL, prawdopodobnie należy ponownie rozważyć włączenie tego obiektu do biblioteki DLL. Można jednak pójść zbyt daleko w drugą stronę; DLL dla każdej klasy jest złym pomysłem, ponieważ spowalnia szybkość kompilacji (więcej DLL do przebudowania z powiązanym narzutem) i sprawia, że ​​przechowywanie wersji jest koszmarem.

Przykład: jeśli jakiekolwiek użycie biblioteki kodu w świecie rzeczywistym wymagałoby użycia większości lub wszystkich wyliczeń, które umieszczasz w tym pojedynczym pliku „enumerations.h”, to z całą pewnością zgrupuj je razem; będziesz wiedział, gdzie ich szukać. Ale jeśli koder zużywający może potrzebować tylko jednego lub dwóch z tuzinów wyliczeń, które podajesz w nagłówku, zachęcam do umieszczenia ich w osobnej bibliotece i uczynienia z niej zależności większej od reszty wyliczeń . Pozwala to programistom uzyskać tylko jeden lub dwa, których chcą, bez konieczności łączenia się z bardziej monolityczną biblioteką DLL.

KeithS
źródło
2

Tego rodzaju sprawa staje się problemem, gdy wielu programistów pracuje na tej samej podstawie kodu.

Z pewnością widziałem, jak podobne pliki globalne stają się ogniwem łączącym kolizje i wszelkiego rodzaju żal przy (większych) projektach.

Jeśli jednak jako jedyny pracujesz nad projektem, powinieneś robić wszystko, co najbardziej ci odpowiada, i stosować „najlepsze praktyki” tylko wtedy, gdy rozumiesz (i zgadzasz się) z motywacją.

Lepiej popełniaj błędy i ucz się na nich, niż ryzykować utknięcie w praktykach programowania kultowego przez resztę życia.

William Payne
źródło
1

Tak, złą praktyką jest robienie tego przy dużym projekcie. POCAŁUNEK.

Młody współpracownik zmienił nazwę prostej zmiennej w podstawowym pliku .h, a 100 inżynierów czekało 45 minut na przebudowanie wszystkich plików, co wpłynęło na wydajność wszystkich. ;)

Z biegiem lat wszystkie projekty zaczynają się od małych balonów, a my przeklinamy tych, którzy na wczesnych skrótach stworzyli dług techniczny. Najlepsze praktyki, ograniczaj globalną treść .h do tego, co niekoniecznie jest globalne.

AmyInNH
źródło
Nie korzystasz z systemu recenzji, jeśli ktoś (młody lub stary, ten sam) może po prostu (z zabawy) zmusić wszystkich do przebudowy projektu, prawda?
Sanctus
1

W tym przypadku powiedziałbym, że idealną odpowiedzią jest to, że zależy to od tego, w jaki sposób używane są wyliczenia, ale w większości przypadków prawdopodobnie najlepiej jest zdefiniować wszystkie wyliczenia osobno, ale jeśli którakolwiek z nich jest już sprzężona projektowo, powinieneś podać środki wspólnego wprowadzania wspomnianych sprzężonych enum. W efekcie masz tolerancję sprzężenia do ilości już zamierzonego sprzężenia, ale już nie.

Biorąc to pod uwagę, najbardziej elastyczne rozwiązanie prawdopodobnie zdefiniuje każde wyliczenie w osobnym pliku, ale zapewni sprzężone pakiety, gdy będzie to uzasadnione (zgodnie z zamierzonym użyciem zaangażowanych wyliczeń).


Zdefiniowanie wszystkich wyliczeń w tym samym pliku powoduje ich połączenie, a przez rozszerzenie każdy kod zależny od jednego lub większej liczby wyliczeń zależy od wszystkich wyliczeń, niezależnie od tego, czy kod faktycznie używa innych wyliczeń.

#include "enumList.h"

// Draw map texture.  Requires map_t.
// Not responsible for rendering entities, so doesn't require other enums.
// Introduces two unnecessary couplings.
void renderMap(map_t, mapIndex);

renderMap()wolałbym tylko wiedzieć o tym map_t, ponieważ w przeciwnym razie wszelkie zmiany w innych wpłyną na to, nawet jeśli tak naprawdę nie wchodzi w interakcje z innymi.

#include "mapEnum.h" // Theoretical file defining map_t.

void renderMap(map_t, mapIndex);

Jednak w przypadku, gdy komponenty są już połączone ze sobą, zapewnienie wielu wyliczeń w jednym pakiecie może łatwo zapewnić dodatkową jasność i prostotę, pod warunkiem, że istnieje wyraźny logiczny powód, aby wyliczyć wyliczenia, że ​​użycie tych wyliczeń jest również połączone, a zapewnienie ich nie wprowadza żadnych dodatkowych połączeń.

#include "entityEnum.h"    // Theoretical file defining entity_t.
#include "materialsEnum.h" // Theoretical file defining materials_t.

// Can entity break the specified material?
bool canBreakMaterial(entity_t, materials_t);

W takim przypadku nie ma bezpośredniego, logicznego połączenia między typem elementu a typem materiału (przy założeniu, że elementy nie są wykonane z jednego ze zdefiniowanych materiałów). Gdybyśmy jednak mieli przypadek, w którym np. Jedno wyliczenie jest wyraźnie zależne od drugiego, wówczas sensownym jest dostarczenie pojedynczego pakietu zawierającego wszystkie sprzężone wyliczenia (jak również wszelkie inne sprzężone elementy), aby połączenie mogło być izolowane do tego pakietu w jak największym stopniu.

// File: "actionEnums.h"

enum action_t { ATTACK, DEFEND, SKILL, ITEM };               // Action type.
enum skill_t  { DAMAGE, HEAL, BUFF, DEBUFF, INFLICT, NONE }; // Skill subtype.

// -----

#include "actionTypes.h" // Provides action_t & skill_t from "actionEnums.h", and class Action (which couples them).
#include "entityEnum.h"  // Theoretical file defining entity_t.

// Assume ActFlags is or acts as a table of flags indicating what is and isn't allowable, based on entity_t and Action.
ImplementationDetail ActFlags;

// Indicate whether a given type of entity can perform the specified action type.
// Assume class Action provides members type() and subtype(), corresponding to action_t and skill_t respectively.
// Is only slightly aware of the coupling; knows type() and subtype() are coupled, but not how or why they're coupled.
bool canAct(entity_t e, const Action& act) {
    return ActFlags[e][act.type()][act.subtype()];
}

Ale niestety ... nawet jeśli dwa wyliczenia są ze sobą wewnętrznie połączone, nawet jeśli jest to coś tak silnego, jak „drugi wylicznik zapewnia podkategorie dla pierwszego wyliczenia”, może jeszcze przyjść czas, w którym tylko jedna z wyliczeń jest konieczna.

#include "actionEnums.h"

// Indicates whether a skill can be used from the menu screen, based on the skill's type.
// Isn't concerned with other action types, thus doesn't need to be coupled to them.
bool skillUsableOnMenu(skill_t);

// -----
// Or...
// -----

#include "actionEnums.h"
#include "gameModeEnum.h" // Defines enum gameMode_t, which includes MENU, CUTSCENE, FIELD, and BATTLE.

// Used to grey out blocked actions types, and render them unselectable.
// All actions are blocked in cutscene, or allowed in battle/on field.
// Skill and item usage is allowed in menu.  Individual skills will be checked on attempted use.
// Isn't concerned with specific types of skills, only with broad categories.
bool actionBlockedByGameMode(gameMode_t mode, action_t act) {
    if (mode == CUTSCENE) { return true; }
    if (mode == MENU) { return (act == SKILL || act == ITEM); }

    //assert(mode == BATTLE || mode == FIELD);
    return false;
}

Dlatego, ponieważ wiemy oba, że ​​zawsze mogą wystąpić sytuacje, w których zdefiniowanie wielu wyliczeń w jednym pliku może dodać niepotrzebne sprzężenie, oraz że zapewnienie sprzężonych wyliczeń w jednym pakiecie może wyjaśnić zamierzone użycie i pozwolić nam wyodrębnić sam kod sprzężenia jako w miarę możliwości idealnym rozwiązaniem jest zdefiniowanie każdego wyliczenia osobno i zapewnienie wspólnych pakietów dla dowolnych wyliczeń, które mają być często używane razem. Jedynymi wyliczeniami zdefiniowanymi w tym samym pliku będą te, które są ze sobą wewnętrznie połączone, tak że użycie jednego z nich wymaga również użycia drugiego.

// File: "materialsEnum.h"
enum materials_t { WOOD, STONE, ETC };

// -----

// File: "entityEnum.h"
enum entity_t { PLAYER, MONSTER };

// -----

// File: "mapEnum.h"
enum map_t { 2D, 3D };

// -----

// File: "actionTypesEnum.h"
enum action_t { ATTACK, DEFEND, SKILL, ITEM };

// -----

// File: "skillTypesEnum.h"
enum skill_t  { DAMAGE, HEAL, BUFF, DEBUFF, INFLICT, NONE };

// -----

// File: "actionEnums.h"
#include "actionTypesEnum.h"
#include "skillTypesEnum.h"
Justin Time - Przywróć Monikę
źródło