SOLID vs. Unikanie przedwczesnej abstrakcji


27

Rozumiem, co SOLID ma osiągnąć i używać go regularnie w sytuacjach, w których modułowość jest ważna, a jej cele są wyraźnie przydatne. Jednak dwie rzeczy uniemożliwiają mi konsekwentne stosowanie go w mojej bazie kodu:

  • Chcę uniknąć przedwczesnej abstrakcji. Z mojego doświadczenia wynika, że ​​rysowanie linii abstrakcji bez konkretnych przypadków użycia (takich, które istnieją obecnie lub w dającej się przewidzieć przyszłości) prowadzi do ich narysowania w niewłaściwych miejscach. Kiedy próbuję zmodyfikować taki kod, linie abstrakcji przeszkadzają, a nie pomagają. Dlatego mam tendencję do błądzenia po stronie nie rysowania żadnych linii abstrakcji, dopóki nie mam pojęcia, gdzie byłyby przydatne.

  • Trudno mi usprawiedliwić zwiększenie modułowości dla samego siebie, jeśli sprawia, że ​​mój kod jest bardziej szczegółowy, trudniejszy do zrozumienia itp. I nie eliminuje żadnego powielania. Uważam, że prosty, ściśle sprzężony kod proceduralny lub obiektowy Boga jest czasem łatwiejszy do zrozumienia niż bardzo dobrze skonstruowany kod ravioli, ponieważ przepływ jest prosty i liniowy. O wiele łatwiej jest pisać.

Z drugiej strony ten sposób myślenia często prowadzi do obiektów Boga. Ogólnie zachowuję je ostrożnie, dodając wyraźne linie abstrakcji tylko wtedy, gdy widzę pojawiające się wyraźne wzory. Co, jeśli w ogóle, jest nie tak z obiektami Boga i ściśle powiązanym kodem, jeśli nie potrzebujesz wyraźnie modułowości, nie ma znaczącego powielania, a kod jest czytelny?

EDYCJA: Jeśli chodzi o indywidualne zasady SOLID, chciałem podkreślić, że Zastępstwo Liskowa jest IMHO formalizacją zdrowego rozsądku i powinno być stosowane wszędzie, ponieważ abstrakcje nie mają sensu, jeśli nie są. Ponadto, każda klasa powinna ponosić jedną odpowiedzialność na pewnym poziomie abstrakcji, chociaż może to być bardzo wysoki poziom ze szczegółami implementacji zatłoczonymi w jedną ogromną klasę 2000 linii. Zasadniczo twoje abstrakcje powinny mieć sens tam, gdzie wybierzesz abstrakty. Zasady, które kwestionuję w przypadkach, w których modułowość nie jest wyraźnie użyteczna, to otwarte-zamknięte, segregacja interfejsu, a zwłaszcza inwersja zależności, ponieważ dotyczą one modułowości, a nie tylko abstrakcji mają sens.


10
@ S.Lott: Kluczowym słowem jest „więcej”. Właśnie w tym celu wymyślono YAGNI. Zwiększanie modułowości lub zmniejszanie sprzężenia dla samego siebie jest programowaniem kultowym.
Mason Wheeler,

3
@ S.Lott: Powinno to być oczywiste z kontekstu. Jak czytam, przynajmniej „w danym kodzie istnieje więcej niż obecnie”.
Mason Wheeler,

3
@ S.Lott: Jeśli nalegasz na bycie pedantycznym, „więcej” odnosi się do czegoś więcej niż obecnie. Implikacja jest taka, że ​​coś, czy to kod, czy wstępny projekt, już istnieje i zastanawiasz się, jak to zmienić.
dsimcha

5
@ back2dos: Zgadza się, ale jestem sceptycznie nastawiony do zaprojektowania czegoś, co można by rozszerzyć, bez wyraźnego pojęcia, jak to się wydłuży. Bez tych informacji linie abstrakcji prawdopodobnie znajdą się we wszystkich niewłaściwych miejscach. Jeśli nie wiesz, gdzie prawdopodobnie będą przydatne linie abstrakcji, sugeruję, abyś napisał najbardziej zwięzły kod, który działa i jest czytelny, nawet jeśli wrzucisz tam kilka obiektów Boga.
dsimcha

2
@dsimcha: Masz szczęście, jeśli wymagania zmienią się po napisaniu fragmentu kodu, ponieważ zwykle zmieniają się podczas pisania . Dlatego implementacje migawkowe nie są odpowiednie do przetrwania rzeczywistego cyklu życia oprogramowania. Ponadto SOLID nie wymaga od ciebie jedynie ślepej abstrakcji. Kontekst abstrakcji jest definiowany przez odwrócenie zależności. Zmniejszasz zależność każdego modułu do najprostszej i wyraźnej abstrakcji innych usług, na których się opiera. Nie jest to arbitralne, sztuczne ani złożone, ale dobrze określone, wiarygodne i proste.
back2dos,

Odpowiedzi:


8

Oto proste zasady, które możesz zastosować, aby pomóc Ci zrozumieć, jak zrównoważyć projekt systemu:

  • Zasada pojedynczej odpowiedzialności: Sw SOLID. Możesz mieć bardzo duży obiekt pod względem liczby metod lub ilości danych i nadal przestrzegać tej zasady. Weźmy na przykład obiekt Ruby String. Ten obiekt ma więcej metod, niż można potrząsnąć kijem, ale nadal ma tylko jedną odpowiedzialność: przytrzymaj ciąg tekstu dla aplikacji. Gdy tylko twój obiekt zacznie przyjmować na siebie nową odpowiedzialność, zacznij się nad tym intensywnie zastanawiać. Problem z utrzymaniem dotyczy pytania „gdzie miałbym znaleźć ten kod, gdybym miał z nim później problemy?”
  • Liczy się prostota: Albert Einstein powiedział „Uczyń wszystko tak prostym, jak to możliwe, ale nie prostszym”. Czy naprawdę potrzebujesz tej nowej metody? Czy możesz osiągnąć to, czego potrzebujesz dzięki istniejącej funkcjonalności? Jeśli naprawdę uważasz, że musi istnieć nowa metoda, czy możesz zmienić istniejącą metodę, aby zaspokoić wszystko, czego potrzebujesz? W zasadzie refaktorem, gdy budujesz nowe rzeczy.

Zasadniczo próbujesz zrobić wszystko, aby nie zastrzelić się w stopę, jeśli przyjdzie czas na utrzymanie oprogramowania. Jeśli twoje duże obiekty są rozsądnymi abstrakcjami, to nie ma powodu, by je dzielić tylko dlatego, że ktoś wymyślił metrykę, która mówi, że klasa nie powinna być większa niż X linii / metod / właściwości / itp. Jeśli tak, to są wytyczne, a nie twarde i szybkie reguły.


3
Dobry komentarz Jak omówiono w innych odpowiedziach, jednym problemem z „pojedynczą odpowiedzialnością” jest to, że odpowiedzialność klasy można opisać na różnych poziomach abstrakcji.
dsimcha

5

Myślę, że w większości odpowiedziałeś na własne pytanie. SOLID to zestaw wskazówek, jak refaktoryzować kod, gdy koncepcje muszą być przenoszone na wyższy poziom abstrakcji.

Jak wszystkie inne dyscypliny twórcze, nie ma absolutów - po prostu kompromisy. Im częściej to robisz, tym „łatwiej” staje się decydowanie, kiedy jest wystarczająca dla bieżącej problematycznej domeny lub wymagań.

Powiedziawszy to - abstrakcja jest sercem rozwoju oprogramowania - więc jeśli nie ma dobrego powodu, aby tego nie robić, to praktyka sprawi, że będziesz w tym lepszy, a ty poczujesz odwagę za kompromisy. Więc faworyzuj to, chyba że stanie się szkodliwe.


5
I want to avoid premature abstraction.In my experience drawing abstraction lines without concrete use cases... 

Jest to częściowo słuszne, a częściowo błędne.

Źle
To nie jest powód, aby uniemożliwiać stosowanie zasad OO / SOLID. To tylko zapobiega ich stosowaniu zbyt wcześnie.

Który jest...

Prawo
Nie refaktoryzuj kodu, dopóki nie można go refaktoryzować; kiedy wymagania są spełnione. Lub, gdy „przypadki użycia” są dostępne, jak mówisz.

I find simple, tightly coupled procedural or God object code is sometimes easier to understand than very well-factored...

Podczas pracy w pojedynkę lub w silosie
Problem z nie robieniem OO nie jest od razu oczywisty. Po napisaniu pierwszej wersji programu:

Code is fresh in your head
Code is familiar
Code is easy to mentally compartmentalize
You wrote it

3/4 z tych dobrych rzeczy szybko umiera w krótkim czasie.

Wreszcie rozpoznajesz, że masz obiekty Boga (obiekty z wieloma funkcjami), więc rozpoznajesz poszczególne funkcje. Hermetyzować. Umieść je w folderach. Raz na jakiś czas korzystaj z interfejsów. Zrób sobie przysługę za długoterminową konserwację. Zwłaszcza, że ​​metody nierefrakcjonowane mają tendencję do wzdęcia w nieskończoność i stają się metodami Boga.

W zespole
Problemy z nie robieniem OO są natychmiast zauważalne. Dobry kod OO jest co najmniej w pewnym stopniu samodokumentujący i czytelny. Zwłaszcza w połączeniu z dobrym zielonym kodem. Ponadto brak struktury utrudnia rozdzielenie zadań i integracji o wiele bardziej skomplikowane.


Dobrze. Rozumiem niejasno, że moje obiekty robią więcej niż jedną rzecz, ale nie rozumiem bardziej konkretnie, jak pożytecznie je rozdzielić, aby linie abstrakcji znajdowały się we właściwych miejscach, gdy trzeba przeprowadzić konserwację. Wydaje się, że refaktoryzacja to strata czasu, jeśli programista konserwacji będzie prawdopodobnie musiał przeprowadzić znacznie więcej refaktoryzacji, aby w każdym razie uzyskać linie abstrakcji we właściwym miejscu.
dsimcha

Kapsułkowanie i abstrakcja to dwa różne pojęcia. Encapsulation to podstawowa koncepcja OO, która w zasadzie oznacza, że ​​masz klasy. Klasy same w sobie zapewniają modułowość i czytelność. To się przydaje.
P.Brian.Mackey,

Abstrakcja może zmniejszyć koszty konserwacji, jeśli jest prawidłowo zastosowana. Niewłaściwe zastosowanie może zwiększyć konserwację. Wiedza o tym, kiedy i gdzie narysować granice, wymaga solidnego zrozumienia biznesu, struktury klienta i podstawowych aspektów technicznych, jak po prostu „zastosować wzór fabryczny” per se.
P.Brian.Mackey,

3

Nie ma nic złego w dużych obiektach i ściśle sprzężonym kodzie, gdy są one odpowiednie i kiedy nie jest wymagane nic lepiej zaprojektowanego . To tylko kolejny przejaw reguły kciuka zmieniającej się w dogmat.

Luźne sprzężenie i małe, proste obiekty zwykle zapewniają określone korzyści w wielu typowych przypadkach, więc ogólnie warto z nich korzystać. Problem leży w tym, że ludzie, którzy nie rozumieją zasad leżących u podstaw zasad, ślepo próbują zastosować je uniwersalnie, nawet tam, gdzie nie są stosowane.


1
+1. Nadal potrzebuję dobrej definicji tego, co oznacza „odpowiedni”.
jprete,

2
@jprete: Dlaczego? Próba zastosowania bezwzględnych definicji jest częścią przyczyn takich pojęć. Budowanie dobrego oprogramowania wymaga dużego kunsztu. Tym, czego naprawdę potrzebuje IMO, jest doświadczenie i dobra ocena sytuacji.
Mason Wheeler,

4
No tak, ale szeroka definicja pewnego rodzaju jest nadal przydatny.
jprete,

1
Przydatna byłaby definicja odpowiedniej, ale o to właśnie chodzi. Trudno jest uchwycić zgryz, ponieważ można to zastosować tylko w indywidualnych przypadkach. To, czy dokonasz właściwego wyboru w każdej sprawie, zależy w dużej mierze od doświadczenia. Jeśli nie potrafisz wyrazić powodu, dla którego jesteś swoją wysoką kohezją, rozwiązanie monolityczne jest lepsze niż rozwiązanie o niskiej kohezji, wysoce modułowe, „prawdopodobnie” lepiej byłoby mieć niską kohezję i modułowość. Zawsze łatwiej jest refaktoryzować.
Ian

1
Wolę, żeby ktoś ślepo pisał oddzielony kod zamiast ślepo pisał kod spaghetti :)
Boris Yankov

2

Podchodzę do tego bardziej z punktu widzenia You Ain't Gonna Need It. Ten post skupi się w szczególności na jednym elemencie: dziedziczeniu.

W moim początkowym projekcie tworzę hierarchię rzeczy, o których wiem, że będą ich dwie lub więcej. Są szanse, że będą potrzebować dużo tego samego kodu, więc warto zaprojektować go od samego początku. Po wprowadzeniu początkowego kodu i konieczności dodania kolejnych funkcji / funkcji, patrzę na to, co mam i myślę: „Czy coś, co już mam, implementuje tę samą lub podobną funkcję?” Jeśli tak, to najprawdopodobniej pojawi się nowa abstrakcja.

Dobrym przykładem tego jest framework MVC. Zaczynając od zera, tworzysz jedną dużą stronę z kodem z tyłu. Ale potem chcesz dodać kolejną stronę. Jednak jeden kod już zaimplementował dużą logikę wymaganą przez nową stronę. Tak więc wyodrębniasz Controllerklasę / interfejs, który zaimplementuje logikę specyficzną dla tej strony, pozostawiając wspólne elementy w oryginalnym „bogu” za kodem.


1

Jeśli jako programista wiesz, kiedy NIE stosować zasad z powodu przyszłości kodu, to dobrze dla Ciebie.

Mam nadzieję, że SOLID pozostaje w twojej głowie, aby wiedzieć, kiedy trzeba wyodrębnić rzeczy, aby uniknąć złożoności i poprawić jakość kodu, jeśli zacznie on obniżać się do podanych przez ciebie wartości.

Co ważniejsze, pamiętaj, że większość programistów wykonuje codzienną pracę i nie dba o ciebie tak bardzo jak ty. Jeśli początkowo wybierzesz metody działające długo, inni programiści, którzy przyjdą do kodu, pomyślą, kiedy go utrzymają lub rozszerzą? ... Tak, zgadłeś, WIĘKSZE funkcje, DŁUŻSZE klasy uruchomieniowe i WIĘCEJ Bóg obiektów, i nie mają na myśli zasad, aby móc złapać rosnący kod i poprawnie go zamknąć. Twój „czytelny” kod staje się teraz gnijącym bałaganem.

Możesz więc argumentować, że zły programista zrobi to bez względu na to, ale przynajmniej będziesz miał większą przewagę, jeśli wszystko będzie proste i dobrze zorganizowane od samego początku.

Nie mam specjalnie na myśli globalnej „mapy rejestru” lub „Boskiego obiektu”, jeśli są one proste. To naprawdę zależy od projektu systemu, czasem można go uciec i zachować prostotę, a czasem nie.

Nie zapominajmy również, że duże funkcje i Boskie Przedmioty mogą okazać się niezwykle trudne do przetestowania. Jeśli chcesz pozostać zwinny i czuć się „bezpiecznie” podczas przefaktoryzowania i zmiany kodu, potrzebujesz testów. Testy są trudne do napisania, gdy funkcje lub obiekty wykonują wiele czynności.


1
Zasadniczo dobra odpowiedź. Moje jedno zastrzeżenie polega na tym, że jeśli jakiś programista konserwacji pojawi się i zrobi bałagan, to jego / jej wina za brak refaktoryzacji, chyba że takie zmiany wydawały się prawdopodobne na tyle, aby uzasadnić ich planowanie lub kod był tak słabo udokumentowany / przetestowany / itp. . refaktoryzacja byłaby formą szaleństwa.
dsimcha

To prawda, dsimcha. Po prostu pomyślałem w systemie, w którym programista tworzy folder „RequestHandlers”, a każda klasa w tym folderze to moduł obsługi żądań, a następnie następny programista, który pojawia się i myśli: „Muszę wprowadzić nowy moduł obsługi żądań”, obecna infrastruktura prawie zmusza go do zejścia z trasy zgodnie z konwencją. Myślę, że to właśnie chciałem powiedzieć. Ustanowienie oczywistej konwencji od samego początku może poprowadzić nawet najbardziej niedoświadczonych programistów, aby kontynuowali ten schemat.
Martin Blore,

1

Problem z boskim przedmiotem polega na tym, że zazwyczaj możesz z zyskiem rozbić go na kawałki. Z definicji robi więcej niż jedną rzecz. Cały cel podzielenia go na mniejsze klasy polega na tym, abyś mógł mieć klasę, która robi jedną rzecz dobrze (i nie powinieneś również rozszerzać definicji „jednej rzeczy”). Oznacza to, że dla każdej klasy, gdy wiesz, co należy zrobić, powinieneś być w stanie dość łatwo ją przeczytać i powiedzieć, czy robi to poprawnie.

Myślę, że jest coś takiego jak zbyt wiele modułowość i zbyt dużą elastyczność, ale to bardziej z nad-projektowej i ponad-inżynierskiej problem, gdzie jesteś rachunkowości dla wymagań, które nawet nie wiedzą, że klient chce. Wiele pomysłów projektowych ma na celu ułatwienie kontrolowania zmian w sposobie działania kodu, ale jeśli nikt nie spodziewa się zmiany, to nie ma sensu uwzględniać elastyczności.


„Robi więcej niż jedną rzecz” może być trudne do zdefiniowania, w zależności od poziomu abstrakcji, nad którym pracujesz. Można argumentować, że każda metoda z dwoma lub więcej liniami kodu robi więcej niż jedną rzecz. Na przeciwległym końcu spektrum coś złożonego, takiego jak silnik skryptowy, będzie wymagało wielu metod, w których wszystkie wykonują indywidualne „rzeczy”, które nie są bezpośrednio ze sobą powiązane, ale każda z nich stanowi ważną część „meta- rzecz ”, która sprawia, że ​​skrypty działają poprawnie, a dzielenie jest tylko tyle, że można to zrobić bez zepsucia silnika skryptu.
Mason Wheeler,

Jest zdecydowanie spektrum poziomów koncepcyjnych, ale można wybrać punkt na niej: wielkości „klasy, że nie jedno” powinno być takie, że można niemal natychmiast zrozumieć realizację. Tj. Szczegóły implementacji pasują do twojej głowy. Jeśli się powiększy, nie będziesz w stanie zrozumieć całej klasy naraz. Jeśli jest mniejszy, to za daleko abstraktujesz. Ale jak powiedziałeś w innym komentarzu, wiąże się to z wieloma osądami i doświadczeniem.
jprete,

IMHO najlepszym poziomem abstrakcji do myślenia jest poziom, którego oczekujesz od konsumentów obiektu. Załóżmy, że masz obiekt o nazwie, Queryerktóry optymalizuje zapytania do bazy danych, wysyła zapytania do RDBMS i analizuje wyniki w obiekty do zwrócenia. Jeśli jest tylko jeden sposób, aby to zrobić w aplikacji i Queryerjest on hermetycznie zamknięty i hermetyzuje ten jeden sposób, to robi tylko jedną rzecz. Jeśli istnieje wiele sposobów, aby to zrobić, a ktoś może dbać o szczegóły jednej jego części, robi to wiele rzeczy i możesz chcieć to rozdzielić.
dsimcha

1

Używam prostszej wytycznej: jeśli potrafisz napisać testy jednostkowe, a nie masz duplikacji kodu, jest to wystarczająco abstrakcyjne. Ponadto masz dobrą pozycję do późniejszego refaktoryzacji.

Poza tym powinieneś pamiętać o SOLID, ale raczej jako wytyczna niż zasada.


0

Co, jeśli w ogóle, jest nie tak z obiektami Boga i ściśle powiązanym kodem, jeśli nie potrzebujesz wyraźnie modułowości, nie ma znaczącego powielania, a kod jest czytelny?

Jeśli aplikacja jest wystarczająco mała, wszystko można utrzymać. W większych aplikacjach obiekty Boga szybko stają się problemem. Ostatecznie wdrożenie nowej funkcji wymaga modyfikacji 17 obiektów Boga. Ludzie nie radzą sobie zbyt dobrze po 17-etapowych procedurach. Obiekty Boga są ciągle modyfikowane przez wielu programistów i zmiany te muszą być wielokrotnie łączone. Nie chcesz tam iść.


0

Podzielam wasze obawy dotyczące nadmiernej i niewłaściwej abstrakcji, ale niekoniecznie jestem tak zaniepokojony przedwczesną abstrakcją.

To prawdopodobnie brzmi jak sprzeczność, ale jest mało prawdopodobne, aby użycie abstrakcji spowodowało problem, o ile nie przesadzisz z nim zbyt wcześnie - tj. O ile będziesz gotów i będziesz mógł dokonać refaktoryzacji w razie potrzeby.

Oznacza to pewien stopień prototypowania, eksperymentowania i cofania w kodzie.

To powiedziawszy, nie ma prostej reguły deterministycznej, która mogłaby postępować wszystkie problemy. Doświadczenie się liczy, ale musisz je zdobyć, popełniając błędy. I zawsze jest więcej błędów, aby poprzednie błędy nie przygotowały cię na to.

Mimo to potraktuj zasady podręcznika jako punkt wyjścia, a następnie naucz się dużo programowania i zobacz, jak te zasady się sprawdzają. Gdyby można było podać lepsze, bardziej precyzyjne i wiarygodne wytyczne niż SOLID, ktoś prawdopodobnie zrobiłby to teraz - a nawet gdyby tak było, te ulepszone zasady byłyby nadal niedoskonałe, a ludzie pytaliby o ograniczenia w tych .

Dobra robota - jeśli ktokolwiek mógłby zapewnić jasny, deterministyczny algorytm do projektowania i kodowania programów, istniałby tylko jeden ostatni program do napisania przez ludzi - program, który automatycznie zapisywałby wszystkie przyszłe programy bez interwencji człowieka.


0

Rysowanie odpowiednich linii abstrakcji jest czymś, co czerpie się z doświadczenia. Popełnisz przy tym błędy, co jest niezaprzeczalne.

SOLID jest skoncentrowaną formą tego doświadczenia, którą otrzymują ci ludzie, którzy zdobyli to doświadczenie na własnej skórze. Prawie go przybiłeś, kiedy mówisz, że powstrzymasz się od tworzenia abstrakcji, zanim zobaczysz jej potrzebę. Cóż, doświadczenie SOLID zapewnia pomoc w rozwiązywaniu problemów, których nigdy wcześniej nie istniałeś . Ale zaufaj mi ... są bardzo prawdziwe.

SOLID to modułowość, modułowość to łatwość konserwacji, a łatwość utrzymania pozwala na zachowanie humanitarnego harmonogramu pracy, gdy oprogramowanie osiągnie produkcję i klient zacznie zauważać błędy, których nie masz.

Największą zaletą modułowości jest testowalność. Im bardziej modułowy jest system, tym łatwiej jest go testować i tym szybciej można zbudować solidne fundamenty, aby poradzić sobie z trudniejszymi aspektami problemu. Twój problem może nie wymagać tej modułowości, ale sam fakt, że pozwoli ci szybciej tworzyć lepszy kod testowy, nie stanowi problemu, aby dążyć do modułowości.

Zwinność polega na próbie znalezienia delikatnej równowagi między szybką wysyłką a zapewnieniem dobrej jakości. Zwinność nie oznacza, że ​​powinniśmy skrócić narożnik, w rzeczywistości najbardziej udane projekty zwinne, w które byłem zaangażowany, to te, które zwróciły szczególną uwagę na takie wytyczne.


0

Ważnym punktem, który wydaje się być przeoczony, jest to, że SOLID jest praktykowany wraz z TDD. TDD ma tendencję do podkreślania tego, co jest „odpowiednie” w twoim konkretnym kontekście i pomaga złagodzić wiele niejednoznaczności, które wydają się być w radzie.


1
Rozważ rozszerzenie swojej odpowiedzi. W tej chwili łączysz dwie ortogonalne koncepcje i nie jest jasne, w jaki sposób wynik rozwiązuje obawy PO.
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.