Czy istnieją warianty OOP, w których niektóre lub wszystkie zasady SOLID są przeciwne do czyszczenia kodu?

26

Niedawno miałem rozmowę z moim przyjacielem na temat OOP w tworzeniu gier wideo.

Wyjaśniałem architekturę jednej z moich gier, która, ku zaskoczeniu mojego przyjaciela, zawierała wiele małych klas i kilka warstw abstrakcji. Argumentowałem, że był to wynik mojego skupienia się na nadaniu wszystkim Jednej Odpowiedzialności, a także na poluzowaniu sprzężenia między komponentami.

Martwił go, że duża liczba zajęć przełoży się na koszmar utrzymania. Moim zdaniem byłby to dokładnie odwrotny skutek. Dyskutowaliśmy o tym przez kilka stuleci, ostatecznie zgadzając się nie zgodzić, mówiąc, że być może zdarzają się przypadki, w których SOLIDNE zasady i właściwe OOP tak naprawdę nie pasują do siebie.

Nawet wpis w Wikipedii na temat zasad SOLID mówi, że są to wytyczne, które pomagają w pisaniu łatwego do utrzymania kodu i że są one częścią ogólnej strategii zwinnego i adaptacyjnego programowania.

Moje pytanie brzmi:

Czy w OOP są przypadki, w których niektóre lub wszystkie zasady SOLID nie nadają się do czyszczenia kodu?

Mogę od razu wyobrazić sobie, że Zasada Zastępstwa Liskowa mogłaby być sprzeczna z innym smakiem bezpiecznego dziedziczenia. Innymi słowy, jeśli ktoś opracuje inny użyteczny wzorzec zaimplementowany w drodze dziedziczenia, jest całkiem możliwe, że LSP może być z nim w bezpośrednim konflikcie.

Czy są jeszcze inni? Być może niektóre typy projektów lub pewne platformy docelowe działają lepiej przy mniejszym podejściu SOLID?

Edytować:

Chciałbym tylko sprecyzować, że nie pytam, jak poprawić mój kod;) Jedynym powodem, dla którego wspomniałem o projekcie w tym pytaniu, było podanie małego kontekstu. Moje pytanie dotyczy OOP i zasad projektowania w ogóle .

Jeśli jesteś ciekawy mojego projektu, zobacz to .

Edycja 2:

Wyobraziłem sobie, że na to pytanie można odpowiedzieć na jeden z 3 sposobów:

  1. Tak, istnieją zasady projektowania OOP, które częściowo kolidują z SOLID
  2. Tak, istnieją zasady projektowania OOP, które są całkowicie sprzeczne z SOLID
  3. Nie, SOLID to kolana pszczoły i OOP na zawsze będzie z nią lepszy. Ale, jak w przypadku wszystkiego, nie jest to panaceum. Pij odpowiedzialnie.

Opcje 1 i 2 prawdopodobnie wygenerowałyby długie i interesujące odpowiedzi. Z drugiej strony wariant 3 byłby krótką, nieciekawą, ale ogólnie uspokajającą odpowiedzią.

Wydaje się, że zbliżamy się do opcji 3.

MetaFight
źródło
Jak definiujesz „czysty kod”?
GrandmasterB
Czysty kod, w zakresie tego pytania, to struktura kodu w sposób, który spełnia cele OOP i zestawu wybranych zasad projektowania. Znam więc czysty kod pod względem celów wyznaczonych przez OOP + SOLID. Zastanawiam się, czy istnieje inny zestaw zasad projektowania, które działają z OOP, ale są całkowicie lub częściowo niezgodne z SOLID.
MetaFight,
1
Bardziej martwię się o wydajność twojej gry niż o cokolwiek innego. Chyba że mówimy o grze tekstowej.
Euforia
1
Ta gra jest w zasadzie eksperymentem. Wypróbowuję różne podejścia programistyczne, a także próbuję sprawdzić, czy kod korporacyjny może działać podczas pisania gry. Oczekuję, że wydajność stanie się problemem, ale jeszcze nie. Jeśli / kiedy tak się stanie, następną rzeczą, z którą będę eksperymentował, jest sposób dostosowania mojego kodu do kodu wykonawczego. Tyle nauki!
MetaFight,
1
Na ryzyko niektórych negatywnych opinii chciałbym wrócić do pytania „dlaczego”: OOP w rozwoju gier . Ze względu na naturę tworzenia gier, „tradycyjne” OOP wydaje się być modne na rzecz różnych smaków systemów encji / komponentów, na przykład tutaj interesujący wariant . To raczej sprawia, że ​​myślę, że pytanie nie powinno brzmieć „SOLID + OOP”, a raczej „OOP + Game Development”. Gdy spojrzysz na wykres interakcji obiektu dla twórcy gry. w porównaniu z aplikacją N-tier różnice mogą oznaczać, że nowy paradygmat jest dobry.
J Trana,

Odpowiedzi:

20

Czy w OOP są przypadki, w których niektóre lub wszystkie zasady SOLID nie nadają się do czyszczenia kodu?

Ogólnie nie. Historia pokazała, że ​​wszystkie zasady SOLID w dużej mierze przyczyniają się do zwiększonego oddzielania, co z kolei wykazało, że zwiększa elastyczność kodu, a tym samym zdolność do dostosowania się do zmian, a także ułatwia kodowanie uzasadnienia, testowania, ponownego użycia. w skrócie, uczyń swój kod czystszym.

Teraz mogą zdarzyć się przypadki, w których zasady SOLID zderzają się z SUCHEM (nie powtarzaj się), KISS (niech to będzie głupie) lub innymi zasadami dobrego projektowania OO. I oczywiście mogą kolidować z rzeczywistością wymagań, ograniczeniami ludzi, ograniczeniami naszych języków programowania, innymi przeszkodami.

Krótko mówiąc, zasady SOLID zawsze nadadzą się do czyszczenia kodu, ale w niektórych scenariuszach nadadzą się mniej niż sprzeczne alternatywy. Zawsze są dobre, ale czasem inne rzeczy są bardziej dobre.

Telastyn
źródło
14
Lubię, ale czasem inne rzeczy są bardziej dobre . Ma charakter „Animal Farm”. ;)
FrustratedWithFormsDesigner
12
+1 Kiedy tylko spojrzałem na SOLIDNE zasady, widzę 5 „miłych do zrobienia”. Ale żaden z nich nie jest NAJWYŻSZY OSUSZANIE, KISS i „wyjaśniaj swoje intencje”. Używaj SOLID, ale zachowaj ostrożność i sceptycyzm.
user949300
Zgadzam się. Myślę, że prawidłowe zasady, których należy przestrzegać, wyraźnie zależą od sytuacji, ale oprócz twardych wymagań wydajnościowych, które zawsze plasują się ponad wszystko inne (ma to znaczenie szczególnie przy tworzeniu gier), myślę, że DRY i KISS są zwykle ważniejsze niż SOLID. Oczywiście, im bardziej czysty jest kod, tym lepiej, więc jeśli można przestrzegać wszystkich zasad bez konfliktów, tym lepiej.
Ben Lee,
Co z segregacją integracyjną a efektywnym projektowaniem agregatów? Jeśli podstawowy interfejs „sekwencji” [np. IEnumerable] zawiera metody takie jak Counti asImmutableoraz właściwości podobne getAbilities[których zwrot wskazywałby, czy rzeczy takie Countbędą „wydajne”], wówczas można zastosować metodę statyczną, która pobiera wiele sekwencji i agreguje je, aby Będę się zachowywać jak pojedyncza dłuższa sekwencja. Jeśli takie umiejętności są obecne w podstawowym typie sekwencji, nawet jeśli łączą jedynie domyślne implementacje, agregat będzie w stanie odsłonić te umiejętności ...
supercat
... i wdrażaj je efektywnie, jeśli są używane z klasami podstawowymi, które to potrafią. Jeśli jeden agreguje dziesięciomilionową listę pozycji, która zna jej liczbę, oraz pięciomilionową listę, która może tylko pobierać pozycje sekwencyjnie, żądanie wpisu w wysokości 10 000 0003 powinno odczytać liczbę z pierwszej listy, a następnie odczytać trzy pozycje z drugiej . Interfejsy zlewozmywaków kuchennych mogą naruszać ISP, ale mogą znacznie poprawić ich wydajność w niektórych scenariuszach dotyczących składu / agregacji.
supercat
13

Myślę, że mam niezwykłą perspektywę, ponieważ pracowałem zarówno w finansach, jak i grach.

Wielu programistów, których spotkałem w grach, było okropnych w inżynierii oprogramowania - ale nie potrzebowali praktyk takich jak SOLID. Tradycyjnie wkładają swoją grę do pudełka - i gotowe.

W finansach zauważyłem, że programiści mogą być naprawdę niechlujni i niezdyscyplinowani, ponieważ nie potrzebują wydajności, którą osiągasz w grach.

Obie powyższe instrukcje są oczywiście nadmiernymi uogólnieniami, ale w obu przypadkach czysty kod ma kluczowe znaczenie. Dla optymalizacji niezbędny jest przejrzysty i łatwy do zrozumienia kod. Ze względu na łatwość konserwacji chcesz tego samego.

Nie oznacza to, że SOLID nie jest bez krytyki. Nazywa się je zasadami, a mimo to należy ich przestrzegać jak wskazówek? Kiedy dokładnie powinienem ich przestrzegać? Kiedy powinienem złamać zasady?

Prawdopodobnie mógłbyś spojrzeć na dwa fragmenty kodu i powiedzieć, który z nich najlepiej odpowiada SOLID. Ale w izolacji nie ma obiektywnego sposobu na przekształcenie kodu w SOLID. Zdecydowanie zależy to od interpretacji dewelopera.

Mówienie, że „duża liczba klas może prowadzić do koszmaru utrzymania” jest non sekitur. Jeśli jednak SRP nie zostanie poprawnie zinterpretowany, możesz łatwo skończyć z tym problemem.

Widziałem kod z wieloma warstwami praktycznie bez żadnej odpowiedzialności. Klasy, które nie robią nic poza przejściem z jednej klasy do drugiej. Klasyczny problem zbyt wielu warstw pośrednich.

W przypadku nadużycia SRP może skończyć się brakiem spójności w kodzie. Możesz to powiedzieć podczas próby dodania funkcji. Jeśli zawsze musisz zmieniać kilka miejsc w tym samym czasie, kodowi brakuje spójności.

Open-Closed nie jest pozbawiony krytyków. Na przykład zobacz recenzję Jona Skeeta dotyczącą zasady otwartego zamknięcia . Nie będę tu powtarzał jego argumentów.

Twój argument na temat LSP wydaje się raczej hipotetyczny i naprawdę nie rozumiem, co masz na myśli, mówiąc o innej formie bezpiecznego dziedziczenia?

Wydaje się, że dostawca usług internetowych nieco powiela SRP. Z pewnością należy je interpretować tak samo, jak nigdy nie można przewidzieć, co zrobią wszyscy klienci Twojego interfejsu. Dzisiejszy spójny interfejs jest jutrzejszym naruszającym ISP. Jedynym nielogicznym wnioskiem jest posiadanie nie więcej niż jednego członka na interfejs.

DIP może prowadzić do bezużytecznego kodu. Widziałem klasy o ogromnej liczbie parametrów w nazwie DIP. Te klasy zwykle „unowocześniały” swoje części kompozytowe, ale ja, biorąc pod uwagę zdolność testowania, nie możemy już nigdy używać nowego słowa kluczowego.

Sam SOLID nie wystarcza do napisania czystego kodu. Widziałem książkę Roberta C. Martina „ Clean Code: A Handbook of Agile Software Craftsmanship ”. Największym problemem jest to, że łatwo jest przeczytać tę książkę i zinterpretować ją jako regułę. Jeśli to zrobisz, tracisz sens. Zawiera dużą listę zapachów, heurystyki i zasad - znacznie więcej niż tylko pięć z SOLID. Wszystkie te „zasady” nie są tak naprawdę zasadami, ale wytycznymi. Wujek Bob sam opisuje je jako „szuflady” dla pojęć. Są aktem równoważącym; ślepe stosowanie się do jakichkolwiek wytycznych spowoduje problemy.

Dave Hillier
źródło
1
Jeśli masz dużą liczbę parametrów konstruktora, sugeruje to, że naruszyłeś SRP. Jednak pojemnik DI usuwa i tak ból związany z dużą liczbą parametrów konstruktora, więc nie zgadzam się z twoimi stwierdzeniami dotyczącymi DIP.
Stephen
Wszystkie te „zasady” nie są tak naprawdę zasadami, ale wytycznymi. Są aktem równoważącym; jak powiedziałem w moim poście po SRP, sama skrajność może być problematyczna.
Dave Hillier
Dobra odpowiedź, +1. Chciałbym dodać jedną rzecz: bardziej czytam recenzję Skeeta jako krytykę standardowej definicji OCP w podręcznikach, a nie głównych idei leżących u podstaw OCP. Według mnie idea chronionej odmiany jest tym, czym powinien być OCP od samego początku.
Doc Brown
5

Oto moja opinia:

Chociaż zasady SOLID mają na celu zapewnienie nie redundantnej i elastycznej bazy kodu, może to być kompromis w zakresie czytelności i konserwacji, jeśli istnieje zbyt wiele klas i warstw.

Chodzi przede wszystkim o liczbę abstrakcji . Duża liczba klas może być w porządku, jeśli są one oparte na kilku abstrakcjach. Ponieważ nie znamy rozmiaru i specyfiki twojego projektu, trudno powiedzieć, ale trochę niepokojące jest to, że wspominasz o kilku warstwach oprócz dużej liczby klas.

Wydaje mi się, że zasady SOLID nie są niezgodne z OOP, ale nadal można pisać przylegający do nich kod.

henginy
źródło
3
+1 Z definicji prawie wysoce elastyczny system musi być mniej konserwowalny niż ten, który jest mniej elastyczny. Elastyczność wiąże się z dodatkowymi kosztami wynikającymi z dodatkowej złożoności.
Andy,
@Andy niekoniecznie. w mojej obecnej pracy wiele najgorszych części kodu jest nieuporządkowana, ponieważ jest po prostu zrzucona razem przez „zrobienie czegoś prostego, po prostu zhakuj go razem i spraw, aby działał tak, aby nie był tak skomplikowany”. W rezultacie wiele części systemu jest w 100% sztywnych. Nie można ich w ogóle modyfikować bez przepisywania dużych części. Naprawdę trudno go utrzymać. Utrzymywalność wynika z wysokiej kohezji i niskiego sprzężenia, a nie z tego, jak elastyczny jest system.
sara,
3

We wczesnych latach rozwoju zacząłem pisać Roguelike w C ++. Ponieważ chciałem zastosować dobrą metodologię obiektową, której nauczyłem się z mojej edukacji, od razu zobaczyłem przedmioty w grze jako obiekty C ++. Potion, Swords, Food, Axe, JavelinsItd. Wszystko pochodzi od rdzenia podstawowego Itemkodu, obsługi nazwa, ikona, wagi, tego rodzaju rzeczy.

Następnie zakodowałem logikę torby i napotkałem problem. Mogę włożyć Itemstorbę, ale jeśli wybiorę jedną, to skąd mam wiedzieć, czy to jest Potiona Sword? Sprawdziłem w Internecie, jak spuszczać przedmioty. Zhakowałem opcje kompilatora, aby włączyć informacje o środowisku wykonawczym, aby wiedzieć, jakie są moje abstrakcyjne Itemtypy prawdziwe, i zacząłem przełączać logikę na typy, aby wiedzieć, na jakie operacje zezwalam (np. Unikaj picia Swordsi postaw na Potionsnogi)

Zasadniczo zmienił kod w dużą kulkę na wpół skopiowanego błota. W miarę realizacji projektu zacząłem rozumieć, co było błędem projektu. Stało się tak, że próbowałem używać klas do reprezentowania wartości. Moja hierarchia klasowa nie była znaczącą abstrakcją. Co ja powinienem był tego robić czyni Itemwdrożyć szereg funkcji, takich jak Equip (), Apply ()i Throw (), i by każdy zachowuje się w oparciu o wartości. Używanie wyliczeń do reprezentowania miejsc na wyposażenie. Używanie wyliczeń do reprezentowania różnych rodzajów broni. Używanie większej liczby wartości i mniejszej klasyfikacji, ponieważ moja podklasa nie miała innego celu niż wypełnienie tych ostatecznych wartości.

Ponieważ stoisz w obliczu mnożenia obiektów pod warstwą abstrakcji, myślę, że mój wgląd może być tutaj cenny. Jeśli wydaje się, że masz zbyt wiele typów obiektów, być może mylisz, co powinno być typem z tym, co powinno być wartością.

Arthur Havlicek
źródło
2
Problem polegał na pomyleniu typów z klasami wartości (uwaga: nie używam słowa class w odniesieniu do pojęcia klasy OOP / C ++.) Istnieje więcej niż jeden sposób przedstawienia punktu na płaszczyźnie kartezjańskiej (współrzędne prostokątne, biegunowe współrzędne), ale wszystkie są częścią tego samego typu. Jednak główne języki OOP nie obsługują naturalnie klasyfikacji wartości. Najbliższą rzeczą jest unia C / C ++, ale musisz dodać dodatkowe pole, aby wiedzieć, co do cholery tam włożyłeś.
Doval
4
Twoje wrażenia można wykorzystać jako przykład. Nie używając SOLID ani żadnej metodologii modelowania, stworzyłeś niemożliwy do utrzymania i niemożliwy do rozszerzenia kod.
Euforia
1
@Euphoric Bardzo się starałem. Miałem metodologię obiektową. Istnieje różnica między posiadaniem metodologii a jej pomyślnym wdrożeniem :)
Arthur Havlicek
2
Jak to jest przykład zasad SOLID działających wbrew czystemu kodowi w OOP? Wygląda to bardziej na przykład niepoprawnego projektu - jest to ortogonalne dla OOP!
Andres F.
1
Twoja podstawowa klasa POZYCJA powinna mieć kilka metod boolowskich „canXXXX”, wszystkie domyślnie ustawione na FAŁSZ. Następnie możesz ustawić „canThrow ()”, aby zwracał wartość true w swojej klasie Javelin, oraz canEat (), aby zwracał wartość true dla swojej klasy Postion.
James Anderson
3

Martwił go, że duża liczba zajęć przełoży się na koszmar utrzymania. Moim zdaniem byłby to dokładnie odwrotny skutek.

Jestem absolutnie po stronie twojego przyjaciela, ale może to być kwestia naszych domen i rodzajów problemów i projektów, które rozwiązujemy, a zwłaszcza tego, jakie rodzaje rzeczy mogą wymagać zmian w przyszłości. Różne problemy, różne rozwiązania. Nie wierzę w dobro ani zło, po prostu programiści starają się znaleźć najlepszy sposób, aby najlepiej rozwiązać swoje problemy projektowe. Pracuję w VFX, który nie jest zbyt podobny do silników gier.

Ale problem, z którym borykam się w czymś, co można by nazwać nieco bardziej architekturą zgodną z SOLID (opartą na modelu COM), można z grubsza sprowadzić do „zbyt wielu klas” lub „zbyt wielu funkcji”, ponieważ twój przyjaciel może to opisać. Powiedziałbym konkretnie: „zbyt wiele interakcji, zbyt wiele miejsc, które mogą być niewłaściwie zachowane, zbyt wiele miejsc, które mogą powodować skutki uboczne, zbyt wiele miejsc, które mogą wymagać zmiany, i zbyt wiele miejsc, które mogą nie robić tego, co naszym zdaniem robią . ”

Mieliśmy garść abstrakcyjnych (i czystych) interfejsów zaimplementowanych przez mnóstwo różnych podtypów, takich jak ten (stworzyliśmy ten diagram w kontekście mówienia o korzyściach ECS, zignoruj ​​lewy dolny komentarz):

wprowadź opis zdjęcia tutaj

Gdzie interfejs ruchu lub interfejs węzła sceny mogą być implementowane przez setki podtypów: światła, kamery, siatki, solwery fizyki, shadery, tekstury, kości, prymitywne kształty, krzywe itp. Itp. (I często istniało wiele rodzajów każdego z nich ). Ostatecznym problemem było to, że projekty te nie były tak stabilne. Mieliśmy zmieniające się wymagania, a czasem same interfejsy musiały się zmienić, a kiedy chcesz zmienić abstrakcyjny interfejs zaimplementowany przez 200 podtypów, jest to niezwykle kosztowna zmiana. Zaczęliśmy to łagodzić, stosując abstrakcyjne klasy podstawowe, między którymi zmniejszały się koszty takich zmian projektowych, ale były one nadal drogie.

Alternatywnie zacząłem badać architekturę systemu encja-komponent, stosowaną raczej powszechnie w branży gier. To zmieniło wszystko tak:

wprowadź opis zdjęcia tutaj

I wow! To była taka różnica pod względem łatwości konserwacji. Zależności nie płynęły już w kierunku abstrakcji , ale w kierunku danych (komponentów). I przynajmniej w moim przypadku dane były znacznie bardziej stabilne i łatwiejsze do poprawnego zaprojektowania, pomimo zmieniających się wymagań (chociaż to, co możemy zrobić z tymi samymi danymi, ciągle się zmienia wraz ze zmieniającymi się wymaganiami).

Ponieważ jednostki w ECS używają kompozycji zamiast dziedziczenia, tak naprawdę nie muszą zawierać funkcji. Są tylko analogicznym „pojemnikiem komponentów”. Dzięki temu analogiczne 200 podtypów, które implementowały interfejs ruchu , zamieniają się w 200 instancji encji (nie oddzielne typy z osobnym kodem), które po prostu przechowują komponent ruchu (który jest niczym innym jak danymi związanymi z ruchem). A PointLightnie jest już osobną klasą / podtypem. To wcale nie jest klasa. Jest to przykład bytu, który po prostu łączy niektóre komponenty (dane) związane z tym, gdzie jest w przestrzeni (ruch) i określone właściwości świateł punktowych. Jedyną związaną z nimi funkcją jest system, taki jakRenderSystem, który szuka lekkich komponentów w scenie, aby określić sposób renderowania sceny.

Wraz ze zmieniającymi się wymaganiami w ramach podejścia ECS często trzeba było zmienić tylko jeden lub dwa systemy działające na tych danych lub po prostu wprowadzić nowy system z boku lub wprowadzić nowy komponent, jeśli potrzebne byłyby nowe dane.

Tak więc przynajmniej dla mojej domeny i jestem prawie pewien, że nie dla wszystkich, to znacznie ułatwiło sprawę, ponieważ zależności płynęły w kierunku stabilności (rzeczy, które wcale nie musiały się często zmieniać). Tak nie było w architekturze COM, gdy zależności jednorodnie płynęły w kierunku abstrakcji. W moim przypadku o wiele łatwiej jest dowiedzieć się, jakie dane są wymagane do ruchu z góry, niż wszystkie możliwe rzeczy, które możesz z tym zrobić, co często zmienia się nieco w ciągu miesięcy lub lat, gdy pojawiają się nowe wymagania.

wprowadź opis zdjęcia tutaj

Czy w OOP są przypadki, w których niektóre lub wszystkie zasady SOLID nie nadają się do czyszczenia kodu?

Cóż, czystego kodu nie mogę powiedzieć, ponieważ niektórzy ludzie utożsamiają czysty kod z SOLID, ale zdecydowanie są pewne przypadki, w których oddzielanie danych od funkcjonalności, podobnie jak ECS, i przekierowywanie zależności od abstrakcji w kierunku danych zdecydowanie może znacznie ułatwić zmień, z oczywistych powodów sprzężenia, jeśli dane będą znacznie bardziej stabilne niż abstrakcje. Oczywiście zależności od danych mogą utrudniać utrzymanie niezmienników, ale ECS ma tendencję do ograniczania tego do minimum dzięki organizacji systemu, która minimalizuje liczbę systemów, które uzyskują dostęp do dowolnego typu elementu.

Zależności nie muszą koniecznie płynąć w kierunku abstrakcji, jak sugerowałby DIP; zależności powinny płynąć w kierunku rzeczy, które prawdopodobnie nie będą wymagały przyszłych zmian. To mogą być lub nie abstrakcje we wszystkich przypadkach (z pewnością nie były moje).

  1. Tak, istnieją zasady projektowania OOP, które częściowo kolidują z SOLID
  2. Tak, istnieją zasady projektowania OOP, które są całkowicie sprzeczne z SOLID.

Nie jestem pewien, czy ECS jest naprawdę smakiem OOP. Niektórzy ludzie definiują to w ten sposób, ale widzę, że jest to zupełnie odmienne z natury charakterystyka sprzężenia i oddzielenie danych (komponentów) od funkcjonalności (systemów) i braku enkapsulacji danych. Gdyby uznać to za formę OOP, pomyślałbym, że jest to w dużym stopniu sprzeczne z SOLID (przynajmniej najostrzejsze idee SRP, open / closed, substytucja Liskova i DIP). Mam jednak nadzieję, że jest to rozsądny przykład jednego przypadku i dziedziny, w której najbardziej fundamentalne aspekty SOLID, przynajmniej tak, jak ludzie na ogół interpretują je w bardziej rozpoznawalnym kontekście OOP, mogą nie mieć zastosowania.

Teeny Classes

Wyjaśniałem architekturę jednej z moich gier, która, ku zaskoczeniu mojego przyjaciela, zawierała wiele małych klas i kilka warstw abstrakcji. Argumentowałem, że był to wynik mojego skupienia się na nadaniu wszystkim Jednej Odpowiedzialności, a także na poluzowaniu sprzężenia między komponentami.

ECS podważyło i zmieniło moje poglądy. Podobnie jak ty, myślałem, że samą ideą łatwości utrzymania jest najprostsza implementacja rzeczy możliwych, co implikuje wiele rzeczy, a ponadto wiele współzależnych rzeczy (nawet jeśli współzależności występują między abstrakcjami). Jest to najbardziej sensowne, jeśli przybliżasz tylko jedną klasę lub funkcję, aby zobaczyć najprostszą i najprostszą implementację, a jeśli jej nie widzimy, przebuduj ją, a może nawet rozłóż. Ale w rezultacie łatwo może przeoczyć to, co dzieje się ze światem zewnętrznym, ponieważ za każdym razem, gdy dzielisz coś stosunkowo złożonego na 2 lub więcej rzeczy, te 2 lub więcej rzeczy musi nieuchronnie oddziaływać * (patrz poniżej) w niektórych sposób, albo coś na zewnątrz musi wchodzić w interakcję z nimi wszystkimi.

W dzisiejszych czasach uważam, że istnieje równowaga między prostotą czegoś a ilością rzeczy i wymaganą interakcją. Systemy w ECS wydają się być dość mocny z nietrywialnych wdrożeń działać na danych, jak PhysicsSystemalbo RenderSystemalbo GuiLayoutSystem. Jednak fakt, że tak skomplikowany produkt potrzebuje tak niewielu, ułatwia cofnięcie się i uzasadnienie ogólnego zachowania całej bazy kodu. Jest w tym coś, co może sugerować, że nie jest złym pomysłem oparcie się na mniejszej liczbie, bardziej masywnych klas (wciąż wykonujących prawdopodobnie osobliwą odpowiedzialność), jeśli oznacza to mniej klas do utrzymania i uzasadnienia oraz mniej interakcji w całym tekście system.

Interakcje

Mówię „interakcje”, a nie „łączenie” (choć redukcja interakcji oznacza redukcję obu), ponieważ można użyć abstrakcji, aby rozdzielić dwa konkretne obiekty, ale one nadal ze sobą rozmawiają. Nadal mogą powodować działania niepożądane w procesie tej pośredniej komunikacji. I często uważam, że moja zdolność do rozumowania poprawności systemu jest bardziej związana z tymi „interakcjami” niż z „sprzężeniem”. Minimalizowanie interakcji znacznie ułatwia mi rozumowanie wszystkiego z lotu ptaka. Oznacza to, że rzeczy w ogóle się ze sobą nie rozmawiają, i z tego punktu widzenia ECS ma tendencję do naprawdę minimalizowania „interakcji”, a nie tylko łączenia, do najmniejszego minimum (przynajmniej ja nie

To powiedziawszy, może to być przynajmniej częściowo ja i moje osobiste słabości. Znalazłem największą przeszkodę dla mnie, aby stworzyć systemy o ogromnej skali, i nadal pewnie o nich myślę, nawigować po nich i czuję, że mogę wprowadzić wszelkie potencjalnie pożądane zmiany w dowolnym miejscu w przewidywalny sposób, zarządzanie stanem i zasobami oraz skutki uboczne. To największa przeszkoda, która zaczyna się pojawiać, gdy przechodzę od dziesiątek tysięcy LOC do setek tysięcy LOC do milionów LOC, nawet dla kodu, który sam stworzyłem. Jeśli coś spowolni mnie do czołgania się przede wszystkim, mam wrażenie, że nie mogę już zrozumieć, co się dzieje pod względem stanu aplikacji, danych, skutków ubocznych. To' to nie czas robota, którego wymaga wprowadzenie zmian, które mnie spowalniają, tak samo jak niemożność zrozumienia pełnego wpływu zmiany, jeśli system wykracza poza zdolność rozumowania o tym. A zmniejszenie interakcji było dla mnie najskuteczniejszym sposobem na zwiększenie produktu o wiele więcej funkcji, przy czym osobiście nie jestem przytłoczony tymi rzeczami, ponieważ ograniczenie interakcji do minimum również zmniejsza liczbę miejsc, które mogą może nawet zmienić stan aplikacji i spowodować znaczne skutki uboczne.

Może zmienić coś w ten sposób (gdzie wszystko na diagramie ma funkcjonalność, i oczywiście scenariusz w świecie rzeczywistym miałby wiele, wiele razy więcej obiektów, a jest to diagram „interakcji”, a nie sprzężony, jako sprzężenie pomiędzy nimi byłyby abstrakcje):

wprowadź opis zdjęcia tutaj

... do tego, gdzie funkcjonują tylko systemy (niebieskie komponenty są teraz tylko danymi, a teraz jest to schemat sprzęgania):

wprowadź opis zdjęcia tutaj

Pojawiają się przemyślenia na ten temat i być może sposób na ujęcie niektórych z tych korzyści w bardziej zgodny kontekst OOP, który jest bardziej kompatybilny z SOLID, ale jeszcze nie do końca znalazłem projekty i słowa, i znajduję to trudne, ponieważ terminologia była przyzwyczajona do rzucania wszystkimi związanymi bezpośrednio z OOP. Próbuję to rozgryźć, czytając odpowiedzi ludzi tutaj, a także staram się sformułować własne, ale są pewne rzeczy bardzo interesujące w naturze ECS, których nie byłem w stanie idealnie położyć na tym palca może mieć szersze zastosowanie nawet do architektur, które go nie używają. Mam również nadzieję, że ta odpowiedź nie wyjdzie jako promocja ECS! Uważam to za bardzo interesujące, ponieważ projektowanie ECS naprawdę zmieniło moje myśli drastycznie,


źródło
Podoba mi się różnica między interakcjami a sprzężeniem. Chociaż twój pierwszy schemat interakcji jest zaciemniony. Jest to wykres płaski, więc nie ma potrzeby przekraczania strzał.
D Drmmr
2

Zasady SOLID są dobre, ale KISS i YAGNI mają pierwszeństwo w przypadku konfliktów. Celem SOLID jest zarządzanie złożonością, ale jeśli zastosowanie SOLID powoduje, że kod staje się bardziej złożony, pokonuje to cel. Na przykład, jeśli masz mały program z zaledwie kilkoma klasami, zastosowanie DI, segregacja interfejsu itp. Może bardzo zwiększyć ogólną złożoność programu.

Zasada otwarta / zamknięta jest szczególnie kontrowersyjna. Zasadniczo mówi, że należy dodać funkcje do klasy, tworząc podklasę lub oddzielną implementację tego samego interfejsu, ale nie zmieniając oryginalnej klasy. Jeśli utrzymujesz bibliotekę lub usługę używaną przez nieskoordynowanych klientów, ma to sens, ponieważ nie chcesz przerywać zachowania, na którym może polegać wychodzący klient. Jeśli jednak modyfikujesz klasę, która jest używana tylko we własnej aplikacji, modyfikacja klasy jest często znacznie prostsza i czystsza.

JacquesB
źródło
Dyskusja na temat OCP ignoruje możliwość rozszerzenia zachowania klasy poprzez kompozycję (np. Użycie obiektu strategii, aby umożliwić zmianę algorytmu używanego przez klasę), co ogólnie uważa się za lepszy sposób na przestrzeganie zasady głównej niż podklasowanie .
Jules
1
Jeśli KISS i YAGNI zawsze mają pierwszeństwo, nadal powinieneś kodować jedynki i zera. Asemblery to coś innego, co może pójść nie tak. Żaden KISS i YAGNI nie wskazują dwóch z wielu kosztów prac projektowych. Koszt ten należy zawsze równoważyć z korzyściami wynikającymi z wszelkich prac projektowych. SOLID ma zalety, które w niektórych przypadkach rekompensują koszty. Nie ma prostego sposobu na określenie, kiedy przekroczyłeś linię.
candied_orange 30.01.16
@CandiedOrange: Nie zgadzam się, że kod maszynowy jest prostszy niż kod w języku wysokiego poziomu. Kod maszynowy zawiera wiele przypadkowej złożoności z powodu braku abstrakcji, więc zdecydowanie KISS nie decyduje się na napisanie typowej aplikacji w kodzie maszynowym.
JacquesB
@Jules: Tak, kompozycja jest preferowana, ale nadal wymaga zaprojektowania oryginalnej klasy w taki sposób, aby można było użyć kompozycji do jej dostosowania, dlatego musisz założyć, jakie aspekty należy dostosować w przyszłości i jakie aspekty nie można dostosowywać. W niektórych przypadkach jest to konieczne (np. Piszesz bibliotekę do dystrybucji), ale wiąże się to z pewnym kosztem, dlatego stosowanie SOLID nie zawsze jest optymalne.
JacquesB
1
@JacquesB - true. A OCP jest w swej istocie przeciwny YAGNI; jakkolwiek dążysz do jego osiągnięcia, wymaga to zaprojektowania punktów rozszerzenia, aby umożliwić późniejszą poprawę. Znalezienie odpowiedniego punktu równowagi między tymi dwoma ideałami jest ... trudne.
Jules
1

Z mojego doświadczenia:

Duże klasy są wykładniczo trudniejsze do utrzymania, ponieważ stają się większe.

Małe klasy są łatwiejsze w utrzymaniu, a trudność w utrzymaniu zbioru małych klas rośnie arytmetycznie do liczby klas.

Czasami duże klasy są nieuniknione, duży problem czasami wymaga dużej klasy, ale są znacznie trudniejsze do odczytania i zrozumienia. W przypadku dobrze nazwanych mniejszych klas wystarczy sama nazwa do zrozumienia, nie trzeba nawet patrzeć na kod, chyba że istnieje bardzo specyficzny problem z klasą.

James Anderson
źródło
0

Zawsze trudno mi wytyczyć granicę między „S” bryły a (być może staroświecką) potrzebą, aby obiekt miał całą wiedzę o sobie.

15 lat temu współpracowałem z programistami Deplhi, którzy mieli świętego Graala, aby mieć klasy zawierające wszystko. Tak więc w przypadku kredytu hipotecznego można dodać ubezpieczenia i płatności oraz dochody i pożyczki oraz informacje podatkowe, a także obliczyć podatek do zapłaty, podatek do odliczenia, a może on ulec serializacji, utrzymać się na dysku itp.

A teraz mam ten sam obiekt podzielony na wiele i wiele mniejszych klas, które mają tę zaletę, że można je testować jednostkowo znacznie łatwiej i lepiej, ale nie dają dobrego obrazu całego modelu biznesowego.

I tak, wiem, że nie chcesz wiązać swoich obiektów z bazą danych, ale możesz wstrzyknąć repozytorium w dużej klasie hipotecznej, aby to rozwiązać.

Poza tym nie zawsze łatwo jest znaleźć dobre nazwy dla klas, które robią tylko małe rzeczy.

Nie jestem więc pewien, czy „S” jest zawsze dobrym pomysłem.

Michel
źródło