Kontekst
Ostatnio zainteresowałem się tworzeniem lepiej sformatowanego kodu. Mówiąc lepiej, mam na myśli „przestrzeganie zasad zatwierdzonych przez wystarczającą liczbę osób, aby uznać to za dobrą praktykę” (ponieważ oczywiście nigdy nie będzie jednego unikalnego „najlepszego” sposobu kodowania).
Obecnie głównie piszę w Ruby, więc zacząłem używać linijki (Rubocop), aby dostarczyć mi informacji o „jakości” mojego kodu (ta „jakość” jest zdefiniowana w przewodniku projektu w stylu ruby prowadzonym przez społeczność ).
Zauważ, że użyję „jakości” jak w „jakości formatowania”, nie tyle o wydajności kodu, nawet jeśli w niektórych przypadkach na efektywność kodu ma wpływ sposób jego pisania.
W każdym razie, robiąc to wszystko, uświadomiłem sobie (a przynajmniej pamiętałem) kilka rzeczy:
- Niektóre języki (w szczególności Python, Ruby i tym podobne) pozwalają tworzyć świetne jednowierszowe kody
- Przestrzeganie kilku wskazówek dotyczących kodu może znacznie go skrócić, a jednocześnie nadal bardzo jasne
- Jednak zbyt ścisłe przestrzeganie tych wskazówek może sprawić, że kod będzie mniej przejrzysty / czytelny
- Kod może prawie całkowicie przestrzegać niektórych wytycznych i nadal być niskiej jakości
- Czytelność kodu jest w większości subiektywna (jak w „to, co według mnie może być całkowicie niejasne dla innych programistów”)
To tylko obserwacje, a nie bezwzględne reguły. Zauważysz również, że czytelność kodu i przestrzeganie wytycznych może wydawać się w tym momencie niezwiązane, ale tutaj wytyczne są sposobem na zawężenie liczby sposobów przepisania jednego fragmentu kodu.
Teraz kilka przykładów, aby wszystko to wyjaśnić.
Przykłady
Weźmy prosty przypadek użycia: mamy aplikację z User
modelem „ ”. Użytkownik ma opcjonalny firstname
i surname
obowiązkowy email
adres.
Chcę napisać metodę „ name
”, która zwróci następnie nazwę ( firstname + surname
) użytkownika, jeśli przynajmniej jego firstname
lub surname
jest obecny, lub jego email
wartość zastępczą, jeśli nie.
Chcę również, aby ta metoda przyjmowała use_email
parametr „ ” (boolean), pozwalając na użycie adresu e-mail użytkownika jako wartości zastępczej. Ten use_email
parametr „ ” powinien być domyślnie (jeśli nie przekazany) jako „ true
”.
Najprostszym sposobem na napisanie tego w języku Ruby byłoby:
def name(use_email = true)
# If firstname and surname are both blank (empty string or undefined)
# and we can use the email...
if (firstname.blank? && surname.blank?) && use_email
# ... then, return the email
return email
else
# ... else, concatenate the firstname and surname...
name = "#{firstname} #{surname}"
# ... and return the result striped from leading and trailing spaces
return name.strip
end
end
Ten kod jest najprostszym i najłatwiejszym do zrozumienia sposobem na zrobienie tego. Nawet dla kogoś, kto nie mówi „Ruby”.
Teraz spróbujmy to skrócić:
def name(use_email = true)
# 'if' condition is used as a guard clause instead of a conditional block
return email if (firstname.blank? && surname.blank?) && use_email
# Use of 'return' makes 'else' useless anyway
name = "#{firstname} #{surname}"
return name.strip
end
Jest to krótszy, wciąż łatwy do zrozumienia, a nawet łatwiejszy (klauzula ochronna jest bardziej naturalna do odczytania niż blok warunkowy). Klauzula ochronna sprawia, że jest ona bardziej zgodna z wytycznymi, których używam, więc korzystajcie z tego wszyscy. Zmniejszamy również poziom wcięcia.
Teraz użyjmy trochę magii Ruby, aby była jeszcze krótsza:
def name(use_email = true)
return email if (firstname.blank? && surname.blank?) && use_email
# Ruby can return the last called value, making 'return' useless
# and we can apply strip directly to our string, no need to store it
"#{firstname} #{surname}".strip
end
Jeszcze krótszy i idealnie zgodny z wytycznymi ... ale o wiele mniej wyraźny, ponieważ brak deklaracji zwrotnej sprawia, że jest to nieco mylące dla tych, którzy nie znają tej praktyki.
To tutaj możemy zacząć zadawać pytanie: czy naprawdę jest tego warte? Jeśli powiemy „nie, uczyń to czytelnym i dodaj return
” ”(wiedząc, że nie będzie to zgodne z wytycznymi). A może powinniśmy powiedzieć: „W porządku, to po rubinsku, naucz się tego cholernego języka!”?
Jeśli weźmiemy opcję B, to dlaczego nie uczynić jej jeszcze krótszą:
def name(use_email = true)
(email if (firstname.blank? && surname.blank?) && use_email) || "#{firstname} #{surname}".strip
end
Oto on-lineer! Oczywiście jest krótszy ... tutaj wykorzystujemy fakt, że Ruby zwróci wartość lub inną w zależności od tego, która z nich jest zdefiniowana (ponieważ e-mail zostanie zdefiniowany pod tymi samymi warunkami, co wcześniej).
Możemy również napisać:
def name(use_email = true)
(email if [firstname, surname].all?(&:blank?) && use_email) || "#{firstname} #{surname}".strip
end
Jest krótki, niezbyt trudny do odczytania (to znaczy wszyscy widzieliśmy, jak może wyglądać brzydki jednowarstwowy), dobry Ruby, jest zgodny z wytycznymi, których używam ... Ale nadal, w porównaniu z pierwszym sposobem pisania to jest o wiele mniej łatwe do odczytania i zrozumienia. Możemy również argumentować, że ta linia jest za długa (ponad 80 znaków).
Pytanie
Niektóre przykłady kodu mogą pokazać, że wybór między kodem „pełnowymiarowym” a wieloma jego zredukowanymi wersjami (aż do słynnego jednowierszowego) może być trudny, ponieważ, jak widzimy, jednowierszowy może nie być aż tak przerażający, ale jednak nic nie przebije kodu „pełnowymiarowego” pod względem czytelności ...
Oto prawdziwe pytanie: gdzie się zatrzymać? Kiedy jest krótki, wystarczająco krótki? Jak się dowiedzieć, kiedy kod staje się „zbyt krótki” i mniej czytelny (pamiętając, że jest dość subiektywny)? I jeszcze więcej: jak zawsze odpowiednio kodować i unikać mieszania jednowarstwowych z „pełnowymiarowymi” fragmentami kodu, kiedy tylko mam na to ochotę?
TL; DR
Najważniejsze pytanie brzmi: jeśli chodzi o wybór między „długim, ale jasnym, czytelnym i zrozumiałym fragmentem kodu” a „mocnym, krótszym, ale trudniejszym do odczytania / zrozumienia linkiem”, wiedząc, że te dwa elementy są najważniejsze dno skali, a nie dwie jedyne opcje: jak określić, gdzie jest granica między „wystarczająco wyraźnym” a „nie tak wyraźnym, jak powinna być”?
Głównym pytaniem nie jest klasyczne „One-linery vs. czytelność: który z nich jest lepszy?” ale „Jak znaleźć równowagę między tymi dwoma?”
Edytuj 1
Komentarze w przykładach kodu mają być „ignorowane”, mają na celu wyjaśnienie, co się dzieje, ale nie należy ich brać pod uwagę przy ocenie czytelności kodu.
źródło
return
dodanym słowem kluczowym . Te siedem postaci dodaje mi trochę jasności w oczach.[firstname,surname,!use_email].all?(&:blank?) ? email : "#{firstname} #{surname}".strip
... ponieważfalse.blank?
zwraca true, a operator potrójny oszczędza ci kilka znaków ... ¯ \ _ (ツ) _ / ¯return
ma dodać słowo kluczowe ?! Nie zawiera żadnych informacji . To czysty bałagan.Odpowiedzi:
Bez względu na kod, który piszesz, najlepiej jest go odczytać. Krótki jest drugim najlepszym. A czytelność oznacza zwykle wystarczająco krótko, aby można było zrozumieć kod, dobrze nazwane identyfikatory i przestrzegać wspólnych idiomów języka, w którym kod jest napisany.
Gdyby był to język niezależny od języka, myślę, że zdecydowanie byłby oparty na opiniach, ale w granicach języka Ruby myślę, że możemy na to odpowiedzieć.
Po pierwsze, cechą i idiomatycznym sposobem pisania Rubiego jest pominięcie
return
słowa kluczowego podczas zwracania wartości, chyba że wraca wcześniej z metody.Kolejną połączoną cechą i idiomem jest używanie końcowych
if
instrukcji w celu zwiększenia czytelności kodu. Jednym z głównych pomysłów w Rubim jest pisanie kodu, który brzmi jak język naturalny. W tym celu przechodzimy do _why's Poignant Guide to Ruby, rozdział 3 .Biorąc to pod uwagę, przykład kodu nr 3 jest najbardziej idiomatyczny dla Ruby:
Teraz, gdy czytamy kod, mówi:
Co jest dość cholernie zbliżone do faktycznego kodu Ruby.
To tylko 2 wiersze rzeczywistego kodu, więc jest dość zwięzły i przestrzega idiomów języka.
źródło
use_email
przed innymi warunkami, ponieważ jest to zmienna, a nie wywołanie funkcji. Ale i tak interpolacja łańcuchów i tak zamienia różnicę.do send an email if A, B, C but no D
, przestrzeganie twojego założenia byłoby naturalne, aby wpisać 2 bloki if / else , kiedy prawdopodobnie łatwiej byłoby napisać kodif not D, send an email
. Zachowaj ostrożność w trakcie czytania języka naturalnego i przekształć go w kod, ponieważ może on skłonić Cię do napisania nowej wersji „Niekończącej się historii” . Z klasami, metodami i zmiennymi. W końcu nie jest to wielka sprawa.if !D
jest lepsze, to w porządku, tak długo, jakD
ma sensowną nazwę. A jeśli!
operator zgubi się wśród innych kodów,NotD
odpowiednie byłoby posiadanie identyfikatora o nazwie .Nie sądzę, że uzyskasz lepszą odpowiedź niż „użyj najlepszej oceny”. Krótko mówiąc, powinieneś dążyć raczej do jasności niż do krótkości . Często najkrótszy kod jest również najczystszy, ale jeśli skupisz się tylko na osiągnięciu skrótu, może to ucierpieć. Widać to wyraźnie w dwóch ostatnich przykładach, które wymagają więcej wysiłku, aby zrozumieć niż trzy poprzednie przykłady.
Ważną kwestią jest odbiorca kodu. Czytelność jest oczywiście całkowicie zależna od osoby czytającej. Czy ludzie, których spodziewasz się przeczytać kod (oprócz ciebie) faktycznie znają idiomy języka Ruby? Cóż, to pytanie nie jest czymś, na co przypadkowi ludzie w Internecie mogą odpowiedzieć, to tylko twoja decyzja.
źródło
Częściowym problemem tutaj jest „czym jest czytelność”. Dla mnie patrzę na twój pierwszy przykład kodu:
Trudno mi ją przeczytać, ponieważ jest pełna „głośnych” komentarzy, które po prostu powtarzają kod. Usuń je:
i teraz jest o wiele bardziej czytelny. Czytając to, myślę „hmm, zastanawiam się, czy Ruby obsługuje operatora trójskładnikowego? W C # mogę napisać to jako:
Czy coś takiego jest możliwe w rubinie? Po przejrzeniu twojego postu widzę, że są:
Wszystkie dobre rzeczy. Ale to nie jest dla mnie czytelne; po prostu dlatego, że muszę przewijać, aby zobaczyć całą linię. Naprawmy to:
Teraz jestem szczęśliwy. Nie jestem całkowicie pewien, jak działa składnia, ale rozumiem, co robi kod.
Ale to tylko ja. Inni ludzie mają bardzo różne pomysły na to, co sprawia, że czytanie kodu jest przyjemne. Dlatego podczas pisania kodu musisz znać swoich odbiorców. Jeśli jesteś absolutnym początkującym nauczycielem, zechcesz to uprościć i być może napisać to tak, jak na pierwszym przykładzie. Jeśli pracujesz w grupie profesjonalnych programistów z wieloletnim doświadczeniem w języku Ruby, napisz kod, który korzysta z języka i krótko go używaj. Jeśli jest gdzieś pomiędzy, to celuj gdzieś pomiędzy.
Powiem jednak jedno: strzeż się „sprytnego kodu”, takiego jak w twoim ostatnim przykładzie. Zadaj sobie pytanie, czy
[firstname, surname].all?(&:blank?)
dodaje coś innego niż sprawia, że czujesz się sprytny, ponieważ popisujesz się swoimi umiejętnościami, nawet jeśli teraz jest trochę trudniej czytać? Powiedziałbym, że ten przykład prawdopodobnie pasuje do tej kategorii. Jeśli porównasz pięć wartości, uważam to za dobry kod. Więc znowu, nie ma tutaj absolutnej linii, pamiętaj tylko, że jesteś zbyt sprytny.Podsumowując: czytelność wymaga znajomości odbiorców i odpowiedniego ukierunkowania kodu oraz pisania zwięzłego, ale przejrzystego kodu; nigdy nie pisz „sprytnego” kodu. Mów krótko, ale nie za krótko.
źródło
return "#{firstname} #{surname}".strip
Jest to prawdopodobnie pytanie, w którym trudno nie udzielić odpowiedzi opartej na opiniach, ale oto moje dwa centy.
Jeśli okaże się, że skrócenie kodu nie wpływa na czytelność, a nawet poprawia go, idź do niego. Jeśli kod staje się mniej czytelny, musisz rozważyć, czy istnieje dość dobry powód, aby go tak zostawić. Robienie tego tylko dlatego, że jest krótsze lub fajne, albo tylko dlatego, że możesz, to przykłady złych powodów. Musisz także rozważyć, czy skrócenie kodu może uczynić go mniej zrozumiałym dla innych osób, z którymi współpracujesz.
Jaki byłby dobry powód? To naprawdę wezwanie do osądu, ale przykładem może być optymalizacja wydajności (po testach wydajności oczywiście nie wcześniej). Coś, co daje ci jakąś korzyść, za którą jesteś gotów zapłacić przy zmniejszonej czytelności. W takim przypadku możesz złagodzić tę wadę, dostarczając pomocnego komentarza (który wyjaśnia, co robi kod i dlaczego musiał zostać nieco zaszyfrowany). Co więcej, możesz wyodrębnić ten kod do oddzielnej funkcji o sensownej nazwie, aby była to tylko jedna linia w witrynie wywoływania, która wyjaśnia, co się dzieje (poprzez nazwę funkcji) bez wchodzenia w szczegóły (jednak ludzie różnią się opinie na ten temat, więc jest to kolejne wezwanie do osądu).
źródło
Odpowiedź jest nieco subiektywna, ale musisz zadać sobie całą uczciwość, jaką możesz zdobyć, czy będziesz w stanie zrozumieć ten kod, kiedy wrócisz do niego za miesiąc lub dwa.
Każda zmiana powinna poprawić zdolność przeciętnego użytkownika do zrozumienia kodu. Aby kod był zrozumiały, pomocne są następujące wytyczne:
To powiedziawszy, są chwile, w których rozszerzony kod pomaga zrozumieć, co się dzieje lepiej. Jeden przykład z tego pochodzi z C # i LINQ. LINQ jest doskonałym narzędziem i może poprawić czytelność w niektórych sytuacjach, ale natknąłem się również na wiele sytuacji, w których było to znacznie bardziej mylące. Miałem pewne opinie w recenzjach, które sugerowały przekształcenie wyrażenia w pętlę z odpowiednimi instrukcjami if, aby inni mogli go lepiej utrzymywać. Kiedy się zastosowałem, mieli rację. Technicznie, LINQ jest bardziej idiomatyczny dla C #, ale są przypadki, w których obniża to zrozumiałość, a bardziej szczegółowe rozwiązanie go poprawia.
Mówię to wszystko, aby to powiedzieć:
Pamiętaj, że ty lub ktoś taki jak ty będziesz musiał zachować ten kod później. Następnym razem, gdy natkniesz się na to, może upłynąć kilka miesięcy. Zrób sobie przysługę i nie gonić za zmniejszaniem liczby wierszy kosztem zrozumienia kodu.
źródło
Czytelność to właściwość, którą chcesz mieć, posiadanie wielu linijek nie jest. Zatem zamiast „jednokreskowych kontra czytelność” pytanie powinno brzmieć:
Kiedy jednolinijki zwiększają czytelność i kiedy to szkodzą?
Uważam, że wkładki jednowarstwowe poprawiają czytelność, gdy spełniają te dwa warunki:
Załóżmy na przykład, że
name
nie była to dobra ... nazwa dla twojej metody. To połączenie imienia i nazwiska lub użycie adresu e-mail zamiast imienia nie było czymś naturalnym. Zamiastname
tego najlepsza rzecz, jaką można wymyślić, okazała się długa i kłopotliwa:Tak długa nazwa wskazuje, że jest to bardzo specyficzne - gdyby było bardziej ogólne, można by znaleźć bardziej ogólną nazwę. Tak więc zawinięcie go w metodę nie pomaga ani w czytelności (jest za długa), ani w OSUSZANIU (zbyt specyficznym, aby można go było użyć gdzie indziej), więc lepiej po prostu zostawić tam kod.
Nadal - dlaczego warto uczynić z niego jedną wkładkę? Są zwykle mniej czytelne niż kod wielowierszowy. Tutaj powinniśmy sprawdzić mój drugi warunek - przepływ otaczającego kodu. Co jeśli masz coś takiego:
Sam kod wielowierszowy jest czytelny - ale gdy próbujesz odczytać otaczający kod (drukując różne pola), konstrukcja wielowierszowa zakłóca przepływ. Łatwiej to odczytać:
Twój przepływ nie jest przerywany i możesz skupić się na konkretnym wyrażeniu, jeśli potrzebujesz.
Czy to twoja sprawa? Absolutnie nie!
Pierwszy warunek jest mniej istotny - już uznałeś, że jest wystarczająco ogólny, aby zasłużyć na metodę, i wymyśliłeś nazwę tej metody, która jest znacznie bardziej czytelna niż jej implementacja. Oczywiście nie wyodrębnisz go ponownie do funkcji.
Co do drugiego warunku - czy zakłóca przepływ otaczającego kodu? Nie! Otaczający kod jest deklaracją metody, która wybiera
name
jest jedynym celem. Logika wybierania nazwy nie zakłóca przepływu otaczającego kodu - to jest właśnie cel otaczającego kodu!Wniosek - nie rób całego ciała funkcji jednowierszowym
Jednowarstwowe są dobre, gdy chcesz zrobić coś trochę skomplikowanego bez zakłócania przepływu. Deklaracja funkcji już zakłóca przepływ (tak, że nie zostanie przerwana, gdy wywołasz tę funkcję), więc uczynienie całego korpusu funkcji jednowierszowym nie poprawia czytelności.
Uwaga
Mam na myśli funkcje i metody „w pełni rozwinięte” - nie funkcje wbudowane ani wyrażenia lambda, które zwykle są częścią otaczającego kodu i muszą mieścić się w jego przepływie.
źródło