Zidentyfikowałeś ważny kompromis
Zatem rzeczywiście istnieje tutaj kompromis i jest on nieodłączny od abstrakcji jako całości. Ilekroć ktoś próbuje wciągnąć N linii kodu do swojej funkcji w celu nazwania go i odizolowania, jednocześnie ułatwia czytanie strony wywołującej (odwołując się do nazwy, a nie do wszystkich krwawych szczegółów leżących u podstaw tej nazwy) i bardziej złożone (teraz masz znaczenie splątane w dwóch różnych częściach bazy kodu). „Łatwy” jest przeciwieństwem „twardego”, ale nie jest synonimem „prostego”, który jest przeciwieństwem „złożonego”. Obie nie są przeciwieństwami, a abstrakcja zawsze zwiększa złożoność w celu wstawienia jakiejś formy z łatwością.
Widzimy dodatkową złożoność bezpośrednio, gdy jakaś zmiana wymagań biznesowych powoduje, że abstrakcja zaczyna wyciekać. Być może jakaś nowa logika poszedłaby najbardziej naturalnie w środek wstępnie wyodrębnionego kodu, na przykład, jeśli wyodrębniony kod przechodzi przez jakieś drzewo i naprawdę chciałbyś zbierać (i być może działać) jakieś informacje, gdy jesteś przemierzając drzewo. W międzyczasie, jeśli wyodrębniłeś ten kod, mogą istnieć inne strony wywołujące, a dodanie wymaganej logiki w środku metody może uszkodzić te inne strony wywoływania. Zobacz, ilekroć zmieniamy wiersz kodu, musimy tylko spojrzeć na jego bezpośredni kontekst; kiedy zmieniamy metodę, musimy Cmd-F cały nasz kod źródłowy szukać wszystkiego, co może się zepsuć w wyniku zmiany kontraktu tej metody,
W takich przypadkach chciwy algorytm może zawieść
Złożoność sprawiła również, że kod jest w pewnym sensie mniej czytelny niż bardziej. W poprzednim zadaniu miałem do czynienia z interfejsem API HTTP, który był bardzo starannie i precyzyjnie podzielony na kilka warstw, każdy punkt końcowy jest określany przez kontroler, który sprawdza poprawność kształtu wiadomości przychodzącej, a następnie przekazuje ją menedżerowi „warstwy logiki biznesowej” , który następnie zwrócił się z prośbą o „warstwę danych”, która była odpowiedzialna za kilka zapytań do warstwy „obiektu dostępu do danych”, która była odpowiedzialna za utworzenie kilku Delegatów SQL, którzy faktycznie odpowiedzieliby na twoje pytanie. Pierwszą rzeczą, jaką mogę o tym powiedzieć, było to, że około 90% kodu to płyta kopiująca i wklejająca, innymi słowy, nie było żadnych operacji. Tak więc w wielu przypadkach odczytanie dowolnego fragmentu kodu było bardzo „łatwe”, ponieważ „och ten menedżer właśnie przekazuje żądanie do tego obiektu dostępu do danych”.dużo przełączania kontekstu i znajdowania plików oraz próby śledzenia informacji, których nigdy nie powinieneś śledzić, „na tej warstwie nazywa się X, na tej drugiej warstwie nazywa się X”, a na drugiej inna warstwa ”.
Myślę, że kiedy skończyłem, ten prosty interfejs CRUD API był na etapie, w którym wydrukowanie go przy 30 liniach na stronie zajęłoby 10-20 pięćset stron podręczników na półce: była to cała encyklopedia powtarzalnych kod. Jeśli chodzi o zasadniczą złożoność, nie jestem pewien, czy była tam nawet połowa podręcznika o zasadniczej złożoności; mieliśmy do dyspozycji tylko 5-6 diagramów baz danych. Dokonanie jakiejkolwiek drobnej zmiany było gigantycznym przedsięwzięciem, uczenie się, że to gigantyczne przedsięwzięcie, dodanie nowej funkcjonalności stało się tak bolesne, że faktycznie mieliśmy pliki szablonów, których użylibyśmy do dodania nowej funkcjonalności.
Widziałem więc z pierwszej ręki, jak uczynienie każdej części bardzo czytelną i oczywistą może sprawić, że całość będzie bardzo nieczytelna i nieoczywista. Oznacza to, że chciwy algorytm może zawieść. Znasz chciwy algorytm, tak? „Zamierzam zrobić wszystko, by lokalny krok najbardziej poprawił sytuację, a potem będę mieć pewność, że znajdę się w sytuacji globalnej poprawy”. Jest to często piękna pierwsza próba, ale można ją również pominąć w skomplikowanym kontekście. Na przykład w produkcji możesz spróbować zwiększyć wydajność każdego poszczególnego etapu złożonego procesu produkcyjnego - rób większe partie, krzycz na ludzi na podłodze, którzy wydają się nic nie robić, aby zająć się czymś innym - i może to często zniszczyć globalną wydajność systemu.
Najlepsza praktyka: nawiąż połączenie używając SUCHOŚCI i długości
(Uwaga: ten tytuł sekcji jest trochę żartem; często mówię moim znajomym, że gdy ktoś mówi „powinniśmy zrobić X, bo tak mówią najlepsze praktyki ”, to w 90% przypadków nie mówią o czymś takim jak wstrzyknięcie SQL lub mieszanie hasła czy cokolwiek innego - jednostronne najlepsze praktyki - dlatego stwierdzenie to można przełożyć w 90% przypadków na „powinniśmy zrobić X, bo tak mówię ”. Na przykład mogą mieć artykuł na blogu z jakiegoś biznesu, który spisał się lepiej z X zamiast X ', ale generalnie nie ma gwarancji, że Twoja firma jest podobna do tej firmy, i ogólnie jest jakiś inny artykuł z innej firmy, który lepiej sobie radził z X' niż X. Więc nie bierz tytułu poważnie.)
To, co poleciłbym, opiera się na wykładzie Jacka Diedericha pt. Stop Writing Classes (youtube.com) . Mówi w tym przemówieniu o kilku ważnych kwestiach: na przykład, że możesz wiedzieć, że klasa jest tak naprawdę tylko funkcją, gdy ma tylko dwie publiczne metody, a jedną z nich jest konstruktor / inicjator. Ale w jednym przypadku mówi o tym, w jaki sposób hipotetyczna biblioteka, którą zastąpił ciągiem mowy, gdy „Muffin” zadeklarowała własną klasę „MuffinHash”, która była podklasą wbudowanego dict
typu, który ma Python. Implementacja była całkowicie pusta - ktoś pomyślał: „być może będziemy musieli później dodać niestandardowe funkcje do słowników Pythona, na wszelki wypadek wprowadzimy abstrakcję”.
A jego wyzywająca odpowiedź brzmiała po prostu: „zawsze możemy to zrobić później, jeśli zajdzie taka potrzeba”.
Myślę, że czasami udajemy, że w przyszłości będziemy gorszymi programistami niż obecnie, więc możemy chcieć wstawić coś małego, co może nas uszczęśliwić w przyszłości. Przewidujemy przyszłe potrzeby. „Jeśli ruch jest 100 razy większy, niż się spodziewamy, takie podejście nie będzie się skalować, dlatego musimy włożyć w to trudniejsze podejście, które będzie skalowane”. Bardzo podejrzane.
Jeśli poważnie podchodzimy do tych rad, musimy ustalić, kiedy nadejdzie „później”. Prawdopodobnie najbardziej oczywistą rzeczą byłoby ustalenie górnej granicy długości rzeczy ze względów stylowych. I myślę, że najlepszą pozostałą radą byłoby użycie OSUSZANIA - nie powtarzaj się - z tymi heurystykami dotyczącymi długości linii, aby załatać dziurę w zasadach SOLID. Opierając się na heurystyce 30 wierszy będących „stroną” tekstu i analogią do prozy,
- Prześlij ponownie czek do funkcji / metody, jeśli chcesz go skopiować i wkleić. Niby istnieją uzasadnione powody, by kopiować i wklejać, ale zawsze powinieneś czuć się z tego powodu brudny. Prawdziwi autorzy nie zmuszają cię do ponownego przeczytania dużego długiego zdania 50 razy w całej narracji, chyba że naprawdę próbują podkreślić temat.
- Funkcja / metoda powinna być idealnie „akapitem”. Większość funkcji powinna mieć długość około połowy strony lub 1-15 linii kodu, a tylko 10% funkcji powinno mieć zakres do półtorej strony, 45 linii lub więcej. Gdy znajdziesz się w ponad 120 liniach kodu i komentarzy, rzecz musi zostać podzielona na części.
- Plik powinien być idealnie „rozdziałem”. Większość plików powinna mieć maksymalnie 12 stron, więc 360 linii kodu i komentarzy. Tylko może 10% twoich plików powinno mieć zasięg do 50 stron lub 1500 linii kodu i komentarzy.
- Idealnie, większość twojego kodu powinna być wcięta linią bazową funkcji lub głębokością jednego poziomu. Opierając się na niektórych heurystykach dotyczących drzewa źródeł Linuksa, jeśli podchodzisz do niego religijnie, tylko może 10% twojego kodu powinno mieć wcięcie 2 lub więcej poziomów w linii bazowej, mniej niż 5% wcięcia 3 poziomy lub więcej. Oznacza to w szczególności, że rzeczy, które muszą „zawinąć” jakieś inne obawy, takie jak obsługa błędów w dużych próbach / połowach, powinny zostać usunięte z rzeczywistej logiki.
Jak już wspomniałem, przetestowałem te statystyki na bieżącym drzewie źródeł Linuksa, aby znaleźć te przybliżone wartości procentowe, ale są one również w pewnym sensie uzasadnione w literackiej analogii.