Prostota wynika z abstrakcji czy jej braku?

2

Przemyślenia mnie naszły na podstawie tego postu i komentarzy pod nim oraz na podstawie zalinkowanego tam https://wiki.c2.com/?FearOfAddingClasses .

Do jednego się muszę przyznać: w swojej karierze dotąd praktycznie nie zdarzało się, bym musiał pracować z kodem kogokolwiek innego, niż swoim własnym. W konsekwencji jestem przyzwyczajony do ogarniania w pełni całego projektu, na którym pracuję. Pewnie wpływa to na moją perspektywą.

W rzadkich wypadkach, gdy musiałem pracować z czyimś kodem, kończyło się na tym, że przepisywałem ten kod, by go ogarnąć. Albo po prostu nie tykałem się go.

Znana jest częsta preferencja licznych, ale bardzo krótkich funkcji. Uncle Bob doprowadza ją do ekstremum, promując, że jeśli tylko funkcję/metodę da się sensownie podzielić, trzeba ją podzielić. Jak się domyślam, cel jest taki, by kod był samodokumentujący się: nazwy metod robią za komentarz do tych dwóch-trzech linijek kodu, który ta metoda ma zawierać.

Ja, przyznam się szczerze, nie lubię tego podejścia. Wolę dłuższe metody. Nie ekstremalnie długie, powiedzmy pół - dwa ekrany na długość? Powód jest prosty: Jeśli już muszę czytać kod, to interesuje mnie, co ten kod robi, a niekoniecznie to, co programista (najczęściej ja) miał na myśli, że ma robić. Liczne metody sprawiają, że logika staje się rozkichana, trudno ją prześledzić. Trzeba ciągle skakać od metody do metody.

Tak się zastanawiam... Czy nie ma tutaj dwóch, zupełnie różnych, przeciwnych w zasadzie podejść do prostoty?

  • Podejście pierwsze, naiwne, charakterystyczne dla początkujących i chyba także - co trochę paradoksalne - do programistów naprawdę starej daty ("old Unix guy"): Kod musi być możliwy do ogarnięcia w całości przez jednego człowieka. Wskutek tego będziemy zmniejszać abstrakcje do niezbędnego minimum. Mało klas, o ile w ogóle będą klasy, bo z tym podejściem zazwyczaj wiąże się odrzucenie OOP, no ale w niektórych językach muszą być klasy, więc trochę ich, siłą rzeczy, będzie (chociaż z OOP dalej nie będzie miało to wiele wspólnego). Minimum zależności, najchętniej tylko te, które są naprawdę konieczne albo powszechnie znane. Naginanie wymogów biznesowych do tego, co dyktuje architektura programu, wrogość wobec mnożenia ficzerów. Także preferencja do mniejszej ilości funkcji, za to większych - nie do przesady jednak: tak, by funkcja była na tyle duża, by można było prześledzić bieg logiki bez skakania po kodzie, ale znowu na tyle mała, by ten bieg logiki nie był na tyle zamotany, by nie mieścił się w głowie.

Rzadko się obecnie spotyka ludzi promujących to podejście, ale jednak są wyjątki. W bardzo radykalny sposób promują je ludzie stojący za stroną https://cat-v.org/ . Ich poglądy są tak absurdalne jak na obecne czasy, że lektura ich strony przypomina groteskę czasem.

  • Podejście drugie, obecnie uważane za profesjonalne i jedynie słuszne: Cała aplikacja nie będzie możliwy do ogarnięcia przez jedną osobę, wobec czego musi być zaprojektowany tak, by możliwe było zajęcie się jakąś jego częścią, mając tylko bardzo ogólne i niejasne pojęcie o całości. To jest, w praktyce, podejście dokładnie przeciwne do poprzedniego. Tutaj będziemy właśnie mnożyć abstrakcje, by zapewnić tę własność. Liczne drobniutkie funkcje utrudniają dokładne prześledzenie control flow, ale właśnie nikt ma tego control flow nie śledzić dokładnie, tylko zadowolić się nazwami funkcji, pokazującymi intencję stojącą za kodem, bez konieczności odszyfrowywania tej intencji z control flow i z komentarzy. Liczne abstrakcje, klasy i wzorce powodują eksplozję LoC, ale za to maja w założeniu zagwarantować lokalność koniecznych zmian i łatwość w rozszerzaniu aplikacji. Taki był chyba zamysł OOP, chociaż teraz promuje się (z tych samych powodów zresztą) FP oraz mikroserwisy. FP narzuca przestrzeganie ograniczeń, które są dość niewygodne dla początkujących (niemutowalność bywa uciążliwa, póki sie do niej nie przyzwyczai), które jednak gwarantują - i to w sposób możliwy do wykazania matematycznie - te właśnie pożądane cechy: lokalność zmian (na działanie funkcji wpływa tylko jej własny kod i argumenty, nie dowolne inne partie programu) i kompozycyjność (można łączyć funkcje bez zbytniego przejmowania się, co mają w srodku). Wreszcie mikroserwisy doprowadzają to do ekstremum: kosztem kolejnej eksplozji w LoC gwarantują jeszcze bardziej rygorystyczny podział całości na lokalne części.

Przypuszczam, że w praktyce będzie jeszcze trzecie podejście, charakterystyczne dla nieco podszkolonych newbie (architecture astronaut): Jak już człowiek się wyleczy z tego podejścia pierwszego, ale jeszcze nie rozumie drugiego, to produkuje potworki. Wali wzorce bez ich zrozumienia. Mnoży abstrakcje, które mu wydaja się eleganckie, ale nic nie wnoszą poza komplikacją. Stosuje FP, które polega na przekazywaniu parunastu argumentów do każdej funkcji, z których jeden to rozległa struktura zawierająca cały stan programu. Dzieli program na mikroserwisy powiązane tak silnie, że i tak trzeba ogarniać całość.

Czyli, to co najgorsze z obydwu światów? Kod niegwarantujący tego, że można patrzeć tylko na jeden jego wycinek (w zasadzie gwarantujący coś dokładnie przeciwnego), a z drugiej strony tak duży (zarówno pod względem LoC, jak i ilości klas / abstrakcji / czego tam jeszcze), że nie da się ogarnąć go w całości.

Muszę uważać, bo mi to trzecie podejście realnie grozi... Jeśli już w to nie wpadałem czasem.

Z drugiej strony, do naprawdę małych / jednoosobowych projektów to pierwsze podejście wydaje się być praktyczniejsze.

Jednak realistycznie rzecz biorąc wypadałoby się wreszcie nauczyć tego drugiego...

1

Hmm ja bym wyszedł chyba od 3 fundamentów:

  1. Właścicielem kodu jest zespół, a nie jakaś pojedyncza osoba.
  2. Kod się często zmienia i nie ma się co do niego przywiązywać.
  3. Przy dostatecznie dużym codebase nie istnieje pojedyczna osoba czy nawet zespół, który ogarnia całość systemu (cognitive overload).

No i z tego mi wprost wynika, że tak długo zespół jest w stanie to ogarnąć, to jest malina ;D pewnie jakby kod został przekazany do innego teamu, zaraz zaczęły by się refaktory i „o panie, kto panu tak spier****” - normalna sprawa. Nie ma idealnego kodu + ego ciężko utrzymać na wodzy.

3 grosze odnośnie długich funkcji - im dłuższe funkcje tym trudniejsza ich modyfikacja i mniejsza reuzywalnosc pomniejszych kawałków. Zwykle lepiej to porozbijać na metody i klasy, ale tak zeby to mialo jakikolwiek sens z punktu widzenia odpowiedzialności czy domeny. Ile linijek powinna mieć zatem funkcja? Nie ma sztywnych reguł i nie lubię takiego podejścia, ze „coś musi być jakieś”.

2

Moim zdaniem jedna osoba nie musi ogarniać całego projektu, jednak dobrze byłoby, gdyby projekt miał na tyle prostą architekturę, że jeden człowiek mógłby ogarnąć całą architekturę.

Więc powinniśmy się moim zdaniem skupiać na tym, żeby architektura była prosta, a jednocześnie skalowalna (pod kątem rozmiaru projektu). Np. architektura oparta o wtyczki albo wymienialne komponenty itp. Wtedy architektura może być banalna, jednak elastyczna. I ktoś nie musi znać całego kodu, ale będzie wiedział, jak poszczególne moduły się ze sobą łączą.

No i jeśli architektura jest okej, to sam kod może być brzydki, ale jakoś się to i tak obroni i jakoś będzie to utrzymywalne.

Jeszcze też pytanie, na ile dana architektura się łatwo utrzymuje. Bo coś może być poprawnie pod kątem architektury, ale niewygodne. Np. aplikacje JSowe pisane z użyciem React, Redux, niby jest separation of concerns i są rzeczy podzielone (na komponenty, reducery, thunki itp. itd.), jednakże rodzi to pewną niewygodę dla programisty, bo żeby dodać jedną rzecz, trzeba ruszyć ileś totalnie innych plików i wszystko ze sobą związać (analogicznie, żeby zrozumieć taki kod, trzeba chodzić po różnych plikach i próbować sobie w myślach to połączyć). Czyli teoretycznie poprawna architektura powoduje jednak natłok mentalny.

Czy więc może być tak, że architektura i wygoda dla programisty czasem ze sobą walczą?

A może jednak sposób, w jaki są pisane aplikacje, nie jest doskonały? (w przypadku powyżej może należałoby pracować bardziej na ficzerach, które tworzą zamkniętą całość, a nie dzielić kod wg konkretnej roli (komponenty, reducery, thunki itp.) i w rezultacie kod dotyczący jednego ficzera będzie się walać po całym projekcie? Może problemem jest, że w wielu projektach architekturę można by ocenić tak na 3 (czyli ocena zaliczająca, ale jednak nie jest to 4 czy 5). Czyli "can we do better"?

0

@YetAnohterone: Istotą abstrakcji jest możliwość spojrzenia inaczej na kod przez co łatwiej wyłuskać interesujący nas fragment. Przykład od wujka Boba
vs. kod robiący to samo od jakiegoś redditora . 105 vs 33 lini. Według mnie wujek Bob zrobił koszmarną abstrakcję, bo metody nie pozwalają na wgłębienie się w interesujący nas kawałek, gdyż istotą działania klasy jest mutowanie pól prywatnych. W efekcie ogarnięcie kodu to przeczytanie całości kilka razy i o dziwo nazwy metod nie pomagają. Drugi przykład ma tylko dwie funkcje, ale są one bardziej "funkcyjne" tj. dużo łatwiej jest mi ogarnąć która zmienna/stała zależy od której. Nie muszę nawet czytać nazw metod, bo po samych zmiennych i argumentach idzie ogarnąć o co mniej więcej chodzi. Dla mnie krótkie funkcje są na tak, ale tylko gdy ma to sens.

Oczywiście wszystko zależy od preferencji i osobistych doświadczeń. Każdy pochodzi od innego środowiska, uczył się w inny sposób i używał innych technologii. Przykładowo ja wolę taki kod:

children.filter(it.isGirl)

a ktoś inny woli taki

filterGirls(children)

Lubię pierwszy przykład, bo nie lubię robić niepotrzebnych metod jak coś jest do wyrażenia przy pomocy połączenia kilku bazowych. Ktoś może lubić drugi przykład, bo jest "czytelniej"

2

@YetAnohterone trik małych funkcji polega na tym, ze możesz przerwać analizę na pewnym poziomie abstrakcji bardzo łatwo i nie wchodzić niżej w analizę, bo często w ogóle cię to nie interesuje. Jak masz wszystko inlinowane to nie da sie tego tak łatwo zrobić, bo nie wiesz gdzie dany poziom abstrakcji albo gdzie jakiś konkretny step się kończy. To jest zresztą też podobny argument jak stosowanie np. explicite imperatywnej pętli vs x = collection.map(someFunction) - przy "zwykłej" pętli musisz skupić się na analizie tego co ona robi, to może np. dzieją sie tam jakieś side-effekty, może control-flow się gdzieś przerywa itd, a jak masz jakieś map czy filter to w zasadzie widać od razu czego się spodziewasz.
Oczywiście że jak jesteś autorem kodu to wszystko wydaje się jasne, ale przy odpowiednio dużym projekcie przy którym pracują całe zespoły ludzi nie ma już tak łatwo. Nawet jeśli ty napisałeś jakiś kawałek kodu to za kilka tygodni może juz być nie do poznania, bo kilku innych programistów coś w nim zmieniało.

3
YetAnohterone napisał(a):

Tak się zastanawiam... Czy nie ma tutaj dwóch, zupełnie różnych, przeciwnych w zasadzie podejść do prostoty?

  • Podejście pierwsze, naiwne, charakterystyczne dla początkujących i chyba także - co trochę paradoksalne - do programistów naprawdę starej daty ("old Unix guy"): Kod musi być możliwy do ogarnięcia w całości przez jednego człowieka. Wskutek tego będziemy zmniejszać abstrakcje do niezbędnego minimum. Mało klas, o ile w ogóle będą klasy, bo z tym podejściem zazwyczaj wiąże się odrzucenie OOP, no ale w niektórych językach muszą być klasy, więc trochę ich, siłą rzeczy, będzie (chociaż z OOP dalej nie będzie miało to wiele wspólnego). Minimum zależności, najchętniej tylko te, które są naprawdę konieczne albo powszechnie znane. Naginanie wymogów biznesowych do tego, co dyktuje architektura programu, wrogość wobec mnożenia ficzerów. Także preferencja do mniejszej ilości funkcji, za to większych - nie do przesady jednak: tak, by funkcja była na tyle duża, by można było prześledzić bieg logiki bez skakania po kodzie, ale znowu na tyle mała, by ten bieg logiki nie był na tyle zamotany, by nie mieścił się w głowie.

Ale dlaczego ogarnianie kodu w całości i klasy mają się wykluczać? Piszę se klasę i po napisaniu wszystkie prywatne funkcje i pola mogę zapomnieć, ogarniam jej publiczne funkcje i używam w innych miejscach projektu.

0

Nie ma sensownej dyskusji nt. modelowania systemów bez kontekstu.

Inne pryncypia obowiązują w Javowym kodzie aplikacji webowej, inne w module kernela klepanym w C, a pewnie jeszcze inne w kodzie skryptów Lua do gierki.

Ogólnie temat liczby linijek był poruszany:

Kiedy gruboziarnisty kod jest lepszy?

@slsy

Istotą abstrakcji jest możliwość spojrzenia inaczej na kod przez co łatwiej wyłuskać interesujący nas fragment. Przykład od wujka Boba
vs. kod robiący to samo od jakiegoś redditora . 105 vs 33 lini. Według mnie wujek Bob zrobił koszmarną abstrakcję, bo metody nie pozwalają na wgłębienie się w interesujący nas kawałek, gdyż istotą działania klasy jest mutowanie pól prywatnych. W efekcie ogarnięcie kodu to przeczytanie całości kilka razy i o dziwo nazwy metod nie pomagają. Drugi przykład ma tylko dwie funkcje, ale są one bardziej "funkcyjne" tj. dużo łatwiej jest mi ogarnąć która zmienna/stała zależy od której. Nie muszę nawet czytać nazw metod, bo po samych zmiennych i argumentach idzie ogarnąć o co mniej więcej chodzi. Dla mnie krótkie funkcje są na tak, ale tylko gdy ma to sens.

ah ten legendarny refactor :D

chociaż on też może być i dobry jak i zły, w zależności od kontekstu.

Jeżeli ten fragment kodu to jest całościowa obsługa jakiegoś tam małego procesiku, to nie ma nic złego w tym aby to była jedna funkcja 60 LoC, a w środku kilka branchy, bo jesteś w stanie to ogarnąć jednym zerknięciem.

Jeżeli jest to jakaś biblioteka która udostępnia wiele funkcji, to lepiej aby to było bardziej granularne.

2

Nie ma czegoś takiego jak "jedno słuszne podejście". Czasami pisanie długich funkcji może mieć sens, ale dość szybko dochodzisz do momentu, kiedy gubisz się w całości, nawet jako pojedyncza osoba i autor tej całości. Zresztą nawet jeżeli pracujesz wyłącznie jako one man's army, to jest to złudzenie, bo nie widzisz zależności stojących za takim someList.sort(). a jest to pewnie całkiem spora liczba linii kodu, którą ktoś napisał i udostępnił w postaci pojedynczej metody/funkcji, dając komfort bycia nieświadomym, jak to jest robione (w przytłaczającej części przypadków).
Zakładając, że ci się ta nieświadomość podoba, to pytanie dlaczego chciałbyś pisać swój własny kod inaczej?

Na poziomie implementacji jakiegoś kawałka kodu, jesteśmy tam, gdzie byliśmy w latach 50 tych ubiegłego wieku. Mamy zmienne, możliwość przypisania do nich jakiejś wartości, podstawowe operacje na danych, instrukcje warunkowe, skoki i połączenie jednego z drugim, czyli pętle. To spokojnie wystarcza do napisania programu, który przyjmie 2 wartości i zwróci ich sumę, albo nawet będzie symulował działanie enigmy (wiem, bo pisałem jeszcze w Atari Basic).

To co się zmieniło przez te 70 lat, to złożoność systemów komputerowych, które nadal używają tych podstawowych operacji, ale do wykonywania znacznie bardziej złożonych zadań. Systemu bankowego, MRP, CMR, Netflixa, nie ogarnie jedna osoba, a nawet jak ogarnie, to będzie miała ostre problemy z odnalezieniem się po roku przerwy.

Popatrzmy na prostszy przykład - prosta aplikacja, która ma wziąć z A plik .png, zmienić go na .jpg i zapisać w miejscu B. Całość dała by się zapisać w taki sposób:

void readPngConvertAndSavePictureAsJpg(File from, File to){
~100 linii kodu
}

Jest z tym trochę problemów, bo:

  • Ciężko to przetestować, może wywalić się na odczycie, konwersji i zapisie, a wiadomo tylko, że "nie działa"
  • Patrząc na te 100 linii kodu, ciężko na pierwszy rzut oka stwierdzić, czy one robią to co mają robić
  • Jeżeli już dojdziesz do tego, że nie działa np. zapis, to nadal musisz poprawiać całość (bo etapy mogą być wymieszane między sobą)
  • Jeżeli zmieni się cokolwiek w wymaganiach (jak to w życiu) i okaże się, że masz przyjmować nie png, tylko bmp i nie z pliku tylko z serwera musisz zmieniać całość, a później zwykle okazuje się, że to nie wymaganie się zmieniło, tylko zostało dodane nowe, więc trzeba napisać ten kawałek kodu od początku.
  • Nie masz możliwości sterowania obszarem testów tego kodu.

Gdyby zamiast tych 100-200 linii kodu znalazło się tam coś takiego:

void readPngConvertAndSavePictureAsJpg(File from, File to){
byte[] input = readBytes(from);
Bitmap asBitmap= buildBitmap(input)
Jpeg jpg = converBitmapToJpg(asBitmap)
byte[] output = jpgAsBytes(jpg)
sendOuptut(output, to)
}

Okaże się, że właściwie wszystkie te problemy, które opisałem wyżej przestają istnieć

  • wystarczy 60s, żeby stwierdzić, czy ten kawałek kodu ma sens
  • można testować każdy z etapów tej konwersji
  • w razie zmiany wymagań wystarczy zmienić jedną linijkę tego kodu i dopisać kawałek nowego. Ale pisanie kodu jest proste, zmienianie kodu jest trudne.
  • możesz przetestować każdy z tych fragmentów osobno (nie zawsze warto, ale zawsze warto mieć taką możliwość)
  • jak odpalisz debuger, możesz łatwo przeskakiwać po "kamieniach milowych" tej funkcji i dość łatwo stwierdzić w którym miejscu przestaje działać
  • masz pewność, że poprawa dowolnej części kodu w tych funkcjach nie wpłynie na resztę, czyli unikasz problemów typu "naprawiłem zapis, ale przestał działać odczyt"

Przez analogię:
Wyobraź sobie projekt jakiegoś umiarkowanie skomplikowanego urządzenia - maszyna do szycia, skrzynia biegów w samochodzie. Jeżeli na nim będzie wszystko co składa się na dokumentację tego urządzenia, łącznie z szczegółami typu moment dokręcenia śrub, wymagania dotyczące materiałów, szczegóły obróbki tych części, słowem cała wiedza o tym jak wykonać to konkretne urządzenie od wyglądu obudowy po parametry hartowania kulek w każdym łożysku. Nie sądzę, żeby dało się na podstawie takiej dokumentacji wykonać to urządzenie, chociaż teoretycznie cała wiedza jest dostępna. W dodatku wykonanie zmian w takim projekcie będzie prawie nie możliwe, bo nie wiemy, jakie będą skutki tych zmian.

Dopiszę jeszcze moje rozumienie inżynierii programowania, samego programowania. Jest bardzo abstrakcyjne, ostatnio jak je tutaj opisywałem, to wywołało generalnie sprzeciw, ale to moje spojrzenie i dla mnie działa.

Oprogramowania się, w przeciwieństwie do maszyn do szycia nie "produkuje". Dla mnie nie istnieje w przypadku oprogramowania granica pomiędzy "projektowaniem" a "implementacją". Nie ważne czy robimy to w głowie, setkach stron dokumentów i diagramów, czy używając jakichś fancy narzędzi CASE.

  • Dostajemy wymagania "system ma robić to i to", "wyglądać tak"
  • Dobieramy narzędzia
  • Chwilę myślimy nad architekturą
  • Dzielimy na jakieś komponenty o znanych odpowiedzialnościach
  • Te komponenty dzielimy dalej na moduły/klasy/funkcje/metody/usługi
  • Dostajemy strukturę oprogramowania (w postaci diagramu, albo w postaci np. zbioru interface'ów)
  • Wiedząc "co ma robić jakaś jednostka kodu" dopisujemy "jak ma to zrobić", najczęściej w postaci kodu

Próbując zrozumieć jakiś system, kroki jakie muszą być wykonane, są właściwie identyczne jak przy projektowaniu. Najbardziej narażone na błędy są w tym procesie punkty wymagające manualnej pracy człowieka w celu zmiany formy tego projektu. Przerysowując wymagania na diagramy, diagramy na bardziej szczegółowe diagramy, te na "kod" można popełnić błędy.
To oznacza, że im bliżej będzie struktura kodu (wg. mnie "najbardziej szczegółowy poziom projektu"), do wymagań biznesowych, tym szybciej, łatwiej i bezpieczniej da się ją zmieniać.

2

@YetAnohterone tytułowe pytanie nawet ciekawe, ale Twój opis obydwu podejść jest mocno tendencyjny. Widząc jak postrzegasz zwolenników prostoty i jak bardzo Twoja wizja odbiega od rzeczywistości praktycznie zamknąłeś się na jakąkolwiek dyskusję. Ten opis jest tak bardzo nieracjonalny oraz mój cringe w co drugim zdaniu tak duży, że nawet nie zamierzam Ci tłumaczyć co jest w nim nie tak bo nie wierzę, żebyś był w stanie wyjrzeć poza swoją bańkę. Ustosunkuję się tylko do jednej rzeczy, gdzie wzdrygnąłem się najmocniej.

Także preferencja do mniejszej ilości funkcji, za to większych

Polecam lekturę obserwacji Johna Carmacka o inlinowaniu funkcji http://number-none.com/blow/blog/programming/2014/09/26/carmack-on-inlined-code.html.

Oczywiście nie zrobiłeś tego świadomie, tym nie mniej wrzuciłeś jednego z najasłynniejszych inżynierów oprogramowania do worka z podpisem

Naginanie wymogów biznesowych do tego, co dyktuje architektura programu, wrogość wobec mnożenia ficzerów.

A i może jeszcze jedna rzecz

Podejście pierwsze, naiwne, charakterystyczne dla początkujących

Spora część początkujących to całkiem kumaci ludzie. Nie mam na myśli tutaj newbie z forów programistycznych, tylko takich co faktycznie są w stanie coś zbudować. Tylko potem niestety trafiają na midów, którzy wmawiają im, że ich podejście jest naiwne, że jak nie masz fabryk to kod śmierdzi, że jak nie masz MVC to się nie da nawigować, że jak nie napiszesz najpierw testów to nie ma jakości i tacy juniorzy wierzą takim midom co skutkuje tym, że tak jak wcześniej byli w stanie sami coś zrobić to już nie są, bo są zajęci tym całym szumem dookoła zamiast skupić się na faktycznym problemie, który jest do rozwiązania.

8

Oczywiście, że prostota wynika z abstrakcji, a nie jej braku, bo abstrahowanie to dzielenie problemu na mniejsze w celu umożliwienia sobie skupienia się na istotnych na danym etapie elementach rozwiązywanego problemu, zaś mniej istotne elementy są delegowane do rozwiązania w innym pod-problemie.
Niestety, niektórym się wydaje, że abstrahowanie polega na używaniu interfejsów i wzorców projektowych, stąd się biorą jakieś dziwne poglądy i pomysły.

To, ile osób może w pełni znać i rozumieć dany system informatyczny, to oddzielny temat, który zależy od jego wielkości oraz tego, czy stosuje się w nim wszelkiego rodzaju dobre praktyki. Bo o ile ogromnego systemu raczej nikt w pojedynkę nie ogarnie, to nawet malutki system da się tak skomplikować, że nie ogarnie go nikt.

1

Oczywiście, że prostota wynika z abstrakcji, a nie jej braku

Tylko prostota czego? Czytania? Zrozumienia? Debugowania? Rozszerzania? Jak to napisał @slsy w komentarzu - dobra abstrakcja jest dobra. Tym nie mniej nawet jeśli odrzucimy fakt, że ludzie mocno przeginają i założymy że dorzucamy tylko tyle szumu ile wydaje nam się konieczne to i tak później osoba, która to będzie czytać/modyfikować ma do rozwiązania dwie zagadki - jaki problem kod rozwiązuje oraz jaki mentalny model autor kodu miał w głowie.

Zwolennicy OOP i pracy na wyższym poziomie abstrakcji wmawiają nam, że istnieje głównie ten drugi problem a ludzi nie mających ochoty rozwikływać zagadek pod tytułem "co autor miał na myśli" lubią wrzucać do worka "on nie rozumie architektury". Tylko, że abstrakcje ciekną i tak jak pisałem akapit wyżej, cześciej niż rzadziej programista ma przed sobą dwa problemy do rozwikłania - ten faktyczny oraz ten wymyślony przez kolegów z zespołu.

Jak żyć w takim razie? Nie mam pojęcia. Sam jednakoż uznałem, że OOP generuje za dużo szumu i zresetowałem moje podejście do architektury dziesięć lat wstecz, zanim zacząłem go używać, uczę się budować relacje między systemami na nowo. Okazało się, że programowanie przychodzi mi dużo łatwiej gdy jedyne czym się przejmuje to faktyczny problem jaki mam a nie co jakiś guru OOP pomyśli o moim kodzie. Nie wszystko związane z OOP jest złe, z tymże te dobre elementy przychodzą naturalnie bez konieczności pochłaniania ksiązek autorstwa programistycznych teoretyków.

Jak do tej pory mam tylko dwie twarde zasady

  1. Kod najpierw musi być używalny zanim stanie się reużywalny.
  2. Abstrakcje najczęściej są warte wprowadzania tylko gdy budujemy relacje między większymi systemami projektu, gdzie kilka luźno leżących funkcji załatwiających nam jakieś I/O to również abstrakcja.

I tak naprzykład chcąc mieć system działający na wielu pratformach konieczne będzie wyabstrahować sobie warstwę platformową, mając serwer http nie powinniśmy na codzień przejmować się niskopoziomową rejestracją i rutowaniem URLi. Tym samym czy konieczne byłoby używanie jakiś grubych abstrakcji, żeby przykładowo pobrać informację EXIF z jpg? Wystarczy na to jedna funkcja parsująca i jeden prosty kontener na dane, ale widziałem też, że ludzie budują wokół tego całe diagramy klas zupełnie niepotrzebnie i w tym przypadku dodatkowo abstrakcja mocno utrudnia.

2

Abstrakcja to pojęcie znacznie szersze, i można jej używać nie tylko bez OOP, ale i bez programowania w ogóle.
A jeśli ktoś robi OOP z cieknącymi abstrakcjami, i ktoś potem nie może się w tym odnaleźć, to jest to dowód na niedziałanie braku abstrakcji właśnie.

Abstrahowanie oznacza dzielenie na problemy rozwiązywalne na jednym poziomie, nie tworzenie skomplikowanych rozwiązań. To drugie to overengineering.

1

A jeśli ktoś robi OOP z cieknącymi abstrakcjami, i ktoś potem nie może się w tym odnaleźć, to jest to dowód na niedziałanie braku abstrakcji właśnie.

No niekoniecznie. Jeśli abstrakcja Ci cieknie i musisz rozwiązywać problemy na dwóch poziomach to bez jednego z tych poziomów powinno być prościej. Warstwy rozwiązującej problem nie usuniesz, za to możesz usunać wadliwą abstrakcję.

Anyway, co do reszty mam wrażenie, że się ze sobą zgadzamy, tylko używamy do tego innych słów lub patrzymy na sprawę pod innymi kątami.

4

Dostrzegam drobny problem.

Człowiek z małym doświadczeniem w tworzeniu abstrakcji (np. ja) będzie tworzył wadliwe abstrakcje, bardziej utrudniające niż ułatwiające, bo tworzenie poprawnych abstrakcji to sztuka. Kod pisany najprościej, jak to możliwe (tu najprościej w sensie: w sposób najbardziej naiwny) będzie lepszy, niż kod zawierający wadliwe abstrakcje.

Z drugiej strony tylko tworząc abstrakcje można się nauczyć tworzenia poprawnych abstrakcji? Więc faza produkowania zoverengineerowanych potworków jest konieczna?

A może nie? może raczej konieczna jest faza pisania naiwnego kodu pozbawionego abstrakcji, potem nacięcia się na skutkach braku abstrakcji i na tej podstawie zrozumienia, które i jakie abstrakcje są naprawdę potrzebne? Potrzebne abstrakcje = te, które bronią mnie przed problemami, o które już się odbiłem. Skoro już się odbiłem o jakiś problem, to pewnie będę lepiej wiedział, jak mu zapobiec na przyszłość. Natomiast będzie mniej prawdopodobne, że wpadnę w pułapkę rozwiązywania nieistniejących problemów kosztem tworzenia innych problemów

W jedną czy w drugą stronę... jak się nie wywrócisz, to się nie nauczysz.

3

no ale chyba prawie wszystko tak działa?

0

Chyba wam się forum pomyliło, takie rozważania to forum dla filozofów.

7

Abstrakcja to z definicji upraszczanie (== odcinanie szczegółów).

Jeśli więc abstrakcja nie upraszcza to albo coś nie tak z konkretną abstrakcją - złe użycie, albo coś nie tak z pojęciem prostoty u analizującego (to drugie dość częste).

3
jarekr000000 napisał(a):

Abstrakcja to z definicji upraszczanie (== odcinanie szczegółów).

Jeśli więc abstrakcja nie upraszcza to albo coś nie tak z konkretną abstrakcją - złe użycie, albo coś nie tak z pojęciem prostoty u analizującego (to drugie dość częste).

I to jest chyba istota. Ludzie w większości kojarzą "abstrakcję" z czymś bardziej skomplikowanym bo w przypadku programowania sam proces implementacji abstrakcji zwykle wymaga większego doświadczenia, wysiłku umysłowego niż implementacja czegoś "wprost / na pałę". Dlatego ludzie myślą, że abstrakcja to coś trudniejszego a nie uproszczonego.

Weźmy taki banalny CSS / HTML, w którym możemy temat ogarnąć na 2 sposoby:

  1. przypisywać kolory i rozmiary fontów wprost do konkretnych elementów na stronie ( do div, span, p itp... )
  2. stworzyć sobie warstwę abstrakcji w postaci kilku klas lub nawet z wykorzystaniem zmiennych CSS.

Młodemu i początkującemu programiście zapewne łatwiej będzie przypisać kolor i rozmiar fontu do konkretnego DIV'a niż stworzyć z głową zmienne i klasy, które będą wykorzystywane w różnych miejscach kodu. Zatem sposób pierwszy jest zapewne "łatwiejszy" i pozornie szybszy.
Drugą metodą także możemy uzyskać taki sam efekt, zatem wizualnie w obu przypadkach oczywiście osiągniemy to samo ale nie korzystając z warstwy abstrakcji jakie dają nam klasy i zmienne CSS stracimy potencjalne możliwości łatwiejszej modyfikacji całości.

Jeśli jednak ktoś powie, a teraz zrób przełącznik na tryby "dark mode" / "day mode" ( ciemny / jasny ) to nagle okaże się, że użycie abstrakcji w postaci klas i zmiennych upraszcza to zadanie właściwie do minimum bo zmieniamy tylko wartości zmiennych. Natomiast w przypadku pierwszym musimy klepać wszystko drugi raz albo szukać jakiś kombinowanych rozwiązań.

Zatem o ile to ma potencjalny i uzasadniony sens to należy z abstrakcji korzystać.
Jest też druga strona medalu bo z abstrakcjami można grubo przegiąć... Co też nie jest dobre.

4
katakrowa napisał(a):

Jest też druga strona medalu bo z abstrakcjami można grubo przegiąć... Co też nie jest dobre.

Przeginki biorą się często z tego, że ludzie nie rozumieją sensu abstrakcji, z których korzystają, a tylko naśladują.
Np. ktoś poczyta o DDD, CQRS itp. i zaczyna wrzucać całą maszynerię, o której wyczytał w książce, bez ładu i składu. Pisze, żeby pisać. Próbuje odtworzyć porządek i nazewnictwo klas z książki nie rozumiejąc, jakie problemy te klasy mają w ogóle rozwiązywać (czyli typowy cargo cult).

Drugi powód, który widzę, to jak ktoś się uczy na produkcji. Tzn. moim zdaniem "przeinżynierowanie" jest normalnym etapem nauki. Poznasz jakiś wzorzec, to chcesz go stosować wszędzie. Albo wpadasz na jakiś "genialny" pomysł (np. zrobienie skomplikowanego wrappera na istniejący framework), to czujesz, że musisz go koniecznie wypróbować. I to jest spoko jak robisz to w swoich projektach do szuflady albo nawet w ramach pracy, ale jako PoC/eksperyment. Tylko, że ludzie wolą uczyć się na produkcji. Eksperymenty, które w ogóle do mastera nie powinny trafić, są merdżowane, a później utrzymywane przez długi czas (najpierw traktowane jako "genialna nowa implementacja", a później już tylko jako legacy, bo "to stary kod, ale wtedy myśleliśmy, że to dobre podejście. Myliliśmy się")

1 użytkowników online, w tym zalogowanych: 0, gości: 1