Oto problem dotyczący programowania / języka, o którym chciałbym usłyszeć twoje przemyślenia.
Opracowaliśmy konwencje, których większość programistów (powinna) przestrzegać, które nie są częścią składni języków, ale służą do zwiększenia czytelności kodu. Oczywiście są one zawsze przedmiotem dyskusji, ale są przynajmniej niektóre podstawowe koncepcje, które większość programistów uważa za przyjemne. Właściwe nazywanie zmiennych, nazywanie w ogólności, sprawianie, że twoje linie nie są skandalicznie długie, unikanie długich funkcji, enkapsulacji itp.
Jest jednak problem, na który nie znalazłem nikogo, kto komentuje i może to być największa z całej gamy. Problem polega na anonimowości argumentów podczas wywoływania funkcji.
Funkcje wynikają z matematyki, w której f (x) ma jasne znaczenie, ponieważ funkcja ma znacznie bardziej rygorystyczną definicję niż zwykle w programowaniu. Czyste funkcje w matematyce mogą zrobić znacznie mniej niż w programowaniu i są znacznie bardziej eleganckim narzędziem, zwykle przyjmują tylko jeden argument (który zwykle jest liczbą) i zawsze zwracają jedną wartość (również zwykle liczbę). Jeśli funkcja przyjmuje wiele argumentów, prawie zawsze są to tylko dodatkowe wymiary domeny funkcji. Innymi słowy, jeden argument nie jest ważniejszy od innych. Oczywiście są uporządkowane wyraźnie, ale poza tym nie mają uporządkowania semantycznego.
W programowaniu mamy jednak więcej funkcji definiujących swobodę, i w tym przypadku argumentowałbym, że to nie jest dobra rzecz. Często zdarza się, że zdefiniowano taką funkcję
func DrawRectangleClipped (rectToDraw, fillColor, clippingRect) {}
Patrząc na definicję, jeśli funkcja jest napisana poprawnie, jest całkowicie jasne, co jest. Podczas wywoływania funkcji możesz mieć nawet magię inteligencji / uzupełniania kodu w twoim edytorze IDE / edytorze, która powie ci, jaki powinien być następny argument. Ale poczekaj. Jeśli potrzebuję tego, kiedy piszę rozmowę, czy nie brakuje nam czegoś? Osoba czytająca kod nie ma zalet IDE i dopóki nie przejdzie do definicji, nie ma pojęcia, który z dwóch prostokątów przekazanych jako argumenty jest używany do czego.
Problem idzie nawet dalej. Jeśli nasze argumenty pochodzą z jakiejś zmiennej lokalnej, mogą wystąpić sytuacje, w których nie wiemy nawet, jaki jest drugi argument, ponieważ widzimy tylko nazwę zmiennej. Weźmy na przykład ten wiersz kodu
DrawRectangleClipped(deserializedArray[0], deserializedArray[1], deserializedArray[2])
Jest to złagodzone w różnych zakresach w różnych językach, ale nawet w językach ściśle typowanych, a nawet jeśli rozsądnie nazywasz swoje zmienne, nawet nie wspominasz o typie zmiennej, gdy przekazujesz ją do funkcji.
Jak to zwykle bywa z programowaniem, istnieje wiele potencjalnych rozwiązań tego problemu. Wiele z nich jest już zaimplementowanych w popularnych językach. Na przykład parametry o nazwie w języku C #. Jednak wszystko, co wiem, ma znaczące wady. Nazewnictwo każdego parametru w każdym wywołaniu funkcji nie może prowadzić do czytelnego kodu. Wydaje się, że może przerastamy możliwości, jakie daje nam programowanie tekstu. Odeszliśmy od tekstu JUST w prawie każdym obszarze, ale nadal kodujemy to samo. Potrzebujesz więcej informacji do wyświetlenia w kodzie? Dodaj więcej tekstu. W każdym razie robi się to trochę stycznie, więc zatrzymam się tutaj.
Jedna odpowiedź, którą dostałem do drugiego fragmentu kodu jest taka, że prawdopodobnie najpierw rozpakujesz tablicę do niektórych nazwanych zmiennych, a następnie użyjesz tych, ale nazwa zmiennej może oznaczać wiele rzeczy, a sposób jej wywoływania niekoniecznie mówi ci, jak powinna być interpretowane w kontekście wywoływanej funkcji. W zakresie lokalnym możesz mieć dwa prostokąty o nazwach leftRectangle i rightRectangle, ponieważ to właśnie one reprezentują semantycznie, ale nie musi rozciągać się na to, co reprezentują, gdy podano je funkcji.
W rzeczywistości, jeśli twoje zmienne są nazwane w kontekście wywoływanej funkcji, to wprowadzasz mniej informacji, niż mogłoby to być potencjalnie w przypadku tego wywołania funkcji i na pewnym poziomie, jeśli prowadzi to do gorszego kodu. Jeśli masz procedurę, w wyniku której powstaje prostokąt przechowywany w rectForClipping, a następnie inną procedurę zapewniającą rectForDrawing, wówczas faktyczne wywołanie DrawRectangleClipped to tylko ceremonia. Linia, która nie znaczy nic nowego, i jest po to, aby komputer wiedział, czego dokładnie chcesz, mimo że już wyjaśniłeś to swoim nazewnictwem. To nie jest dobra rzecz.
Naprawdę chciałbym usłyszeć nowe perspektywy na ten temat. Jestem pewien, że nie jestem pierwszym, który uważa to za problem, więc jak to rozwiązać?
Odpowiedzi:
Zgadzam się, że sposób, w jaki funkcje są często używane, może być mylącą częścią pisania kodu, a zwłaszcza czytania kodu.
Odpowiedź na ten problem częściowo zależy od języka. Jak wspomniałeś, C # nazwał parametry. Rozwiązanie problemu C w rozwiązaniu C wymaga bardziej opisowych nazw metod. Na przykład
stringByReplacingOccurrencesOfString:withString:
jest metodą o wyraźnych parametrach.W Groovy niektóre funkcje pobierają mapy, pozwalając na składnię podobną do następującej:
Ogólnie można rozwiązać ten problem, ograniczając liczbę parametrów przekazywanych do funkcji. Myślę, że 2-3 to dobry limit. Jeśli wydaje się, że funkcja wymaga więcej parametrów, powoduje, że przemyślam projekt. Jednak odpowiedź na to pytanie może być trudniejsza. Czasami próbujesz zrobić za dużo w funkcji. Czasami warto rozważyć klasę do przechowywania parametrów. Ponadto w praktyce często stwierdzam, że funkcje, które przyjmują dużą liczbę parametrów, zwykle mają wiele z nich jako opcjonalnych.
Nawet w języku takim jak Objective-C sensowne jest ograniczenie liczby parametrów. Jednym z powodów jest to, że wiele parametrów jest opcjonalnych. Na przykład zobacz rangeOfString: i jego odmiany w NSString .
Wzorem, którego często używam w Javie, jest użycie klasy płynnego stylu jako parametru. Na przykład:
Wykorzystuje klasę jako parametr, a dzięki klasie płynnego stylu kod jest czytelny.
Powyższy fragment kodu Java pomaga również tam, gdzie kolejność parametrów może nie być tak oczywista. Zwykle zakładamy ze współrzędnymi, że X występuje przed Y. I zwykle postrzegam wysokość przed szerokością jako konwencję, ale wciąż nie jest to zbyt jasne (
something.draw(5, 20)
).Widziałem także niektóre funkcje,
drawWithHeightAndWidth(5, 20)
ale nawet te nie mogą przyjąć zbyt wielu parametrów, w przeciwnym razie stracisz czytelność.źródło
Dimension(int width, int height)
iGridLayout(int rows, int cols)
(liczba rzędów to wysokość, co oznaczaGridLayout
, że najpierw ma wysokość, aDimension
szerokość).array_filter($input, $callback)
kontraarray_map($callback, $input)
,strpos($haystack, $needle)
kontraarray_search($needle, $haystack)
Najczęściej rozwiązuje się to przez dobre nazywanie funkcji, parametrów i argumentów. Jednak już to zbadałeś i odkryłeś, że ma wady. Większość tych braków jest zmniejszana przez utrzymywanie funkcji na niskim poziomie, z niewielką liczbą parametrów, zarówno w kontekście wywoływanym, jak i wywoływanym. Twój konkretny przykład jest problematyczny, ponieważ funkcja, którą wywołujesz, próbuje wykonać kilka rzeczy naraz: określ prostokąt podstawowy, określ obszar obcinania, narysuj go i wypełnij określonym kolorem.
To trochę jak próba napisania zdania, używając tylko przymiotników. Umieść więcej czasowników (wywołań funkcji), stwórz temat (obiekt) dla swojego zdania, a łatwiej jest to przeczytać:
Nawet jeśli
clipRect
icolor
mieć straszliwe nazwy (i nie powinien), nadal można dostrzec swoje typy z kontekstu.Twój zdesrializowany przykład jest problematyczny, ponieważ kontekst wywołujący próbuje zrobić zbyt wiele naraz: deserializowanie i rysowanie czegoś. Musisz przypisać nazwy, które mają sens i wyraźnie rozdzielają dwa obowiązki. Co najmniej coś takiego:
Wiele problemów z czytelnością jest spowodowanych próbą zwięzłości, pomijaniem etapów pośrednich wymaganych przez ludzi do rozpoznania kontekstu i semantyki.
źródło
W praktyce rozwiązuje to lepszy projekt. Wyjątkowo rzadkie jest, aby dobrze napisane funkcje przyjmowały więcej niż 2 dane wejściowe, a kiedy to się zdarza, nie jest tak często, że wiele danych wejściowych nie może być agregowanych w jakiś spójny pakiet. Ułatwia to rozkładanie funkcji lub agregowanie parametrów, dzięki czemu funkcja nie robi zbyt wiele. Jedno ma dwa wejścia, staje się łatwe do nazwania i znacznie jaśniejsze na temat tego, które wejście jest które.
Mój język zabawek miał pojęcie wyrażeń, aby sobie z tym poradzić, a inne języki programowania bardziej naturalne skupiały się na tym, ale wszystkie mają inne wady. Co więcej, nawet frazy są niewiele więcej niż ładną składnią, dzięki czemu funkcje mają lepsze nazwy. Zawsze trudno będzie stworzyć dobrą nazwę funkcji, jeśli wymaga ona wielu danych wejściowych.
źródło
Na przykład w Javascript (lub ECMAScript ) wielu programistów przyzwyczaiło się
I jako praktyka programistyczna dostała się od programistów do ich bibliotek, a stamtąd do innych programistów, którzy polubili go, używają go i piszą kolejne biblioteki itp.
Przykład
Zamiast dzwonić
lubię to:
, który jest prawidłowym i poprawnym stylem, nazywamy go
lubię to:
, który jest ważny, poprawny i miły w odniesieniu do twojego pytania.
Oczywiście muszą istnieć odpowiednie warunki - w Javascripcie jest to znacznie bardziej opłacalne niż, powiedzmy, C. W javascript dało to nawet początek powszechnie stosowanej notacji strukturalnej, która stała się popularniejsza jako lżejszy odpowiednik XML. Nazywa się JSON (być może już o tym słyszałeś).
źródło
params
(często zawierające opcjonalne argumenty i często same opcjonalne), takie jak np. Ta funkcja . To sprawia, że funkcje z wieloma argumentami są dość łatwe do zrozumienia (w moim przykładzie są 2 argumenty obowiązkowe i 6 argumentów opcji).Powinieneś zatem użyć celu C, oto definicja funkcji:
I tutaj jest używany:
Myślę, że Ruby ma podobne konstrukcje i można symulować je w innych językach za pomocą list kluczowych wartości.
Dla złożonych funkcji w Javie lubię definiować zmienne obojętne w brzmieniu funkcji. Na przykład z lewej i prawej:
Wygląda na więcej kodowania, ale możesz na przykład użyć leftRectangle, a następnie refaktoryzować kod później za pomocą opcji „Wyodrębnij zmienną lokalną”, jeśli uważasz, że nie będzie to zrozumiałe dla przyszłego opiekuna kodu, którym może być Ty.
źródło
Moje podejście polega na tworzeniu tymczasowych zmiennych lokalnych - ale nie tylko wywoływaniu ich
LeftRectange
iRightRectangle
. Używam raczej dłuższych nazw, aby przekazać więcej znaczenia. Często staram się różnicować nazwy tak bardzo, jak to możliwe, np. Nie wzywam obusomething_rectangle
, jeśli ich rola nie jest bardzo symetryczna.Przykład (C ++):
i mógłbym nawet napisać funkcję lub szablon opakowania z jedną linią:
(zignoruj znaki ampersands, jeśli nie znasz C ++).
Jeśli funkcja robi kilka dziwnych rzeczy bez dobrego opisu - prawdopodobnie należy ją ponownie przefabrować!
źródło