Prawdopodobnie tak jak wielu, często napotykam bóle głowy z problemami projektowymi, w których na przykład istnieje pewien wzór / podejście projektowe, które wydaje się intuicyjnie pasować do problemu i przynosi pożądane korzyści. Bardzo często występuje pewne zastrzeżenie, które utrudnia wdrożenie wzorca / podejścia bez jakiejś pracy, która następnie neguje korzyści wynikające z wzorca / podejścia. Mogę bardzo łatwo skończyć z wieloma wzorcami / podejściami, ponieważ przewidywalnie prawie wszystkie mają pewne bardzo istotne zastrzeżenia w rzeczywistych sytuacjach, w których po prostu nie jest łatwe rozwiązanie.
Przykład:
Dam ci hipotetyczny przykład oparty luźno na prawdziwym, z którym się ostatnio spotkałem. Powiedzmy, że chcę używać kompozycji zamiast dziedziczenia, ponieważ hierarchie dziedziczenia utrudniały skalowalność kodu w przeszłości. Mogę zmienić kod, ale stwierdzę, że istnieją pewne konteksty, w których nadklasa / klasa podstawowa po prostu musi wywołać funkcjonalność podklasy, pomimo prób jej uniknięcia.
Kolejne najlepsze podejście wydaje się polegać na wdrożeniu wzoru połowy delegata / obserwatora i wzoru połowy składu, aby nadklasa mogła przekazać zachowanie lub aby podklasa mogła obserwować zdarzenia nadklasy. Wtedy klasa jest mniej skalowalna i łatwa w utrzymaniu, ponieważ niejasne jest, w jaki sposób należy ją rozszerzyć, a także trudno jest rozszerzyć istniejących słuchaczy / delegatów. Również informacje nie są dobrze ukryte, ponieważ trzeba znać implementację, aby zobaczyć, jak rozszerzyć nadklasę (chyba że bardzo intensywnie używasz komentarzy).
Więc po tym mogę zdecydować się po prostu całkowicie wykorzystać obserwatorów lub delegatów, aby uniknąć wad związanych z intensywnym mieszaniem podejść do. Jednak wiąże się to z własnymi problemami. Na przykład może się okazać, że potrzebuję obserwatorów lub delegatów do rosnącej liczby zachowań, dopóki nie będę potrzebować obserwatorów / delegatów do praktycznie każdego zachowania. Jedną z opcji może być jeden wielki odbiornik / delegat dla wszystkich zachowań, ale wtedy klasa implementująca kończy się wieloma pustymi metodami itp.
Potem mógłbym spróbować innego podejścia, ale jest z tym tyle samo problemów. Potem następny, następny po itd.
Ten iteracyjny proces staje się bardzo trudny, gdy każde podejście wydaje się mieć tyle problemów, co inne, i prowadzi do pewnego rodzaju paraliżu decyzji projektowych . Trudno jest również zaakceptować fakt, że kod stanie się równie problematyczny, niezależnie od zastosowanego wzorca projektowego lub podejścia. Jeśli skończę w takiej sytuacji, czy to oznacza, że sam problem wymaga ponownego przemyślenia? Co robią inni, gdy napotykają taką sytuację?
Edycja: Wydaje się, że istnieje kilka interpretacji pytania, które chcę wyjaśnić:
- Całkowicie usunąłem OOP z pytania, ponieważ okazuje się, że tak naprawdę nie jest ono specyficzne dla OOP, a ponadto zbyt łatwo jest źle zinterpretować niektóre komentarze, które przekazałem na temat OOP.
- Niektórzy twierdzą, że powinienem zastosować podejście iteracyjne i wypróbować różne wzorce lub odrzucić wzór, gdy przestanie on działać. Jest to proces, do którego przede wszystkim chciałem się odnieść. Pomyślałem, że to wynika z przykładu, ale mogłem to wyjaśnić, więc zredagowałem to pytanie.
Odpowiedzi:
Kiedy podejmuję taką trudną decyzję, zazwyczaj zadaję sobie trzy pytania:
Jakie są zalety i wady wszystkich dostępnych rozwiązań?
Czy istnieje rozwiązanie, którego jeszcze nie rozważałem?
Co najważniejsze:
jakie dokładnie są moje wymagania? Nie wymagania na papierze, prawdziwe podstawowe?
Czy mogę w jakiś sposób przeformułować problem / dostosować jego wymagania, aby umożliwić proste, bezpośrednie rozwiązanie?
Czy mogę reprezentować moje dane w inny sposób, aby umożliwić takie proste i proste rozwiązanie?
To są pytania o podstawy postrzeganego problemu. Może się okazać, że próbujesz rozwiązać zły problem. Twój problem może mieć określone wymagania, które pozwalają na znacznie prostsze rozwiązanie niż w przypadku ogólnym. Podejmij pytanie o sformułowanie problemu!
Uważam, że bardzo ważne jest, aby przemyśleć wszystkie trzy pytania przed kontynuowaniem. Idź na spacer, przemierz swoje biuro, zrób wszystko, aby naprawdę zastanowić się nad odpowiedziami na te pytania. Jednak udzielenie odpowiedzi na te pytania nie powinno zająć zbyt dużo czasu. Właściwe czasy mogą wynosić od 15 minut do około tygodnia, zależą od zła rozwiązań, które już znalazłeś, i ich wpływu na całość.
Zaletą tego podejścia jest to, że czasami znajdziesz zaskakująco dobre rozwiązania. Eleganckie rozwiązania. Rozwiązania warte czasu, który zainwestowałeś w odpowiedź na te trzy pytania. I nie znajdziesz tego rozwiązania, jeśli natychmiast wprowadzisz następną iterację.
Oczywiście czasami wydaje się, że nie istnieje dobre rozwiązanie. W takim przypadku tkwisz w odpowiedziach na pytanie pierwsze, a dobro jest po prostu najmniej złe. W takim przypadku warto poświęcić czas na udzielenie odpowiedzi na te pytania, ponieważ możliwe jest uniknięcie iteracji, które na pewno się nie powiodą. Po prostu upewnij się, że wrócisz do kodowania w odpowiednim czasie.
źródło
Po pierwsze - wzory to użyteczne abstrakcje, a nie koniec - cały projekt, nie mówiąc już o projektowaniu OO.
Po drugie - nowoczesny OO jest wystarczająco inteligentny, aby wiedzieć, że nie wszystko jest przedmiotem. Czasami użycie zwykłych starych funkcji lub nawet skryptów w stylu imperatywnym da lepsze rozwiązanie niektórych problemów.
Teraz do rzeczy:
Dlaczego? Gdy masz wiele podobnych opcji, Twoja decyzja powinna być łatwiejsza! Nie stracisz dużo, wybierając opcję „zła”. I tak naprawdę kod nie jest naprawiony. Spróbuj czegoś, zobacz, czy to dobrze. Iterować .
Twarde orzechy. Trudne problemy - rzeczywiste matematyczne trudne problemy są po prostu trudne. Zapewne trudne. Nie ma dla nich dosłownie dobrego rozwiązania. I okazuje się, że proste problemy nie są tak naprawdę cenne.
Ale bądź ostrożny. Zbyt często widziałem ludzi sfrustrowanych brakiem dobrych opcji, ponieważ utknęli na problemie w określony sposób lub że ograniczali swoje obowiązki w sposób nienaturalny dla danego problemu. „Brak dobrej opcji” może być zapachem, że w twoim podejściu jest coś zasadniczo nie tak.
Idealny jest wrogiem dobra. Spraw, by coś zadziałało, a następnie dokonaj refaktoryzacji.
Jak wspomniałem, iteracyjny rozwój ogólnie minimalizuje ten problem. Gdy już coś zaczniesz działać, lepiej znasz problem stawiający cię w lepszej pozycji do rozwiązania tego problemu. Masz rzeczywisty kod do przejrzenia i oceny, a nie jakiś abstrakcyjny projekt, który wydaje się niewłaściwy .
źródło
Opisana sytuacja wygląda jak podejście oddolne. Weź liść, spróbuj go naprawić i dowiedz się, że jest podłączony do gałęzi, która sama jest również połączona z inną gałęzią itp.
To tak, jakby próbować zbudować samochód zaczynając od opony.
Musisz cofnąć się o krok i spojrzeć na większy obraz. Jak ten liść mieści się w ogólnym projekcie? Czy to wciąż aktualne i prawidłowe?
Jak wyglądałby moduł, gdybyś zaprojektował i wdrożył go od zera? Jak daleko od tego „ideału” jest twoje obecne wdrożenie.
W ten sposób masz większy obraz tego, nad czym pracować. (Lub jeśli zdecydujesz, że to za dużo pracy, jakie są problemy).
źródło
Twój przykład opisuje sytuację, która zwykle pojawia się przy większych fragmentach starszego kodu i gdy próbujesz dokonać „zbyt dużego” refaktoryzacji.
Najlepsza rada, jaką mogę ci dać w tej sytuacji:
nie staraj się osiągnąć swojego głównego celu w jednym „wielkim wybuchu” ,
dowiedz się, jak poprawić swój kod w mniejszych krokach!
Oczywiście łatwiej to napisać niż zrobić, więc jak to zrobić w rzeczywistości? Cóż, potrzebujesz praktyki i doświadczenia, to zależy w dużej mierze od przypadku i nie ma twardej i szybkiej zasady mówiącej „rób to lub tamto”, która pasuje do każdej sprawy. Ale pozwól, że użyję twojego hipotetycznego przykładu. „nadpisanie składu” nie jest celem projektowym „wszystko albo nic”, jest idealnym kandydatem do osiągnięcia w kilku małych krokach.
Powiedzmy, że zauważyłeś, że „kompozycja nad dziedziczeniem” jest właściwym narzędziem dla sprawy. Muszą być pewne oznaki, że jest to rozsądny cel, w przeciwnym razie nie wybrałbyś tego. Załóżmy więc, że w nadklasie jest wiele funkcji, które są po prostu „wywoływane” z podklas, więc ta funkcja jest kandydatem na nie pozostanie w tej nadklasie.
Jeśli zauważysz, że nie możesz natychmiast usunąć nadklasy z podklas, możesz zacząć od przeredagowania nadklasy na mniejsze komponenty, zawierające w sobie funkcje wspomniane powyżej. Zacznij od owoców o najniższym zawieszeniu, najpierw wydobądź kilka prostszych składników, które już sprawią, że twoja nadklasa będzie mniej złożona. Im mniejsza staje się nadklasa, tym łatwiejsze stają się dodatkowe refaktoryzacje. Użyj tych składników z podklas, a także z nadklasy.
Jeśli masz szczęście, pozostały kod w nadklasie stanie się tak prosty w całym tym procesie, że będziesz mógł usunąć nadklasę z podklas bez żadnych dalszych problemów. Albo zauważysz, że utrzymanie nadklasy nie stanowi już problemu, ponieważ już wyodrębniłeś wystarczającą ilość kodu do komponentów, które chcesz ponownie wykorzystać bez dziedziczenia.
Jeśli nie jesteś pewien, od czego zacząć, ponieważ nie wiesz, czy refaktoryzacja okaże się prosta, czasami najlepszym rozwiązaniem jest przeprowadzenie refaktoryzacji .
Oczywiście twoja prawdziwa sytuacja może być bardziej skomplikowana. Więc ucz się, zbieraj doświadczenia i bądź cierpliwy, zdobycie tego prawa zajmuje lata. Tutaj mogę polecić dwie książki, być może okażą się pomocne:
Refaktoryzacja przez Fowler: opisuje pełny katalog bardzo małych refaktoryzacji.
Efektywna praca ze starszym kodem firmy Feathers: daje doskonałą poradę, jak radzić sobie z dużymi fragmentami źle zaprojektowanego kodu i uczynić go bardziej testowalnym w mniejszych krokach
źródło
Czasami dwie najlepsze zasady projektowania to KISS * i YAGNI **. Nie odczuwaj potrzeby wtłaczania każdego znanego wzoru do programu, który musi po prostu wydrukować „Witaj świecie!”.
Edytuj po aktualizacji pytania (i do pewnego stopnia odzwierciedlając to, co mówi Pieter B):
Czasami wcześnie podejmujesz decyzję architektoniczną, która prowadzi do konkretnego projektu, który prowadzi do różnego rodzaju brzydoty, gdy próbujesz go wdrożyć. Niestety w tym momencie „właściwym” rozwiązaniem jest wycofanie się i ustalenie, w jaki sposób osiągnąłeś tę pozycję. Jeśli nie widzisz jeszcze odpowiedzi, cofaj się, aż to zrobisz.
Ale jeśli praca nad tym byłaby nieproporcjonalna, to pragmatyczna decyzja polega na znalezieniu najmniej brzydkiego obejścia problemu.
źródło
Kiedy jestem w takiej sytuacji, pierwszą rzeczą, którą robię, jest zatrzymanie. Przełączam się na inny problem i pracuję nad tym przez chwilę. Może godzina, może dzień, może dłużej. Nie zawsze jest to opcja, ale moja podświadomość będzie działać na rzeczy, podczas gdy mój świadomy mózg robi coś bardziej produktywnego. W końcu wracam do niego ze świeżymi oczami i próbuję ponownie.
Inną rzeczą, którą robię, jest zapytanie kogoś mądrzejszego ode mnie. Może to polegać na pytaniu na Stack Exchange, czytaniu artykułu w Internecie na ten temat lub pytaniu kolegi, który ma większe doświadczenie w tej dziedzinie. Często metoda, która moim zdaniem jest właściwa, okazuje się całkowicie błędna w stosunku do tego, co próbuję zrobić. Zrobiłam błąd w niektórych aspektach problemu i tak naprawdę nie pasuje do wzoru, który moim zdaniem tak jest. Kiedy tak się dzieje, bardzo pomocne może być powiedzenie komuś innemu „Wiesz, to wygląda bardziej jak ...”.
Z powyższym związane jest projektowanie lub debugowanie przez spowiedź. Idziesz do kolegi i mówisz: „Powiem ci o moim problemie, a następnie wyjaśnię ci rozwiązania, które mam. Wskazujesz problemy w każdym podejściu i sugerujesz inne podejścia. . ” Często zanim druga osoba się odezwie, jak wyjaśniam, zaczynam zdawać sobie sprawę, że jedna ścieżka, która wydawała się równa innym, jest w rzeczywistości znacznie lepsza lub gorsza, niż początkowo myślałem. Wynikowa rozmowa może albo wzmocnić to postrzeganie, albo wskazać nowe rzeczy, o których nie myślałem.
Więc TL; DR: Zrób sobie przerwę, nie zmuszaj jej, poproś o pomoc.
źródło