Powielanie kodu bez oczywistej abstrakcji

14

Czy kiedykolwiek spotkałeś się z przypadkiem duplikacji kodu, w którym, patrząc na linie kodu, nie mogłeś dopasować do niego abstrakcji tematycznej, która wiernie opisuje jego rolę w logice? A co zrobiłeś, aby to rozwiązać?

Jest to powielanie kodu, więc idealnie musimy zrobić refrakcję, na przykład nadać jej własną funkcję. Ale ponieważ kod nie ma dobrej abstrakcji, aby go opisać, wynikiem byłaby dziwna funkcja, dla której nie możemy nawet wymyślić dobrego imienia, i której rola w logice nie jest oczywista po prostu patrząc na nią. To, dla mnie, szkodzi przejrzystości kodu. Możemy zachować jasność i pozostawić ją taką, jaka jest, ale wtedy szkodzi utrzymywalności.

Jak myślisz, co jest najlepszym sposobem, aby rozwiązać coś takiego?

EpsilonVector
źródło

Odpowiedzi:

18

Czasami duplikacja kodu jest wynikiem „pun”: dwie rzeczy wyglądają tak samo, ale nie są.

Możliwe jest, że nadmierna abstrakcja może złamać prawdziwą modułowość twojego systemu. Zgodnie z reżimem modułowości musisz zdecydować „co może się zmienić?” i „co jest stabilne?”. Wszystko, co jest stabilne, zostaje umieszczone w interfejsie, a to, co jest niestabilne, zostaje zawarte w implementacji modułu. Następnie, gdy rzeczy się zmieniają, zmiana, którą musisz wprowadzić, jest izolowana dla tego modułu.

Refaktoryzacja jest konieczna, gdy to, co uważasz za stabilne (np. To wywołanie API zawsze przyjmuje dwa argumenty), musi się zmienić.

Tak więc w przypadku tych dwóch zduplikowanych fragmentów kodu zapytam: Czy wymagana zmiana jednego z nich oznacza koniecznie również zmianę drugiego?

To, jak odpowiesz na to pytanie, może dać ci lepszy wgląd w to, czym może być dobra abstrakcja.

Wzory projektowe są również przydatnymi narzędziami. Być może zduplikowany kod wykonuje przejście w jakiejś formie i należy zastosować wzorzec iteratora.

Jeśli zduplikowany kod ma wiele zwracanych wartości (i dlatego nie możesz wykonać prostej metody wyodrębniania), być może powinieneś utworzyć klasę, która przechowuje zwracane wartości. Klasa może wywołać metodę abstrakcyjną dla każdego punktu, który różni się między dwoma fragmentami kodu. Wykonałbyś wtedy dwie konkretne implementacje klasy: po jednej dla każdego fragmentu. [Jest to faktycznie wzorzec projektowy metody szablonów, którego nie należy mylić z koncepcją szablonów w C ++. Alternatywnie to, na co patrzysz, może być lepiej rozwiązane za pomocą wzorca strategii.]

Innym naturalnym i użytecznym sposobem myślenia o tym są funkcje wyższego rzędu. Na przykład tworzenie lambdas lub korzystanie z anonimowych klas wewnętrznych, aby kod mógł przejść do abstrakcji. Ogólnie rzecz biorąc, możesz usunąć duplikację, ale chyba że naprawdę istnieje między nimi relacja [jeśli jedna się zmieni, a także druga], możesz zranić modułowość, nie pomagając.

Macneil
źródło
4

Kiedy napotkasz taką sytuację, najlepiej pomyśleć o „nietradycyjnych” abstrakcjach. Być może masz dużo duplikacji w obrębie funkcji i faktoring zwykłej starej funkcji nie pasuje zbyt dobrze, ponieważ musisz przekazać zbyt wiele zmiennych. Tutaj zagnieżdżona funkcja w stylu D / Python (z dostępem do zakresu zewnętrznego) działałaby świetnie. (Tak, możesz stworzyć klasę, która utrzyma cały ten stan, ale jeśli używasz go tylko w dwóch funkcjach, jest to brzydkie i pełne obejście dla braku funkcji zagnieżdżonych.) Być może dziedziczenie nie do końca pasuje, ale mixin działałby dobrze. Może tak naprawdę potrzebujesz makra. Może powinieneś rozważyć metaprogramowanie szablonu, refleksję / introspekcję, a nawet programowanie generatywne.

Oczywiście, z pragmatycznego punktu widzenia, wszystko to jest trudne, jeśli nie niemożliwe, jeśli Twój język ich nie obsługuje i nie ma wystarczających możliwości metaprogramowania, aby je zaimplementować w obrębie języka. W takim przypadku nie wiem, co powiedzieć, poza „uzyskaniem lepszego języka”. Ponadto nauka języka wysokiego poziomu z wieloma funkcjami abstrakcyjnymi (takimi jak Ruby, Python, Lisp lub D) może pomóc ci lepiej programować w językach niższego poziomu, gdzie niektóre techniki mogą być nadal użyteczne, ale mniej oczywiste.

dsimcha
źródło
+1 za wiele doskonałych technik skompresowanych w ciasnej przestrzeni. (Cóż, byłoby również +1 za opisane techniki).
Macneil
3

Osobiście to ignoruję i idę dalej. Są szanse, że jeśli to dziwny przypadek, lepiej jest go zduplikować, możesz poświęcić całe wieki na refaktoryzację, a następny twórca spojrzy i cofnie twoją zmianę!

Toby
źródło
2

Bez próbki kodu trudno jest powiedzieć, dlaczego w kodzie nie ma łatwo rozpoznawalnej abstrakcji. Z tym zastrzeżeniem, oto kilka pomysłów:

  • zamiast tworzyć jedną nową funkcję do przechowywania wspólnego kodu, podziel funkcjonalność na kilka odrębnych części;
  • grupuj małe elementy razem w oparciu o typowe typy danych lub abstrakcyjne zachowanie;
  • przepisz zduplikowany kod, biorąc pod uwagę nowe elementy;
  • jeśli nowy kod nadal przeczy wyraźnej abstrakcji, podziel go również na małe i powtórz proces.

Największą trudnością w tym ćwiczeniu jest to, że twoja funkcja prawdopodobnie zawiera zbyt wiele niepowiązanych zachowań na danym poziomie abstrakcji i musisz poradzić sobie z niektórymi z nich na niższych poziomach. Prawidłowo przypuszczasz, że przejrzystość jest kluczem do utrzymania kodu, ale wyraźne zachowanie kodu (jego aktualny stan) różni się bardzo od wyjaśnienia intencji kodu.

Uczyń streszczenie mniejszych fragmentów kodu abstrakcyjnymi, dzięki ich sygnaturom funkcji identyfikującym co, a większe fragmenty powinny być łatwiejsze do sklasyfikowania.

Huperniketes
źródło