Nie ma znaczenia, jak ściśle jedna rzecz jest połączona z drugą, jeśli ta druga rzecz nigdy się nie zmienia. Przez lata uważałem, że bardziej produktywne jest skupianie się na szukaniu mniejszej liczby powodów do zmiany rzeczy, dążeniu do stabilności, niż ułatwianie ich zmiany poprzez próbę uzyskania możliwie najlżejszej formy sprzężenia.
Odsprzęganie okazało się bardzo przydatne, do tego stopnia, że czasami preferuję skromne powielanie kodu w celu rozłączania pakietów. Jako podstawowy przykład mogłem użyć biblioteki matematycznej do zaimplementowania biblioteki obrazów. Nie skopiowałem i po prostu skopiowałem kilka podstawowych funkcji matematycznych, których kopiowanie było banalne.
Teraz moja biblioteka obrazów jest całkowicie niezależna od biblioteki matematycznej w taki sposób, że bez względu na to, jakie zmiany wprowadzę do biblioteki matematycznej, nie wpłynie to na bibliotekę obrazów. To stawia stabilność przede wszystkim. Biblioteka obrazów jest teraz bardziej stabilna, ponieważ ma drastycznie mniej powodów do zmiany, ponieważ jest oddzielona od dowolnej innej biblioteki, która mogłaby się zmienić (oprócz standardowej biblioteki C, która, mam nadzieję, nigdy nie powinna się zmienić). Jako bonus można go również łatwo wdrożyć, gdy jest to samodzielna biblioteka, która nie wymaga pobierania wielu innych bibliotek, aby ją zbudować i używać.
Stabilność jest dla mnie bardzo pomocna. Lubię budować kolekcję dobrze przetestowanego kodu, który ma coraz mniej powodów, aby kiedykolwiek zmieniać się w przyszłości. To nie marzenie z fajki; Mam kod C, którego używałem i używam ponownie od późnych lat 80-tych, które od tamtej pory wcale się nie zmieniły. To wprawdzie niskopoziomowe rzeczy, takie jak kod zorientowany na piksele i geometria, podczas gdy wiele moich rzeczy na wyższym poziomie stało się przestarzałych, ale jest to coś, co nadal bardzo pomaga w utrzymaniu. To prawie zawsze oznacza bibliotekę, która polega na coraz mniejszej liczbie rzeczy, jeśli w ogóle nic zewnętrznego. Niezawodność rośnie w górę, jeśli oprogramowanie w coraz większym stopniu zależy od stabilnych podstaw, które nie mają lub nie mają powodów do zmiany. Mniej ruchomych części jest naprawdę fajnych, nawet jeśli w praktyce ruchome części są znacznie większe niż części stabilne.
Luźne sprzężenie jest w tym samym stylu, ale często uważam, że luźne sprzężenie jest o wiele mniej stabilne niż brak sprzężenia. O ile nie pracujesz w zespole z znacznie lepszymi projektantami interfejsów i klientami, którzy nie zmieniają zdania, z którymi kiedykolwiek pracowałem, nawet czyste interfejsy często znajdują powody do zmiany w sposób, który wciąż powoduje kaskadowe awarie w całym kodzie. Pomysł, że stabilność można osiągnąć kierując zależności w stronę abstraktu, a nie konkretów, jest użyteczny tylko wtedy, gdy projekt interfejsu jest łatwiejszy do poprawienia za pierwszym razem niż implementacja. Często zdarza mi się, że jest odwrotnie, gdy deweloper mógł stworzyć bardzo dobrą, jeśli nie cudowną implementację, biorąc pod uwagę wymagania projektowe, które według nich powinny spełnić, ale w przyszłości okazuje się, że wymagania projektowe całkowicie się zmieniają.
Dlatego lubię stabilność i całkowite oddzielenie, aby móc przynajmniej śmiało powiedzieć: „Ta mała izolowana biblioteka, która była używana od lat i zabezpieczona dokładnymi testami, nie ma prawie żadnych szans na konieczność wprowadzenia zmian bez względu na to, co dzieje się w chaotycznym świecie zewnętrznym . ” Daje mi to odrobinę rozsądku bez względu na to, jakie zmiany projektowe są wymagane na zewnątrz.
Sprzężenie i stabilność, przykład ECS
Uwielbiam także systemy encji-komponentów, które wprowadzają wiele ścisłych powiązań, ponieważ wszystkie zależności od systemu do komponentu uzyskują bezpośredni dostęp do surowych danych i manipulują nimi, w następujący sposób:
Wszystkie zależności tutaj są dość ścisłe, ponieważ komponenty po prostu ujawniają surowe dane. Zależności nie płyną w kierunku abstrakcji, płyną w kierunku nieprzetworzonych danych, co oznacza, że każdy system ma maksymalną możliwą wiedzę na temat każdego rodzaju komponentu, o który prosi o dostęp. Komponenty nie mają żadnej funkcjonalności, ponieważ wszystkie systemy uzyskują dostęp do surowych danych i manipulują nimi. Jednak bardzo łatwo jest uzasadnić taki system, ponieważ jest tak płaski. Jeśli tekstura wyjdzie paskudnie, od razu wiesz z tym systemem, że tylko system renderowania i malowania ma dostęp do składników tekstury i prawdopodobnie możesz szybko wykluczyć system renderowania, ponieważ czyta tylko z tekstur koncepcyjnie.
Tymczasem luźno powiązana alternatywa może być następująca:
... z wszystkimi zależnościami płynącymi w kierunku funkcji abstrakcyjnych, a nie danych, i każda pojedyncza rzecz na tym schemacie ujawnia własny interfejs i własną funkcjonalność. Tutaj wszystkie zależności mogą być bardzo luźne. Obiekty mogą nawet nie zależeć bezpośrednio od siebie i oddziaływać ze sobą za pośrednictwem czystych interfejsów. Nadal bardzo trudno jest uzasadnić ten system, szczególnie jeśli coś pójdzie nie tak, biorąc pod uwagę złożoną plątaninę interakcji. Będzie też więcej interakcji (więcej sprzężeń, choć luźniejszych) niż ECS, ponieważ podmioty muszą wiedzieć o agregowanych przez siebie komponentach, nawet jeśli wiedzą tylko o swoim abstrakcyjnym interfejsie publicznym.
Również jeśli coś zmieni się w projekcie, otrzymasz więcej kaskadowych uszkodzeń niż ECS, i zwykle będzie więcej powodów i pokus dla zmian w projekcie, ponieważ każda rzecz stara się zapewnić ładny obiektowy interfejs i abstrakcję. To natychmiast przychodzi z pomysłem, że każda drobiazg będzie próbować nałożyć ograniczenia i ograniczenia na projekt, a te ograniczenia są często tym, co uzasadnia zmiany w projekcie. Funkcjonalność jest znacznie bardziej ograniczona i musi przyjmować o wiele więcej założeń projektowych niż surowe dane.
Odkryłem w praktyce, że powyższy typ „płaskiego” systemu ECS jest o wiele łatwiejszy do uzasadnienia niż nawet najbardziej luźno sprzężone systemy ze złożoną siecią luźnych zależności i, co najważniejsze, mam tak mało powodów aby wersja ECS musiała kiedykolwiek zmieniać istniejące komponenty, ponieważ te komponenty nie były odpowiedzialne, poza dostarczeniem odpowiednich danych potrzebnych do funkcjonowania systemów. Porównaj trudność zaprojektowania czystego IMotion
interfejsu i konkretnego obiektu ruchu implementującego ten interfejs, który zapewnia wyrafinowaną funkcjonalność, jednocześnie próbując utrzymać niezmienniki danych prywatnych w porównaniu z komponentem ruchu, który musi tylko dostarczyć surowe dane istotne dla rozwiązania problemu i nie przeszkadza funkcjonalność.
Funkcjonalność jest o wiele trudniejsza do uzyskania niż dane, dlatego myślę, że często lepiej jest kierować przepływ zależności w kierunku danych. W końcu ile jest bibliotek wektorów / macierzy? Ilu z nich używa dokładnie tej samej reprezentacji danych i tylko nieznacznie różni się funkcjonalnością? Niezliczone, a jednak wciąż mamy ich tyle, pomimo identycznych reprezentacji danych, ponieważ chcemy subtelnych różnic w funkcjonalności. Ile jest bibliotek obrazów? Ile z nich reprezentuje piksele w inny i niepowtarzalny sposób? Prawie żaden, i ponownie pokazując, że funkcjonalność jest znacznie bardziej niestabilna i podatna na zmiany projektowe niż dane w wielu scenariuszach. Oczywiście w pewnym momencie potrzebujemy funkcjonalności, ale możesz zaprojektować systemy, w których większość zależności płynie w kierunku danych, a nie ogólnie abstrakcje lub funkcjonalność. Taki priorytet nadałby stabilności ponad sprzężenie.
Najbardziej stabilne funkcje, jakie kiedykolwiek napisałem (takie, których używałem i których używałem od późnych lat 80. bez konieczności ich zmiany), wszystkie polegały na surowych danych, takie jak funkcja geometrii, która akceptuje tablicę liczba zmiennoprzecinkowa i liczby całkowite, nie te zależne od złożonego Mesh
obiektu lub IMesh
interfejsu, lub mnożenie wektora / macierzy, które zależały od float[]
lub double[]
nie, od którego zależało FancyMatrixObjectWhichWillRequireDesignChangesNextYearAndDeprecateWhatWeUse
.