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