Jak uniknąć niekończącego się iterowania poprzez równie nieoptymalne projekty?

10

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.
Jonathan
źródło
3
Nie „popieraj filozofii OO”. To narzędzie, a nie ważna decyzja życiowa. Używaj go, gdy pomaga i nie używaj go, gdy nie pomaga.
user253751
Jeśli utkniesz w myśleniu o pewnych wzorach, może spróbuj napisać umiarkowanie skomplikowany projekt w C, ciekawe będzie, jak wiele możesz zrobić bez tych wzorców.
user253751
2
Oh C ma wzory. Są to po prostu bardzo różne wzory. :)
candied_orange
Usunąłem większość tych błędnych interpretacji w edycji
Jonathan
Pojawianie się tych problemów raz na jakiś czas jest normalne. Ich częste występowanie nie powinno tak być. Najprawdopodobniej używasz niewłaściwego paradygmatu programowania dla problemu lub twoje projekty są mieszanką paradygmatów w niewłaściwych miejscach.
Dunk

Odpowiedzi:

8

Kiedy podejmuję taką trudną decyzję, zazwyczaj zadaję sobie trzy pytania:

  1. Jakie są zalety i wady wszystkich dostępnych rozwiązań?

  2. Czy istnieje rozwiązanie, którego jeszcze nie rozważałem?

  3. 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.

cmaster - przywróć monikę
źródło
Mógłbym zaznaczyć to jako odpowiedź, to jest dla mnie najbardziej sensowne i wydaje się, że istnieje konsensus w sprawie ponownej oceny samego problemu.
Jonathan
Podoba mi się również to, jak podzieliłeś to na etapy, więc istnieje pewna luźna procedura oceny sytuacji.
Jonathan
OP, widzę, że utknąłeś w mechanice rozwiązania kodu, więc tak, „równie dobrze możesz zaznaczyć tę odpowiedź”. Najlepszy kod, który napisaliśmy, był po bardzo starannej analizie wymagań, pochodnych wymagań, diagramów klas, interakcji klasowych itp. Dzięki Bogu oparliśmy się wszystkim pieszczotom z tanich miejsc (czytaj zarządzanie i współpracownicy): „Ty „spędzam zbyt dużo czasu na projektowaniu”, „pospiesz się i zacznij kodować!”, „to zbyt wiele klas!” itd. Kiedy już do tego doszliśmy, programowanie było przyjemnością - więcej niż zabawą. Obiektywnie był to najlepszy kod w całym projekcie.
radarbob
13

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:

Staje się to bardzo trudne, gdy każde podejście wydaje się mieć tyle problemów, co każde inne.

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ć .

Trudno również zaakceptować fakt, że kod stanie się bardzo problematyczny, niezależnie od tego, jaki wzór projektowy lub podejście zostanie zastosowane.

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.

Powoduje to utratę motywacji, ponieważ „czysty” kod czasami przestaje być opcją.

Idealny jest wrogiem dobra. Spraw, by coś zadziałało, a następnie dokonaj refaktoryzacji.

Czy są jakieś podejścia do projektowania, które mogą pomóc zminimalizować ten problem? Czy istnieje przyjęta praktyka dotycząca tego, co należy zrobić w takiej sytuacji?

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 .

Telastyn
źródło
Pomyślałem, że powinienem powiedzieć, że usunąłem kilka błędnych interpretacji w edycji. Generalnie wykonuję podejście „uruchom to” i refaktoryzację. Jednak z mojego doświadczenia wynika, że ​​często prowadzi to do długu technicznego. Na przykład, jeśli po prostu coś działa, ale wtedy ja lub inni budujemy na nim, niektóre z tych rzeczy mogą mieć nieuniknione zależności, które również wymagają mnóstwa refaktoryzacji. Oczywiście nie zawsze możesz to wiedzieć z góry. Ale jeśli już wiesz, że to podejście jest wadliwe, czy powinieneś go uruchomić, a następnie iteracyjnie zreformować przed zbudowaniem kodu?
Jonathan
@Jathanathan - wszystkie projekty są serią kompromisów. Tak, powinieneś natychmiast iterować, jeśli projekt jest wadliwy i masz wyraźny sposób na jego poprawę. Jeśli nie ma wyraźnej poprawy, przestań się pieprzyć.
Telastyn
„obciążyli swoje obowiązki w sposób nienaturalny dla danego problemu. Żadna dobra opcja nie może być zapachem, że coś jest zasadniczo nie tak z twoim podejściem”. Założę się o to. Niektórzy programiści nigdy nie napotykają problemów opisanych w OP, a inni wydają się to robić dość często. Często rozwiązanie nie jest łatwe do zobaczenia, gdy zostało postawione przez pierwotnego programistę, ponieważ już uprzedzili projekt w sposobie ujęcia problemu. Czasami jedynym sposobem na znalezienie dość oczywistego rozwiązania jest rozpoczęcie od samego początku wymagań projektowych.
Dunk
8

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).

Pieter B.
źródło
4

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

Doktor Brown
źródło
1
Jest to również bardzo pomocne. Małe refaktoryzowanie krokowe lub scratching jest dobrym pomysłem, ponieważ staje się czymś, co można stopniowo ukończyć wraz z innymi pracami, nie utrudniając go zbytnio. Chciałbym móc zatwierdzić wiele odpowiedzi!
Jonathan
1

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!”.

 * Keep It Simple, Stupid
 ** You Ain't Gonna Need It

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.

Simon B.
źródło
Wyjaśniłem trochę tego w edycji, ale ogólnie odnoszę się dokładnie do tych problemów, w których nie ma prostego rozwiązania, które nie jest dostępne.
Jonathan
Ach miło, tak, wydaje się, że istnieje konsensus w sprawie wycofania się i ponownej oceny problemu. To jest dobry pomysł.
Jonathan
1

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.

użytkownik1118321
źródło