W Clean Code autor podaje przykład
assertExpectedEqualsActual(expected, actual)
vs
assertEquals(expected, actual)
z tym pierwszym twierdzono, że jest bardziej przejrzysty, ponieważ eliminuje potrzebę pamiętania, gdzie idą argumenty i potencjalne niewłaściwe użycie z tego wynikające. Jednak nigdy nie widziałem przykładu poprzedniego schematu nazewnictwa w żadnym kodzie i cały czas go widzę. Dlaczego koderzy nie przyjmują tego pierwszego, jeśli jest, jak twierdzi autor, jaśniejszy od drugiego?
clean-code
EternalStudent
źródło
źródło
assertEquals()
tej metody stosuje się setki razy w bazie kodu, więc można oczekiwać, że czytelnicy raz zapoznają się z konwencją. Różne frameworki mają różne konwencje (np.(actual, expected) or an agnostic
(Lewy, prawy) `), ale z mojego doświadczenia wynika, że co najwyżej niewielkie źródło zamieszania.assert(a).toEqual(b)
(nawet jeśli IMO wciąż jest niepotrzebnie gadatliwy), gdzie możesz połączyć kilka powiązanych twierdzeń.assertExpectedValueEqualsActualValue
? Ale poczekaj, jak pamiętamy, czy używa==
lub.equals
czyObject.equals
? Powinno byćassertExpectedValueEqualsMethodReturnsTrueWithActualValueParameter
?Odpowiedzi:
Ponieważ bardziej jest pisać, a więcej czytać
Najprostszym powodem jest to, że ludzie lubią pisać mniej, a kodowanie tych informacji oznacza więcej pisania. Czytając go, za każdym razem muszę przeczytać całość, nawet jeśli wiem, jaka powinna być kolejność argumentów. Nawet jeśli nie zna kolejności argumentów ...
Wielu programistów używa IDE
IDE często zapewniają mechanizm przeglądania dokumentacji dla danej metody poprzez najechanie myszką lub za pomocą skrótu klawiaturowego. Z tego powodu nazwy parametrów są zawsze pod ręką.
Kodowanie argumentów wprowadza powielanie i łączenie
Nazwy parametrów powinny już dokumentować, jakie są. Pisząc nazwy w nazwie metody, kopiujemy również te informacje w podpisie metody. Tworzymy również sprzężenie między nazwą metody a parametrami. Powiedz
expected
iactual
są mylące dla naszych użytkowników. Przejście zassertEquals(expected, actual)
doassertEquals(planned, real)
nie wymaga zmiany kodu klienta za pomocą funkcji. Przejście odassertExpectedEqualsActual(expected, actual)
doassertPlannedEqualsReal(planned, real)
oznacza przełomową zmianę w interfejsie API. Lub nie zmieniamy nazwy metody, która szybko staje się myląca.Używaj typów zamiast dwuznacznych argumentów
Prawdziwy problem polega na tym, że mamy niejednoznaczne argumenty, które można łatwo przełączać, ponieważ są tego samego typu. Zamiast tego możemy użyć naszego systemu typów i naszego kompilatora do wymuszenia prawidłowej kolejności:
Można to następnie wymusić na poziomie kompilatora i zagwarantować, że nie będzie można uzyskać ich wstecz. Podchodząc z innej strony, to właśnie robi biblioteka Hamcrest dla testów.
źródło
assertExpectedEqualsActual
„bo to więcej do pisania, a więcej do czytania”, to jak możesz się bronićassertEquals(Expected.is(10), Actual.is(x))
?assertExpectedEqualsActual
nadal wymaga od programisty dokładnego określenia argumentów we właściwej kolejności.assertEquals(Expected<T> expected, Actual<T> actual)
Podpis wykorzystuje kompilator do egzekwowania prawidłowego użytkowania, co jest zupełnie inne podejście. Możesz zoptymalizować to podejście pod kątem zwięzłości, np.expect(10).equalsActual(x)
Ale to nie było pytanie…Pytasz o długotrwałą debatę w programowaniu. Ile gadatliwości jest dobre? Ogólnie rzecz biorąc, programiści stwierdzili, że dodatkowa gadatliwość nazywania argumentów nie jest tego warta.
Gadatliwość nie zawsze oznacza większą przejrzystość. Rozważać
copyFromSourceStreamToDestinationStreamWithoutBlocking(fileStreamFromChoosePreferredOutputDialog, heuristicallyDecidedSourceFileHandle)
przeciw
copy(output, source)
Oba zawierają ten sam błąd, ale czy rzeczywiście ułatwiliśmy jego znalezienie? Zasadniczo najłatwiej jest debugować, gdy wszystko jest maksymalnie zwięzłe, z wyjątkiem kilku rzeczy, które zawierają błąd, a te są wystarczająco szczegółowe, aby powiedzieć ci, co poszło nie tak.
Istnieje długa historia dodawania gadatliwości. Na przykład istnieje ogólnie niepopularna „ notacja węgierska ”, która dała nam wspaniałe nazwy, takie jak
lpszName
. To na ogół poszło na marne w ogólnej populacji programistów. Jednak dodawanie znaków do nazw zmiennych członków (takich jakmName
lubm_Name
lubname_
) nadal cieszy się popularnością w niektórych kręgach. Inni całkowicie to porzucili. Zdarza mi się pracować na bazie kodu symulacji fizyki, której dokumenty w stylu kodowania wymagają, aby każda funkcja zwracająca wektor musiała określać ramkę wektora w wywołaniu funkcji (getPositionECEF
).Być może zainteresują Cię niektóre języki popularne w Apple. Cel-C obejmuje nazwy argumentów jako część podpisu funkcji (funkcja
[atm withdrawFundsFrom: account usingPin: userProvidedPin]
jest zapisana w dokumentacji jakowithdrawFundsFrom:usingPin:
. To jest nazwa funkcji). Swift podjął podobny zestaw decyzji, wymagając umieszczenia nazw argumentów w wywołaniach funkcji (greet(person: "Bob", day: "Tuesday")
).źródło
copyFromSourceStreamToDestinationStreamWithoutBlocking(fileStreamFromChoosePreferredOutputDialog, heuristicallyDecidedSourceFileHandle)
zostały napisanecopy_from_source_stream_to_destination_stream_without_blocking(file_stream_from_choose_preferred_output_dialog, heuristically_decided_source_file_handle)
. Widzisz, o ile było to łatwiejsze ! Jest tak, ponieważ zbyt łatwo jest przeoczyć małe zmiany w połowie tej humungousunbrokenwordsalad i dłużej zajmuje ustalenie, gdzie są granice słów. Smashing dezorientuje.withdrawFundsFrom: account usingPin: userProvidedPin
jest faktycznie zapożyczona z SmallTalk.Addingunderscoresnakesthingseasiertoreadnotharderasyousee
manipuluje argumentem. W odpowiedzi użyto wielkich liter, które pomijasz.AddingCapitalizationMakesThingsEasyEnoughToReadAsYouCanSeeHere
. Po drugie, 9 razy na 10, nazwa nigdy nie powinna rosnąć poza[verb][adjective][noun]
(gdzie każdy blok jest opcjonalny), format, który jest dobrze czytelny przy użyciu wielkich liter:ReadSimpleName
Autor „Czystego kodu” wskazuje na uzasadniony problem, ale jego sugerowane rozwiązanie jest raczej nieeleganckie. Zwykle istnieją lepsze sposoby poprawy niejasnych nazw metod.
Ma rację, że
assertEquals
(z bibliotek testowych jednostek stylu xUnit) nie wyjaśnia, który argument jest oczekiwany, a który rzeczywisty. To też mnie ugryzło! Wiele bibliotek testów jednostkowych zauważyło ten problem i wprowadziło alternatywne składnie, takie jak:Lub podobne. Co z pewnością jest o wiele wyraźniejsze niż,
assertEquals
ale także o wiele lepsze niżassertExpectedEqualsActual
. I jest również o wiele bardziej składalny.źródło
fun(x)
5, to co może pójść nie tak, jeśli odwrócisz kolejność -assert(fun(x), 5)
? Jak cię ugryzło?expected
iactual
, więc ich odwrócenie może spowodować, że komunikat nie będzie dokładny. Ale zgadzam się, że brzmi to bardziej naturalnie :)assert(expected, observed)
lubassert(observed, expected)
. Lepszym przykładem byłoby coś takiegolocateLatitudeLongitude
- jeśli odwrócisz współrzędne, poważnie się zepsuje.Próbujesz poprowadzić swoją drogę między Scyllą a Charybdą do jasności, starając się unikać bezużytecznej gadatliwości (znanej również jako bezcelowe wędrowanie), a także nadmiernej zwięzłości (znanej również jako tajemnicza zwięzłość).
Musimy więc przyjrzeć się interfejsowi, który chcesz ocenić, sposobowi na potwierdzenie-debugowanie, że dwa obiekty są równe.
Nie, więc sama nazwa jest wystarczająco jasna.
Nie, więc zignorujmy ich. Już to zrobiłeś? Dobry.
Niemal w przypadku błędu komunikat umieszcza reprezentację każdego argumentu we własnym miejscu.
Zobaczmy więc, czy ta niewielka różnica ma jakiekolwiek znaczenie i nie jest objęta istniejącymi silnymi konwencjami.
Czy zamierzeni odbiorcy są niewygodni, jeśli argumenty zostaną przypadkowo zamienione?
Nie, programiści dostają również śledzenie stosu i muszą sprawdzić kod źródłowy, aby naprawić błąd.
Nawet bez pełnego śledzenia stosu pozycja asercji rozwiązuje to pytanie. A jeśli nawet tego brakuje i nie jest to oczywiste z przesłania, które jest, co najwyżej podwaja możliwości.
Czy kolejność argumentów jest zgodna z konwencją?
Wydaje się, że tak jest. Choć w najlepszym razie wydaje się to słabą konwencją.
Różnica wygląda więc dość nieznacznie, a porządek argumentów objęty jest wystarczająco silną konwencją, aby każda próba umieszczenia go w nazwie funkcji miała negatywną użyteczność.
źródło
expected
iactual
(przynajmniej z ciągów)assertEquals("foo", "doo")
daje komunikat o błędzie jestComparisonFailure: expected:<[f]oo> but was:<[d]oo>
... Zamiana wartości byłoby odwrócić znaczenie wiadomości, że brzmi bardziej anty symetryczny do mnie. W każdym razie, jak powiedziałeś, deweloper ma inne wskaźniki, aby rozwiązać błąd, ale może wprowadzać w błąd IMHO i zajmować trochę więcej czasu na debugowanie.Często nie dodaje żadnej logicznej przejrzystości.
Porównaj „Dodaj” do „AddFirstArgumentToSecondArgument”.
Jeśli potrzebujesz przeciążenia, które, powiedzmy, dodaje trzy wartości. Co miałoby więcej sensu?
Kolejne „Dodaj” z trzema argumentami?
lub
„AddFirstAndSecondAndThirdArgument”?
Nazwa metody powinna przekazywać jej logiczne znaczenie. Powinien powiedzieć, co robi. Mówienie na poziomie mikro, jakie kroki należy podjąć, nie ułatwia czytelnikowi. W razie potrzeby nazwy argumentów dostarczą dodatkowych szczegółów. Jeśli nadal potrzebujesz więcej szczegółów, kod będzie dla Ciebie odpowiedni.
źródło
Add
sugeruje operację przemienną. OP dotyczy sytuacji, w których zamówienie ma znaczenie.sum
jest czasownikiem doskonale buntowniczym . Jest to szczególnie powszechne w zdaniu „podsumować”.Chciałbym dodać coś, na co wskazują inne odpowiedzi, ale nie sądzę, by zostało to wyraźnie wymienione:
@puck mówi: „Nadal nie ma gwarancji, że pierwszy wymieniony argument w nazwie funkcji jest naprawdę pierwszym parametrem”.
@cbojar mówi „Używaj typów zamiast niejednoznacznych argumentów”
Problem polega na tym, że języki programowania nie rozumieją nazw: są one po prostu traktowane jako nieprzezroczyste, atomowe symbole. Dlatego, podobnie jak w przypadku komentarzy do kodu, niekoniecznie istnieje korelacja między nazwą funkcji a jej faktycznym działaniem.
Porównaj
assertExpectedEqualsActual(foo, bar)
z niektórymi alternatywami (z tej strony i gdzie indziej), takimi jak:Wszystkie mają większą strukturę niż pełna nazwa, co nadaje temu językowi coś nieprzejrzystego. Definicja i użycie funkcji zależy również od tej struktury, więc nie może ona zsynchronizować się z tym, co robi implementacja (jak nazwa lub komentarz może).
Kiedy napotykam lub widzę taki problem, zanim sfrustruję swój komputer, najpierw zastanawiam się, czy to „uczciwe” obwinianie maszyny. Innymi słowy, czy maszyna otrzymała wystarczającą ilość informacji, aby odróżnić to, czego chciałam od tego, o co prosiłam?
Takie wezwanie
assertEqual(expected, actual)
ma tak samo sens, jakassertEqual(actual, expected)
, więc łatwo jest je pomieszać, a maszyna pługa i zrobić coś złego. Jeśli użyliśmyassertExpectedEqualsActual
zamiast, może to uczynić nas mniej prawdopodobne, aby popełnić błąd, ale to nie daje więcej informacji do urządzenia (nie można zrozumieć po angielsku, a wybór nazwy nie powinna mieć wpływu na semantykę).To, co sprawia, że podejście „ustrukturyzowane” jest bardziej preferowane, takie jak argumenty słów kluczowych, pola oznaczone etykietami, różne typy itp., Polega na tym, że dodatkowe informacje są również do odczytu maszynowego , dzięki czemu możemy wykryć nieprawidłowe użycie maszyny i pomóc nam zrobić wszystko dobrze.
assertEqual
Sprawa nie jest tak źle, skoro jedynym problemem byłoby niedokładne wiadomości. Bardziej złowrogi może być przykładString replace(String old, String new, String content)
, który łatwo pomylić zString replace(String content, String old, String new)
którym ma zupełnie inne znaczenie. Prostym rozwiązaniem byłoby zabranie pary[old, new]
, która sprawiłaby, że błędy natychmiast spowodowałyby błąd (nawet bez typów).Zauważ, że nawet w przypadku typów możemy nie powiedzieć „maszynie, co chcemy”. Na przykład anty-wzorzec zwany „programowaniem ciągłym” traktuje wszystkie dane jako ciągi znaków, co ułatwia pomieszanie argumentów (jak w tym przypadku), zapomnienie wykonania jakiegoś kroku (np. Ucieczkę), przypadkowe złamanie niezmienników (np. co nie do rozdzielenia JSON) itp.
Jest to również związane z „ślepotą logiczną”, w której obliczamy kilka wartości logicznych (lub liczb itp.) W jednej części kodu, ale przy próbie użycia ich w innej nie jest jasne, co tak naprawdę reprezentują, czy mamy pomieszane itp. Porównaj to np. z różnymi wyliczeniami, które mają opisowe nazwy (np.
LOGGING_DISABLED
zamiastfalse
) i które powodują komunikat o błędzie, jeśli się pomieszamy.źródło
Czy to naprawdę Nadal nie ma gwarancji, że pierwszy wymieniony argument w nazwie funkcji jest naprawdę pierwszym parametrem. Lepiej to poszukaj (lub pozwól, aby IDE to zrobiło) i pozostań przy rozsądnych nazwach, niż ślepo polegaj na dość niemądrych nazwach.
Jeśli czytasz kod, powinieneś łatwo zobaczyć, co się stanie, gdy parametry zostaną nazwane tak, jak powinny.
copy(source, destination)
jest o wiele łatwiejszy do zrozumienia niż coś takiegocopyFromTheFirstLocationToTheSecondLocation(placeA, placeB)
.Ponieważ istnieją różne punkty widzenia na różne style i można znaleźć x autorów innych artykułów, które twierdzą inaczej. Zwariowałbyś, próbując śledzić wszystko, co ktoś gdzieś pisze ;-)
źródło
Zgadzam się, że kodowanie nazw parametrów w nazwach funkcji sprawia, że pisanie i używanie funkcji jest bardziej intuicyjne.
Łatwo zapomnieć o uporządkowaniu argumentów w funkcjach i poleceniach powłoki, a wielu programistów polega na funkcjach IDE lub odwołaniach do funkcji z tego powodu. Posiadanie argumentów opisanych w nazwie byłoby wymownym rozwiązaniem tej zależności.
Jednak po napisaniu opis argumentów staje się zbędny dla następnego programisty, który musi przeczytać instrukcję, ponieważ w większości przypadków zostaną użyte zmienne nazwane.
Zwięzłość tego zwycięży większość programistów, a ja osobiście łatwiej mi to czytać.
EDYCJA: Jak wskazał @Blrfl, kodowanie parametrów wcale nie jest tak „intuicyjne”, ponieważ trzeba przede wszystkim zapamiętać nazwę funkcji. Wymaga to wyszukania odniesień do funkcji lub uzyskania pomocy z IDE, które prawdopodobnie dostarczy informacji o porządkowaniu parametrów.
źródło
copyFromSourceToDestination
czycopyToDestinationFromSource
twoje wybory znajdują ją metodą prób i błędów lub czytasz materiał referencyjny. IDE, które mogą uzupełniać częściowe nazwy, to tylko automatyczna wersja tych ostatnich.copyFromSourceToDestination
że jeśli uważasz, że takcopyToDestinationFromSource
, kompilator znajdzie twój błąd, ale jeśli zostanie wywołanycopy
, nie będzie. Odczytywanie parametrów procedury kopiowania w niewłaściwy sposób jest łatwe, ponieważ strcpy, strcat itp. Stanowią precedens. Czy ten zwięzły jest łatwiejszy do odczytania? Czy mergeLists (lista A, lista B, lista C) tworzy listę A z listy B i listy C, czy czyta listę A i listę B i zapisuje listę C?dir1.copy(dir2)
działa? Brak pomysłu. Codir1.copyTo(dir2)
?