W jakich przypadkach mniej kodu nie jest lepsze? [Zamknięte]

55

Ostatnio zmieniłem kod w pracy i pomyślałem, że wykonałem dobrą robotę. Zrzuciłem 980 linii kodu do 450 i zmniejszyłem o połowę liczbę klas.

Pokazując to moim kolegom, niektórzy nie zgodzili się, że to poprawa.

Powiedzieli - „mniej linii kodu niekoniecznie jest lepsze”

Widzę, że mogą wystąpić ekstremalne przypadki, w których ludzie piszą naprawdę długie linie i / lub umieszczają wszystko w jednej metodzie, aby zapisać kilka linii, ale nie to zrobiłem. Moim zdaniem kod jest dobrze skonstruowany i łatwiejszy do zrozumienia / utrzymania, ponieważ jest o połowę mniejszy.

Próbuję zrozumieć, dlaczego ktokolwiek chciałby pracować z podwójnym kodem wymaganym do wykonania pracy, i zastanawiam się, czy ktoś czuje się tak samo jak moi koledzy i może zrobić dobre przypadki, aby mieć więcej kodu niż mniej ?

PiersyP
źródło
145
Rozmiar kodu jest mierzony w czasie, który należy przeczytać i zrozumieć, a nie w wierszach lub liczbie znaków.
Bergi,
13
Twoje pytanie w formie pisemnej jest zdecydowanie zbyt ogólne. Zalecamy napisanie nowego o konkretnych wprowadzonych zmianach.
jpmc26,
8
Rozważ szybki algorytm odwrotnego pierwiastka kwadratowego . Wdrożenie pełnej metody Newtona z poprawnym nazewnictwem zmiennych byłoby znacznie jaśniejsze i znacznie łatwiejsze do odczytania, chociaż prawdopodobnie zawierałoby więcej wierszy kodu. (Należy pamiętać, że w tym konkretnym przypadku użycie inteligentnego kodu było uzasadnione obawami perf).
Maciej Piechotka,
65
Istnieje cała strona wymiany stosów poświęcona odpowiedziom na twoje pytanie: codegolf.stackexchange.com . :)
Federico Poloni

Odpowiedzi:

124

Chuda osoba niekoniecznie jest zdrowsza niż osoba z nadwagą.

980-liniowa historia dla dzieci jest łatwiejsza do odczytania niż 450-tezowa fizyka.

Istnieje wiele atrybutów, które określają jakość kodu. Niektóre są po prostu obliczane, na przykład złożoność cykliczna i złożoność Halsteada . Inne są bardziej luźno zdefiniowane, takie jak spójność , czytelność, zrozumiałość, rozszerzalność, niezawodność, poprawność, samok dokumentacja, czystość, testowalność i wiele innych.

Możliwe, że na przykład zmniejszyłeś całkowitą długość kodu - wprowadziłeś dodatkową nieuzasadnioną złożoność i uczyniłeś kod bardziej tajemniczym.

Podział długiego kodu na małe metody może być tak samo szkodliwy, jak i korzystny .

Poproś współpracowników o udzielenie konkretnej informacji zwrotnej na temat tego, dlaczego uważają, że Twoje działania refaktoryzacyjne przyniosły niepożądany rezultat.

MA Hanin
źródło
1
@ PiersyP to tylko informacja finansowa, jedną z wytycznych, których nauczono mnie na temat dobrego refaktoryzacji, jest to, że powinniśmy zobaczyć złożoność cyklomatyczną zredukowaną do pierwiastka kwadratowego z tego, co pierwotnie było.
MA Hanin
4
@PiersyP również, nie mówię, że twój kod jest gorszy lub lepszy niż był. Jako outsider nie mogę tak naprawdę powiedzieć. Może być również tak, że twoi koledzy są zbyt konserwatywni i boją się twoich zmian po prostu dlatego, że nie podjęli wysiłku, aby je przejrzeć i zweryfikować. Dlatego zasugerowałem, aby poprosić ich o dodatkowe informacje zwrotne.
MA Hanin
6
Dobra robota, chłopaki - ustaliliście, że gdzieś jest „odpowiednia” waga (dokładna liczba może się różnić). Nawet oryginalne posty @Neil mówią „OVERweight”, w przeciwieństwie do „cięższej osoby”, a to dlatego, że JEST słodki punkt, tak jak w przypadku programowania. Dodanie kodu poza tym „właściwym rozmiarem” jest po prostu bałaganem, a usunięcie linii poniżej tego punktu po prostu poświęca zrozumienie ze względu na zwięzłość. Wiedzieć, gdzie dokładnie jest ten punkt ... TO jest trudne.
AC
1
To, że nie jest konieczne, nie oznacza, że ​​nie ma ono wartości.
Chris Wohlert,
1
@Neil Ogólnie masz rację, ale zawsze nieuchwytna „równowaga”, do której nawiązujesz, jest mitem, obiektywnie mówiąc. Każdy ma inne pojęcie o tym, czym jest „dobra równowaga”. Najwyraźniej OP pomyślał, że zrobił coś dobrego, a jego współpracownicy tego nie zrobili, ale jestem pewien, że wszyscy pomyśleli, że mieli „właściwą równowagę”, kiedy pisali kod.
code_dredd
35

Co ciekawe, ja i kolega jesteśmy obecnie w środku refaktora, który zwiększy liczbę klas i funkcji o nieco mniej niż dwukrotnie, chociaż linie kodu pozostaną takie same. Tak się składa, że ​​mam dobry przykład.

W naszym przypadku mieliśmy jedną warstwę abstrakcji, która naprawdę powinna być dwie. Wszystko zostało wciśnięte w warstwę interfejsu użytkownika. Dzięki podzieleniu go na dwie warstwy wszystko staje się bardziej spójne, a testowanie i konserwacja poszczególnych elementów staje się znacznie prostsza.

To nie rozmiar kodu przeszkadza kolegom, to coś innego. Jeśli nie potrafią go sformułować, spróbuj spojrzeć na kod tak, jakbyś nigdy nie widział starej implementacji, i oceń go na podstawie własnych zalet, a nie tylko w porównaniu. Czasami, gdy robię długi refaktor, tracę z oczu pierwotny cel i posuwam się za daleko. Przyjrzyj się krytycznie „dużemu obrazowi” i przywróć go na właściwe tory, być może z pomocą programisty parowego, którego porady cenisz.

Karl Bielefeldt
źródło
1
Tak, zdecydowanie oddziel interfejs użytkownika od innych rzeczy, zawsze jest to warte zachodu. Jeśli chodzi o twój punkt widzenia utraty pierwotnego celu, zgadzam się nieco, ale możesz również przeprojektować coś lepszego lub w drodze do lepszego. Podobnie jak stary argument na temat ewolucji („co dobrego jest częścią skrzydła?”), Rzeczy nie poprawią się, jeśli nigdy nie poświęcisz czasu na ich ulepszenie. Nie zawsze wiesz, dokąd zmierzasz, dopóki nie będziesz w drodze. Zgadzam się z próbą ustalenia, dlaczego współpracownicy są niespokojni, ale może to naprawdę „ich problem”, a nie twój.
18

Przychodzi mi na myśl cytat, często przypisywany Albertowi Einsteinowi:

Spraw, aby wszystko było tak proste, jak to możliwe, ale nie prostsze.

Jeśli przesadzisz, przycinając rzeczy, może to utrudnić odczytanie kodu. Ponieważ „łatwy / trudny do odczytania” może być bardzo subiektywnym terminem, wyjaśnię dokładnie, co rozumiem przez to: miarę stopnia trudności, jaką będzie miał wykwalifikowany programista w określaniu „co robi ten kod?”. po prostu patrząc na źródło, bez pomocy specjalistycznych narzędzi.

Języki takie jak Java i Pascal są niesławne ze względu na swoją gadatliwość. Ludzie często wskazują na pewne elementy składniowe i szyderczo mówią, że „są po to, aby ułatwić kompilatorowi pracę”. Jest to mniej więcej prawda, z wyjątkiem części „sprawiedliwej”. Im bardziej wyraźne są informacje, tym kod jest łatwiejszy do odczytania i zrozumienia, nie tylko przez kompilator, ale także przez człowieka.

Jeśli powiem var x = 2 + 2;, to od razu oczywiste, że xma to być liczba całkowita. Ale jeśli powiem var foo = value.Response;, o wiele mniej jasne jest to, co fooreprezentuje lub jakie są jego właściwości i możliwości. Nawet jeśli kompilator może to łatwo wywnioskować, nakłada na osobę znacznie więcej wysiłku poznawczego.

Pamiętaj, że programy muszą być napisane, aby ludzie mogli je czytać, a tylko przypadkowo, aby maszyny mogły je uruchomić. (Jak na ironię, ten cytat pochodzi z podręcznika poświęconego niesławnemu językowi, ponieważ jest niezwykle trudny do odczytania!) Dobrym pomysłem jest usunięcie rzeczy, które są zbędne, ale nie zabieraj kodu, który ułatwi innym ludziom dowiedzieć się, co się dzieje, nawet jeśli nie jest to absolutnie konieczne do napisania programu.

Mason Wheeler
źródło
7
varprzykład nie jest szczególnie dobry uproszczenia, ponieważ większość czasu na czytanie i zrozumienie kodu polega na zastanawianie się, zachowania na pewnym poziomie abstrakcji, więc znając rzeczywiste typy konkretnych zmiennych zwykle niczego nie zmienia (tylko to pomaga zrozumieć niższe abstrakcje). Lepszym przykładem może być wiele wierszy prostego kodu zmiażdżonych w jedną zwiniętą instrukcję - np. if ((x = Foo()) != (y = Bar()) && CheckResult(x, y)) Potrzeba czasu na grok, a znajomość typów xlub ywcale nie pomaga.
Ben Cottrell,
15

Dłuższy kod może być łatwiejszy do odczytania. Zwykle jest odwrotnie, ale istnieje wiele wyjątków - niektóre z nich zostały przedstawione w innych odpowiedziach.

Ale spójrzmy z innej perspektywy. Zakładamy, że nowy kod będzie postrzegany jako lepszy przez większość wykwalifikowanych programistów, którzy widzą 2 fragmenty kodu bez dodatkowej wiedzy o kulturze firmy, podstawie kodu lub mapie drogowej. Nawet wtedy istnieje wiele powodów, by sprzeciwić się nowemu kodowi. Dla zwięzłości zadzwonię do „Ludzi krytykujących nowy kod” Pecritenc :

  • Stabilność. Jeśli wiadomo, że stary kod jest stabilny, stabilność nowego kodu jest nieznana. Przed użyciem nowego kodu należy go jeszcze przetestować. Jeśli z jakiegoś powodu prawidłowe testowanie nie jest dostępne, zmiana jest dość dużym problemem. Nawet jeśli testy są dostępne, Pecritenc może uznać, że wysiłek nie jest wart (niewielkiej) poprawy kodu.
  • Wydajność / skalowanie. Stary kod mógł skalować się lepiej, a Pecritenc zakłada, że ​​wydajność stanie się problemem w przyszłości, gdy klienci i funkcje wkrótce * się zwiększą.
  • Rozciągliwość. Stary kod mógł pozwolić na łatwe wprowadzenie niektórych funkcji, które Pecritenc zakłada, że ​​zostaną wkrótce dodane *.
  • Znajomość. Stary kod mógł ponownie wykorzystywać wzorce, które są używane w 5 innych miejscach bazy kodu firmy. Jednocześnie nowy kod wykorzystuje fantazyjny wzór, o którym tylko połowa firm słyszała w tym momencie.
  • Pomadka na świni. Pecritenc może uważać, że zarówno stary, jak i nowy kod są bzdurami lub są nieistotne, przez co wszelkie porównania między nimi są bezcelowe.
  • Duma. Pecritenc mógł być oryginalnym autorem kodu i nie lubi ludzi, którzy dokonują ogromnych zmian w jego kodzie. Może nawet postrzegać ulepszenia jako lekką zniewagę, ponieważ implikują, że powinien był postąpić lepiej.
Piotr
źródło
4
+1 za „Pecritenc” i bardzo miłe podsumowanie uzasadnionych zastrzeżeń, które należy rozważyć przed wstępnym przygotowaniem.
1
I +1 za „rozszerzalność” - myślałem, że oryginalny kod mógł mieć funkcje lub klasy, które były przeznaczone do użycia w przyszłym projekcie, więc abstrakcje mogły wydawać się zbędne lub niepotrzebne, ale tylko w kontekście jednego programu.
Darren Ringer
Ponadto kod, o którym mowa, może nie być kodem krytycznym, więc uznano go za marnotrawstwo zasobów inżynieryjnych, aby go wyczyścić.
Erik Eidt,
@nocomprende Jakiś powód, dla którego zastosowałeś rozsądne, wstępnie przemyślane i wstępne przetwarzanie? Może metoda podobna do Pecritenc?
Milind R
@MilindR Prawdopodobnie uprzedzenie, upodobanie, a może osobiste preferencje? A może po prostu nie ma żadnego powodu, kosmicznego zbiegu kofaktorów, mylących warunków konspiracyjnych. Naprawdę nie mam pojęcia. Jak o tobie?
1

Jaki rodzaj kodu jest lepszy, może zależeć od wiedzy programistów i narzędzi, których używają. Na przykład oto, dlaczego to, co normalnie byłoby uważane za źle napisany kod, może być bardziej skuteczne w niektórych sytuacjach niż dobrze napisany kod obiektowy, który w pełni wykorzystuje dziedziczenie:

(1) Niektórzy programiści po prostu nie mają intuicyjnej wiedzy na temat programowania obiektowego. Jeśli twoją metaforą projektu oprogramowania jest obwód elektryczny, wówczas będziesz się spodziewać dużo duplikacji kodu. Będziesz chciał zobaczyć mniej więcej te same metody w wielu klasach. Sprawią, że poczujesz się jak w domu. A projekt, w którym musisz szukać metod w klasach rodzicielskich, a nawet w klasach dziadków, aby zobaczyć, co się dzieje, może wydawać się wrogi. Nie chcesz zrozumieć, jak działa klasa nadrzędna, a następnie zrozumieć, czym różni się bieżąca klasa. Chcesz bezpośrednio zrozumieć, jak działa bieżąca klasa, a fakt, że informacje są rozłożone na kilka plików, jest mylący.

Ponadto, gdy chcesz po prostu naprawić konkretny problem w określonej klasie, możesz nie chcieć zastanawiać się, czy rozwiązać problem bezpośrednio w klasie podstawowej, czy też zastąpić metodę z bieżącej klasy zainteresowań. (Bez dziedziczenia nie musiałbyś podejmować świadomej decyzji. Domyślnie po prostu ignorujesz podobne problemy w podobnych klasach, dopóki nie zostaną zgłoszone jako błędy). Ten ostatni aspekt nie jest tak naprawdę uzasadnionym argumentem, chociaż może wyjaśniać niektóre z sprzeciw.

(2) Niektórzy programiści często używają debuggera. Mimo że ogólnie jestem zdecydowanie zwolennikiem dziedziczenia kodu i zapobiegania duplikowaniu, dzielę się frustracją opisaną w (1) podczas debugowania kodu zorientowanego obiektowo. Podczas wykonywania kodu czasami przeskakuje on między klasami (przodkami), nawet jeśli pozostaje w tym samym obiekcie. Ponadto, ustawiając punkt przerwania w dobrze napisanym kodzie, jest bardziej prawdopodobne, że zostanie on wyzwolony, gdy nie będzie to pomocne, więc może być konieczne poświęcenie wysiłku na uzależnienie go (tam, gdzie jest to praktyczne) lub nawet na ręczne kontynuowanie wiele razy przed odpowiednim wyzwalaczem.


źródło
3
„zajęcia dla dziadków”! Haw Haw! Uważaj tylko na zajęcia Adama i Ewy. (I oczywiście klasa Boga) Przedtem była bez form i pustka.
1

To całkowicie zależy. Pracowałem nad projektem, który nie dopuszcza zmiennych logicznych jako parametrów funkcji, ale zamiast tego wymaga dedykowania enumdla każdej opcji.

Więc,

enum OPTION1 { OPTION1_OFF, OPTION1_ON };
enum OPTION2 { OPTION2_OFF, OPTION2_ON };

void doSomething(OPTION1, OPTION2);

jest dużo bardziej gadatliwy niż

void doSomething(bool, bool);

Jednak,

doSomething(OPTION1_ON, OPTION2_OFF);

jest o wiele bardziej czytelny niż

doSomething(true, false);

Kompilator powinien wygenerować ten sam kod dla obu, więc nie można nic zyskać, używając krótszej formy.

Simon Richter
źródło
0

Powiedziałbym, że spójność może stanowić problem.

Na przykład w aplikacji internetowej Załóżmy, że masz stronę administratora, na której indeksujesz wszystkie produkty, czyli zasadniczo ten sam kod (indeks), którego używałbyś w sytuacji na stronie głównej, aby po prostu zindeksować produkty.

Jeśli zdecydujesz się podzielić na części wszystko, abyś mógł pozostać SUCHY i elegancki, musisz dodać wiele warunków dotyczących tego, czy użytkownik przeglądający jest administratorem, czy nie, i zaśmiecać kod niepotrzebnymi rzeczami, co spowoduje, że będzie on bardzo nieczytelny projektant!

Więc w takiej sytuacji, nawet jeśli kod jest prawie taki sam, tylko dlatego, że można go skalować do czegoś innego, a przypadki użycia mogą się nieznacznie zmienić, źle byłoby pójść za każdym z nich, dodając warunki i ifs. Tak więc dobrą strategią byłoby porzucenie koncepcji DRY i rozbicie kodu na części, które można konserwować.

ciastko
źródło
0
  • Gdy mniej kodu nie wykonuje tej samej pracy, co więcej kodu. Refaktoryzacja w celu uproszczenia jest dobra, ale należy uważać, aby nie uprościć miejsca problemów, które spełnia to rozwiązanie. 980 linii kodu może obsłużyć więcej przypadków narożnych niż 450.
  • Gdy mniej kodu nie zawodzi tak płynnie, jak więcej kodu. Widziałem kilka zadań „ref *** toring” wykonanych na kodzie, aby usunąć „niepotrzebne” try-catch i inną obsługę przypadków błędów. Nieuniknionym rezultatem było wyświetlenie okna dialogowego z ładnym komunikatem o błędzie i o tym, co użytkownik może zrobić, aplikacja uległa awarii lub YSODed.
  • Kiedy mniej kodu jest mniej konserwowalne / rozszerzalne niż więcej kodu. Refaktoryzacja pod kątem zwięzłości kodu często usuwa „niepotrzebne” konstrukcje kodu w interesie LoC. Problem w tym, że takie konstrukcje kodu, jak deklaracje interfejsu równoległego, wyodrębnione metody / podklasy itp. Są konieczne, jeśli ten kod kiedykolwiek będzie musiał zrobić więcej niż obecnie, lub zrobić to inaczej. W skrajności niektóre rozwiązania dostosowane do konkretnego problemu mogą w ogóle nie działać, jeśli definicja problemu zmieni się tylko trochę.

    Jeden przykład; masz listę liczb całkowitych. Każda z tych liczb całkowitych ma zduplikowaną wartość na liście, z wyjątkiem jednej. Twój algorytm musi znaleźć tę niesparowaną wartość. Ogólnym rozwiązaniem jest porównanie każdej liczby z każdą inną liczbą, dopóki nie znajdziesz liczby, która nie ma duplikatu na liście, co jest operacją N ^ 2-krotną. Możesz również zbudować histogram za pomocą tablicy mieszającej, ale jest to bardzo mało miejsca. Można jednak ustawić czas liniowy i stałą przestrzeń za pomocą bitowej operacji XOR; XOR każdą liczbą całkowitą względem działającej „sumy” (zaczynając od zera), a na końcu suma bieżąca będzie wartością twojej niesparowanej liczby całkowitej. Bardzo elegancko. Dopóki wymagania się nie zmienią, więcej niż jedna liczba na liście może zostać sparowana lub liczby całkowite zawierają zero. Teraz twój program zwraca śmieci lub niejednoznaczne wyniki (jeśli zwraca zero, czy to oznacza, że ​​wszystkie elementy są sparowane, czy też niesparowany element ma zero?). Taki jest problem „sprytnych” wdrożeń w programowaniu w świecie rzeczywistym.

  • Gdy mniej kodu oznacza mniej samodokumentowania niż więcej kodu. Umiejętność czytania samego kodu i określania jego działania ma kluczowe znaczenie dla rozwoju zespołu. Przekazanie napisanego przez siebie algorytmu brain-f ***, który działa pięknie, młodszemu programistowi i poproszenie go o drobną modyfikację w celu nieznacznej modyfikacji danych wyjściowych, nie zaprowadzi cię daleko. Mnóstwo starszych deweloperów również miałoby problemy z tą sytuacją. Zdolność do zrozumienia w dowolnym momencie, co robi kod i co może się z nim nie udać, jest kluczem do działającego środowiska programistycznego zespołu (a nawet solo; Gwarantuję ci błysk genialności, który miałeś, gdy napisałeś 5 -liniowa metoda leczenia raka już dawno zniknie, gdy wrócisz do tej funkcji, która ma na celu wyleczenie również choroby Parkinsona.)
KeithS
źródło
0

Kod komputerowy musi robić wiele rzeczy. „Minimalistyczny” kod, który nie robi tych rzeczy, nie jest dobrym kodem.

Na przykład program komputerowy powinien obejmować wszystkie możliwe (lub, co najmniej, wszystkie prawdopodobne przypadki). Jeśli fragment kodu obejmuje tylko jeden „przypadek podstawowy” i ignoruje inne, nie jest to dobry kod, nawet jeśli jest krótki.

Kod komputerowy powinien być „skalowalny”. Kod tajemniczy może działać tylko dla jednej specjalistycznej aplikacji, a dłuższy, ale bardziej otwarty program może ułatwić dodawanie nowych aplikacji.

Kod komputerowy powinien być wyraźny. Jak wykazał inny program odpowiadający, możliwe jest, aby koder hard-core mógł wygenerować jedno-liniową funkcję typu „algorytmicznego”, która wykonuje zadanie. Ale jednolinijka musiała zostać podzielona na pięć różnych „zdań”, zanim stanie się jasne dla przeciętnego programisty.

Tom Au
źródło
Obowiązek leży w oku patrzącego.
-2

Wydajność obliczeniowa. Podczas optymalizacji podszewki rur lub równoległych części kodu może być korzystne na przykład nie zapętlanie od 1 do 400, ale od 1 do 50 i umieszczanie 8 wystąpień podobnego kodu w każdej pętli. Nie zakładam, że tak było w twojej sytuacji, ale jest to przykład, w którym więcej linii jest lepszych (pod względem wydajności).

Hans Janssen
źródło
4
Dobry kompilator powinien wiedzieć lepiej niż przeciętny programista, jak rozwijać pętle dla określonej architektury komputera, ale ogólna uwaga jest poprawna. Kiedyś spojrzałem na kod źródłowy procedury mnożenia macierzy z wysokowydajnej biblioteki Cray. Mnożenie macierzy to trzy zagnieżdżone pętle i łącznie około 6 linii kodu, prawda? Źle - ta procedura biblioteczna zawierała około 1100 linii kodu oraz podobną liczbę linii komentarza wyjaśniających, dlaczego tak długo trwało!
alephzero
1
@alephzero wow, chciałbym zobaczyć ten kod, to musi być po prostu Cray Cray.
@alephzero, dobre kompilatory mogą wiele zrobić, ale niestety nie wszystko. Jasną stroną jest to, że są rzeczy, które sprawiają, że programowanie jest interesujące!
Hans Janssen,
2
@alephzero Rzeczywiście, dobry kod mnożenia macierzy nie tylko goli trochę czasu (tj. zmniejsza go o stały współczynnik), ale używa zupełnie innego algorytmu o różnej złożoności asymptotycznej, np . algorytm Strassena ma w przybliżeniu O (n ^ 2,8) zamiast O (n ^ 3).
Arthur Tacca,