Złudzenie duplikacji kodu

56

Zwykle instynkt polega na usunięciu wszelkich duplikacji kodu widocznych w kodzie. Znalazłem się jednak w sytuacji, w której duplikacja jest iluzoryczna .

Bardziej szczegółowo opisując sytuację: tworzę aplikację internetową, a większość widoków jest w zasadzie taka sama - wyświetla listę elementów, które użytkownik może przewijać i wybierać, drugą listę zawierającą wybrane elementy oraz „Zapisz ”, aby zapisać nową listę.

Wydawało mi się, że problem jest łatwy. Jednak każdy widok ma swoje dziwactwa - czasem trzeba coś przeliczyć, czasem trzeba zapisać dodatkowe dane itp. Rozwiązałem je, wstawiając haki wywołania zwrotnego do głównego kodu logicznego.

Jest tak wiele drobnych różnic między widokami, że staje się ono coraz mniej łatwe w utrzymaniu, ponieważ muszę zapewnić wywołania zwrotne dla praktycznie całej funkcjonalności, a główna logika zaczyna wyglądać jak ogromna sekwencja wywołań wywołań zwrotnych. W końcu nie oszczędzam czasu ani kodu, ponieważ każdy widok ma swój kod, który jest wykonywany - wszystko w wywołaniach zwrotnych.

Problemy są następujące:

  • różnice są tak małe, że kod wygląda prawie dokładnie tak samo we wszystkich widokach,
  • jest tak wiele różnic, że kiedy patrzysz na szczegóły, kodowanie nie jest trochę podobne

Jak mam poradzić sobie z tą sytuacją?
Czy posiadanie podstawowej logiki złożonej wyłącznie z wywołań zwrotnych jest dobrym rozwiązaniem?
A może powinienem raczej powielić kod i porzucić złożoność kodu opartego na wywołaniu zwrotnym?

Mael
źródło
38
Zazwyczaj uważam, że pomocne jest początkowe zwolnienie duplikacji. Gdy mam kilka przykładów, łatwiej jest zobaczyć, co jest wspólne, a co nie, i wymyślić sposób, aby podzielić się wspólnymi częściami.
Winston Ewert
7
Bardzo dobre pytanie - jedną rzeczą do rozważenia jest nie tylko fizyczne powielanie kodu, ale także powielanie semantyczne. Jeśli zmiana w jednym fragmencie kodu koniecznie oznacza, że ​​ta sama zmiana byłaby powielona w innych, to prawdopodobnie ta część jest kandydatem do refaktoryzacji lub abstrakcji. Czasami można znormalizować się do punktu, w którym faktycznie się uwięzisz, więc rozważę również praktyczne implikacje dla traktowania duplikacji jako semantycznie odrębne - mogą być one zrównoważone konsekwencjami próby deduplikacji.
Ant P
Pamiętaj, że możesz ponownie użyć tylko kodu, który robi to samo. Jeśli Twoja aplikacja robi różne rzeczy na różnych ekranach, będzie wymagała różnych wywołań zwrotnych. Żadnych „i”, „ale”.
corsiKa
13
Moja osobista ogólna zasada brzmi: jeśli dokonam zmiany kodu w jednym miejscu, czy to błąd, jeśli nie dokonuję dokładnie takiej samej zmiany w innym miejscu? Jeśli tak, to zły rodzaj powielania. Jeśli nie jestem pewien, wybierz na razie coś bardziej czytelnego. W twoim przykładzie różnice w zachowaniu są celowe i nie są uważane za błędy, dlatego pewne powielanie jest w porządku.
Ixrec
Być może zainteresuje Cię programowanie zorientowane na aspekt.
Ben Jackson,

Odpowiedzi:

53

Ostatecznie musisz dokonać oceny, czy połączyć podobny kod, aby wyeliminować powielanie.

Wydaje się, że istnieje niefortunna tendencja do przyjmowania zasad takich jak „Nie powtarzaj się” jako zasad, które muszą być przestrzegane przez cały czas. W rzeczywistości nie są to uniwersalne zasady, ale wytyczne, które powinny pomóc ci pomyśleć i opracować dobry projekt.

Jak wszystko w życiu, należy wziąć pod uwagę korzyści w stosunku do kosztów. Ile zduplikowanych kodów zostanie usuniętych? Ile razy kod się powtarza? Ile wysiłku będzie wymagało napisanie bardziej ogólnego projektu? Ile prawdopodobnie opracujesz kod w przyszłości? I tak dalej.

Nie znając swojego konkretnego kodu, jest to niejasne. Być może istnieje bardziej elegancki sposób na usunięcie duplikacji (taki jak sugerowany przez LindaJeanne). A może po prostu nie ma wystarczającej liczby prawdziwych powtórzeń, aby uzasadnić abstrakcję.

Niewystarczająca dbałość o design jest pułapką, ale strzeż się też nadmiernego projektowania.


źródło
Twój komentarz na temat „niefortunnych tendencji” i ślepego przestrzegania wytycznych jest chyba na miejscu.
Mael
1
@Mael Czy mówisz, że jeśli nie utrzymasz tego kodu w przyszłości, nie masz dobrych powodów, aby uzyskać odpowiedni projekt? (bez obrazy, po prostu chcę wiedzieć, co o tym sądzisz)
Zauważyłem
2
@Mael Oczywiście możemy uznać to za niefortunny zwrot frazy! : D Uważam jednak, że powinniśmy być wobec siebie tak samo surowi, jak inni, pisząc kod (uważam się za kogoś innego, kiedy czytam własny kod 2 tygodnie po jego napisaniu).
Zauważył
2
@ user61852 to bardzo nie lubisz The Codeless Code .
RubberDuck,
1
@ user61852, haha - ale co jeśli to nie wszystko zależy (nie na podstawie informacji podanych w pytaniu)? Niewiele rzeczy jest mniej pomocnych niż nadmierna pewność.
43

Pamiętaj, że DRY to wiedza . Nie ma znaczenia, czy dwa fragmenty kodu wyglądają podobnie, identycznie czy zupełnie inaczej, ważne jest, czy w obu z nich można znaleźć tę samą wiedzę na temat systemu.

Wiedza może być faktem („maksymalne dozwolone odchylenie od zamierzonej wartości wynosi 0,1%”) lub może być pewnym aspektem Twojego procesu („kolejka nigdy nie zawiera więcej niż trzech elementów”). Zasadniczo jest to każda pojedyncza informacja zakodowana w kodzie źródłowym.

Więc kiedy decydujesz, czy coś jest duplikacją, którą należy usunąć, zapytaj, czy to duplikacja wiedzy. Jeśli nie, prawdopodobnie jest to przypadkowe powielenie, a wyodrębnienie go do jakiegoś wspólnego miejsca spowoduje problemy, gdy później będziesz chciał utworzyć podobny komponent, w którym ta pozornie powielona część jest inna.

Ben Aaronson
źródło
12
To! DRY koncentruje się na unikaniu powielania zmian .
Matthieu M.,
To jest bardzo pomocne.
1
Myślałem, że celem DRY jest upewnienie się, że nie ma dwóch bitów kodu, które powinny zachowywać się identycznie, ale nie. Problemem nie jest podwójna praca, ponieważ zmiany kodu muszą być zastosowane dwukrotnie, prawdziwy problem polega na tym, że zmiana kodu musi zostać zastosowana dwukrotnie, ale nie jest.
gnasher729,
3
@ gnasher729 Tak, o to chodzi. Jeśli dwa fragmenty kodu zawierają powielanie wiedzy , można się spodziewać, że gdy jeden z nich będzie wymagał zmiany, wówczas drugi również będzie musiał się zmienić, co prowadzi do opisanego problemu. Jeśli mają one przypadkowe powielanie , wtedy gdy jedna musi się zmienić, druga może równie dobrze pozostać taka sama. W takim przypadku, jeśli wyodrębniłeś wspólną metodę (lub inną metodę), masz teraz inny problem do rozwiązania
Ben Aaronson
1
Również niezbędne powielanie i przypadkowe powielanie , patrz Przypadkowy doppelgänger w Ruby i I DRY-ed Up My Code i teraz ciężko jest z tym pracować. Co się stało? . Przypadkowe duplikaty występują również po obu stronach granicy kontekstu . Podsumowanie: scalaj duplikaty tylko wtedy, gdy ich klienci mają sens jednoczesnej modyfikacji tych zależności .
Eric
27

Czy rozważałeś zastosowanie wzorca strategii ? Miałbyś jedną klasę View, która zawiera wspólny kod i procedury wywoływane przez kilka widoków. Klasa Children of View zawierałaby kod specyficzny dla tych instancji. Wszystkie wykorzystałyby wspólny interfejs utworzony dla Widoku, a zatem różnice byłyby enkapsulowane i spójne.

LindaJeanne
źródło
5
Nie, nie rozważałem tego. Dziękuję za sugestię. Z krótkiej lektury na temat wzorca strategii wydaje się, że czegoś szukam. Zdecydowanie zbadam dalej.
Mael,
3
istnieje wzorzec metody szablonu . Możesz także wziąć to pod uwagę
Shakil
5

Jaki jest potencjał zmian? Na przykład nasza aplikacja ma 8 różnych obszarów biznesowych z potencjałem 4 lub więcej typów użytkowników dla każdego obszaru. Widoki są dostosowywane na podstawie typu użytkownika i obszaru.

Początkowo dokonano tego przy użyciu tego samego widoku z kilkoma sprawdzeniami tu i tam, aby ustalić, czy różne rzeczy powinny się pokazać. Z czasem niektóre obszary działalności postanowiły zrobić drastycznie różne rzeczy. Ostatecznie przeprowadziliśmy migrację do jednego widoku (widoki częściowe, w przypadku ASP.NET MVC) na część funkcjonalności na obszar biznesowy. Nie wszystkie obszary biznesowe mają tę samą funkcjonalność, ale jeśli ktoś chce mieć funkcjonalność inną, ten obszar ma swój własny widok. Jest to znacznie mniej kłopotliwe dla zrozumienia kodu, a także dla testowalności. Na przykład dokonanie zmiany dla jednego obszaru nie spowoduje niepożądanej zmiany dla innego obszaru.

Jak wspomniano @ dan1111, może sprowadzać się do wezwania sądu. Z czasem możesz sprawdzić, czy to działa, czy nie.

ps2goat
źródło
2

Jednym z problemów może być to, że udostępniasz interfejs (interfejs teoretyczny, a nie funkcję językową) tylko do jednego poziomu funkcjonalności:

A(a,b,c) //a,b,c are your callbacks or other dependencies

Zamiast wielu poziomów w zależności od wymaganej kontroli:

//high level
A(a,b,c)
//lower
A myA(a,b)
B(myA,c)
//even lower
A myA(a)
B myB(myA,b)
C myC(myB,c)
//all the way down to you just having to write the code yourself

O ile rozumiem, udostępniasz tylko interfejs wysokiego poziomu (A), ukrywając szczegóły implementacji (inne rzeczy tam).

Ukrywanie szczegółów implementacji ma zalety, a właśnie znalazłeś wadę - kontrola jest ograniczona, chyba że wyraźnie dodasz funkcje dla każdej pojedynczej rzeczy, które byłyby możliwe przy bezpośrednim użyciu interfejsów niskiego poziomu.

Masz dwie opcje. Albo używasz tylko interfejsu niskiego poziomu, użyj interfejsu niskiego poziomu, ponieważ interfejs wysokiego poziomu był zbyt pracochłonny do utrzymania, lub ujawniaj interfejsy wysokiego i niskiego poziomu. Jedyną sensowną opcją jest oferowanie interfejsów wysokiego i niskiego poziomu (i wszystkiego pomiędzy nimi), zakładając, że chcesz uniknąć zbędnego kodu.

Następnie, pisząc coś innego, patrzysz na wszystkie dostępne dotąd funkcje (niezliczone możliwości, od ciebie zależy, które z nich mogą zostać ponownie wykorzystane) i poskładasz je razem.

Użyj jednego obiektu, w którym potrzebujesz niewielkiej kontroli.

Użyj funkcji najniższego poziomu, gdy musi wydarzyć się dziwność.

Jest również niezbyt czarno-biały. Może twoja duża klasa wysokiego poziomu MOŻE rozsądnie obejmować wszystkie możliwe przypadki użycia. Być może przypadki użycia są tak różne, że nie wystarczy nic oprócz prymitywnej funkcjonalności najniższego poziomu. Do ciebie, aby znaleźć równowagę.

Waterlimon
źródło
1

Istnieją już inne przydatne odpowiedzi. Dodam moje.

Powielanie jest złe, ponieważ

  1. zaśmieca kod
  2. zaśmieca to naszą znajomość kodu, ale co najważniejsze
  3. ponieważ jeśli coś tu zmienisz , a także musisz coś tam zmienić , możesz zapomnieć / wprowadzić błędy / .... i trudno nigdy nie zapomnieć.

Chodzi o to, że nie eliminujesz powielania ze względu na to lub dlatego, że ktoś powiedział, że to ważne. Robisz to, ponieważ chcesz zmniejszyć liczbę błędów / problemów. W twoim przypadku wydaje się, że jeśli zmienisz coś w widoku, prawdopodobnie nie będziesz musiał zmieniać dokładnie tej samej linii we wszystkich innych widokach. Masz więc oczywiste powielanie , a nie faktyczne powielanie.

Inną ważną kwestią jest, aby nigdy nie przepisywać od nowa czegoś, co działa teraz tylko na zasadzie zasady, jak powiedział Joel (mogłeś już o nim słyszeć ...). Tak więc, jeśli twoje poglądy działają, postępuj krok po kroku i nie padaj ofiarą „pojedynczego najgorszego błędu strategicznego, jaki może popełnić każda firma produkująca oprogramowanie”.

Francesco
źródło