Gdzie jest granica między powtarzaniem a uogólnianiem?

0

Jak wiemy uogólnianie bywa dobre, wynika z DRY i często jak coś się notorycznie powtarza to jednak warto to uogólnić, by mieć jedną definicję w systemie. Inaczej prosimy się o kłopoty, prawda?

Pisząc ogólnie można się zakręcić, poniżej daję dwa najprostsze przykłady jakie przychodzą mi do głowy, przy których zdawanie się na logikę nie wiele mi pomaga:

  1. Jak w pythonie zapiszemy wywołanie funkcji map zamiast pętli to taka rzecz byłaby mniej czytelna, prawda? Mniej osób spodziewa się map, mniej osób jest przygotowana na zderzenie (że teraz map zwróci generator), co więcej by zrozumieć wywołanie musi też rozumieć callback jaki otrzymał map. Możliwe, że w innych językach użycie map byłoby lepsze, ale w pythonie ten for dla wielu osób jest jednak poręczniejszy / prostszy w dalszym dostosowywaniu nawet jeśli 90% przypadków to typowe iterowanie.

  2. Gdy masz warunek, który sprawdza czy wartość jest None, jeśli jest None to rzucasz wyjątek, który jest znany systemowi. Czy lepiej wtedy powtarzać te warunki i za każdym razem pisać if + raise, czy może lepiej jest mieć funkcję, która nazywa się check_value i ona sama to ogarnia (tzn ukrywa ifa i rzuca wyjątek). W takim przypadku też myślę, że mimo powtarzania to taki if + raise pisany za każdym razem z palca jest przyjemniejszy w odbiorze niż check_value po którym nie zawsze wiadomo czego się spodziewać.

Z drugiej strony jak mamy 2 wartości i mamy wybrać tą mniejszą to zamiast warunku ludzie preferują użyć funkcję min. Nie jest to dziwne?

By problem nie dotyczył samego pythona zwróćcie uwagę na pisanie kodu reacta i html. W react używamy html, bazujemy na nim i powtarzamy te znaczniki. Możemy z nich robić komponenty i jeśli te znaczniki mają znaczenie to tym bardziej możemy robić z nich komponenty. Gdy mam komponent, który składa się z N komponentów (np. lista linków) to mam wrażenie, że oszukuje siebie samego - bo akurat w tym przypadku pisanie konkretnego html bez abstrakcyjnej otoczki byłoby prostsze do zrozumienia. Może niekoniecznie to byłaby łatwa rzecz do rozszerzenia, ale do wprowadzania prostych modyfikacji jak najbardziej tak. W końcu to tylko lista linków.

Jeszcze mógłbym wypisać parę przypadków, ale docelowo cieżko mi jednoznacznie stwierdzić co sprawia, że w danym przypadku uogólnienie może być gorsze niż powtarzanie.

Mam nadzieję, że odpowiedź nie zależy od przypadku / natury / rozsądku / umiaru - chociaż sam tak to na razie postrzegam. Im bardziej o tym myślę tym bardziej wydaje mi się, że jest to swego rodzaju moja wymówka, unik od konkretnej odpowiedzi.

1
  1. W pythonie powinno się użyć list comphrehension. To jest python way i tego się każdy spodziewa.
  2. A może coś na wzór Optional/Option? ;)
1

Ktoś tam zapytany o definicję pornografii powiedział, że jej nie zna, ale wie, że to ona, gdy ją widzi. Podobnie jest z czystym kodem, to po części również intuicja i przyzwyczajenie. Czasem zamiana kodu, który się powtarza w dwóch miejscach wydaje się oczywista, czasem nie.

Z grubsza biorąc, jest trochę ustalonych konwencji, przykładowo list comprehension praktycznie zawsze jest lepszym rozwiązaniem niż map. Od lat nie miałem okazji zastosować map w swoim kodzie i usuwam podczas refactoringów jeżeli napotkam. Co do drugiego problemu, to wszystko zależy co i jak chcesz napisać, bo strategie i podejścia są różne. Przypadkowo, można zamiast None zwracać domyślną wartość, wówczas masz gwarantowane, że wyjątek nigdy nie nastąpi. Przykładowo przy wywołaniu get na słowniku możesz podać domyślną wartość. Jeżeli takiej metody nie ma to można łatwo uzyskać domyślną wartość z użyciem or

x = some_function_that_can_return_none() or 'default_value'

Jeżeli musisz jednak rzucić swój wyjątek, to możesz go przerzucić do tej funkcji. Albo i nie. Popatrzmy na kod.

def foo():
    # ...
    ret = None
    # ...
    return ret

x = foo()
if x is None:
    raise MyException()

Taki kod może mieć sens, czemu nie, jeżeli akurat w jakimś konkretnym miejscu chcesz propagować swój wyjątek. Jeżeli jednak wywołujesz foo() w 10 miejscach i za każdym razem dajesz tego ifa to być może lepiej będzie przenieść rzucanie do samej funkcji:

def foo():
    # ...
    ret = None
    # ...
    if ret is None:
        raise MyException()
    return ret

x = foo()

To może mieć sens, ale nie musi. Przykładowo, może chcielbyśmy, aby foo() było "czyste" i zawsze coś zwracało, a nie rzucało wyjątki...? To zatem co należy w danej sytuacji zrobić zależy od konkretnej sytuacji.

0

A co byście powiedzieli na to, ze docelowo chodzi o unikanie takiej jakby eksplozji w kodzie? Tzn uogólniać kod o ile nie doprowadzi do swego rodzaju wybuchu sztucznych tworów, takich o wątpliwej użyteczności? Czyli problem bezpośrednio nie jest w samej konstrukcji tylko tym jak ona rzutuje na pozostałe przypadki.

Przykład z map, skutkuje tym, że trzeba doprodukować wiecej pomniejszych funkcji, trzeba później te funkcje znać, czasem składać, i to jest mało poręczne. Szczególnie gdy wynik z map następnie trzeba przetworzyć czymś pokrewnym typu filter. W sume taka praktyka z map/filter ma jedynie sens gdy obracasz niemutowalnymi wartościami - inaczej naprawdę trudno to usprawiedliwić.

Przykład z wyjątkami podobnie - jeśli wyjątek opakujemy w funkcje check_value, to jak mam postąpić w przypadku innych wyjątków? Kolejne funkcje check_something? Potem pojawia sie myśl, czy z każdym wyjątkiem mam tworzyć? A potem pytanie: czemu w pythonie (czy innym języku) od razu nie doprodukowuje się taka funkcja.

Przykład z min ma to do siebie, że ciężko to przekombinować, niedoprowadza do powstawania kolejnych konstrukcji (no może cześciowo nie doprowadza, bo można przekazać predykaty).

Może o to chodzi, co myślicie o tym?

EDIT:

Analogiczny przypadek widzę w funkcjach liczba pojedyncza, liczba mnoga. Dla przykładu:

sendEmail(email)
sendEmails(emails)

Druga funkcja jeśli nie ma czegoś co sprawa, że operacja działa wydajniej, a zamiast tego pod spodem jest tylko pętla for która używa sendEmail - to taki przypadek jest totalnie niepraktyczny - bo utrudnia dostosowanie, i skutkuje produkcją kolejnych niewiele wnoszących funkcji.

1
semicolon napisał(a):

Możliwe, że w innych językach użycie map byłoby lepsze, ale w pythonie ten for dla wielu osób jest jednak poręczniejszy / prostszy w dalszym dostosowywaniu nawet jeśli 90% przypadków to typowe iterowanie.

A co Ci daje map w pythonie? W haskellu map się przydaje, bo w tym języku można robić czytelną kompozycję funkcji, masz częściową aplikację i lambdy nie są tak paskudne/upośledzone. I tu nawet nie mówię, że fp > wszystko inne, smalltalk też ma niezłą obsługę bloków.

Przykład z map, skutkuje tym, że trzeba doprodukować wiecej pomniejszych funkcji, trzeba później te funkcje znać, czasem składać, i to jest mało poręczne. Szczególnie gdy wynik z map następnie trzeba przetworzyć czymś pokrewnym typu filter. W sume taka praktyka z map/filter ma jedynie sens gdy obracasz niemutowalnymi wartościami - inaczej naprawdę trudno to usprawiedliwić.

Z operatorami . albo nawet |> robi się całkiem poręczne.

0
semicolon napisał(a):

By problem nie dotyczył samego pythona zwróćcie uwagę na pisanie kodu reacta i html. W react używamy html, bazujemy na nim i powtarzamy te znaczniki. Możemy z nich robić komponenty i jeśli te znaczniki mają znaczenie to tym bardziej możemy robić z nich komponenty. Gdy mam komponent, który składa się z N komponentów (np. lista linków) to mam wrażenie, że oszukuje siebie samego - bo akurat w tym przypadku pisanie konkretnego html bez abstrakcyjnej otoczki byłoby prostsze do zrozumienia. Może niekoniecznie to byłaby łatwa rzecz do rozszerzenia, ale do wprowadzania prostych modyfikacji jak najbardziej tak. W końcu to tylko lista linków.

nie zawsze chodzi o czytelność. Bo ten zapis:

<a href="https://example.com" />
<a href="https://google.com" />
<a href="https://wikipedia.org"/>

będzie bardzo czytelny dla każdego, kto miał do czynienia z HTMLem. Użycie abstrakcji "lista linków" (np. w postaci tablicy) i wyświetlanie jej za pomocą funkcji map może nie być już tak czytelne.

Jednak podejście straightforward ma wadę, że wpisujemy na sztywno listę linków w HTML(czy w zasadzie w JSX). I jak lista się zmieni, to będziemy musieli edytować komponent. Poza tym nie będziemy mogli listy linków np. pobrać AJAXem.

A jeśli pójdziemy w abstrakcję i umieścimy linki np. w tablicy:

["https://example.com", "https://google.com", "https://wikipedia.org"]

To potem możemy zrobić z nimi wszystko. Pobierać je z netu, albo jakoś transformować, modyfikować, dodawać do listy itp. i na końcu renderować dowolnie, jak chcemy.

O ile faktycznie umieścimy je jako dane.

Natomiast czasem widuję podejście, że ludzie robią komponenty reactowe, ale traktują je tylko jak "zbiorniki na tagi". I np. w tym przypadku to mogłoby być wydzielanie komponentu Links tylko po to, żeby wrzucić tam luzem tagi <a> z hardkodowanymi adresami. Czyli niby jest abstrakcja, ale tak, jakby jej nie było.

Albo załóżmy, że by ktoś sobie wymyślił jakiś DSL, że zamiast <a> będzie komponent <Link> i by używało się komponentu Links i Link o tak np.:

<Links>
       <Link href="https://example.com" />
       <Link href="https://google.com" />
       <Link href="https://wikipedia.org"/>
</Link>

dla mnie to mija się z celem i z jednej strony przeinżynierowanie (bo kod staje się mniej czytelny i nabuzowany abstrakcjami), a z drugiej strony niedoinżynierowanie (bo nie mamy abstrakcji na dane, tylko je wrzucamy "luzem" do komponentu Link). Czyli jest to nie do końca fajne. A podobne kody widuję, w których ludzie za dużo danych wpychają luzem do komponentów (chociaż czasem taki zapis jak powyżej mógłby być okej, wszystko zależy od konkretnego przypadku, żeby nie przesadzić w żadną stronę).

Gdzie jest granica między powtarzaniem a uogólnianiem?

odpowiedź na pytanie znajduje się w artykule The Wrong Abstraction: https://www.sandimetz.com/blog/2016/1/20/the-wrong-abstraction

0

@LukeJL: Dzięki za link, ale gościu tylko straszy przed złem i nic więcej :-(

Na razie czuję, że ta zła abstrakcja to taka, która powstaje po to, aby wyrazić to samo działanie N razy. Druga poprawna, gdy docelowo chodzi o wyrażenia działania na N różnych sposobów.

By daleko nie sięgać to 1 typ rozpoznaje właśnie u Juliana, który skręca w kierunku ogólnego kodu: [scala]: tworzenie klasy przez refleksję

Być może przykładem jasnej abstrakcji byłoby użycie kompletnego mappera. Mapper koncentruje się na sposobie, on nie wie jakie docelowo będą obsługiwane klasy.

Ale w przypadku mapperów to sam nie wiem czy to jest dobry przykład, co jesli mapper przecieka?

z jednej strony przeinżynierowanie (bo kod staje się mniej czytelny i nabuzowany abstrakcjami), a z drugiej strony niedoinżynierowanie (bo nie mamy abstrakcji na dane, tylko je wrzucamy "luzem" do komponentu Link)

Tak. Myślę, że już teraz wiesz co miałem na myśli pisząc wyżej o oszukiwaniu samego siebie.

W przypadku przeglądarki widzę parę problemów, które uniemożliwiają mi podejmowanie większych założeń:

  1. W komponentach najgorsze jest to, że to trochę mix logiki i mix wyglądu. Ciężko jest wystawiać abstrakcyjne rzeczy, bo za jakiś czas albo jedno albo drugie zostanie złamane przez komponenty pochodne i wychodzi efekt odwrotny od zamierzonego. Pół biedy jeśli mój komponent nie ma logiki, a ma tylko strukturę z elementami wyglądu, ale to i tak nie jest warte wprowadzania dodatkowych abstrakcji, ponieważ robiąc abstrakcje muszę wystawić sposób użycia - co jeśli mój pochodny komponent musi przekazać jakieś propsy do komponentu bazowego, np. skorygować styl jednego ze znaczników, a jeśli tak to który znacznik będzie w centrum zainteresowania - w tym wszystkim jest za dużo wyborów, by ostatecznie uzyskać spójne api komponentów.

Tutaj techniki jakie były dobre w przypadku GUI (klasy / dziedziczenie) słabo się sprawdzają moim zdaniem. W GUI rozszerzanie widgetów miało więcej sensu bo wszędzie widgety miały podobny styl, i co więcej były bardziej regularne. Tu w web jest więcej nieregularności, przez co więcej rzeczy może się w międzyczasie zmienić np. technika stylowania, animacji i innych pierdół.

Pomyśl też o abstrakcjach jakie abstrakcje są najcenniejsze? Te najmniejsze, te które opierają się na 1 prostym zalożeniu, bo inaczej łatwo je złamać albo są zbyt złożone. W przypadku komponentów ciężko jest wskazać taką dobrą abstrakcję, mieszanie wyglądu i logiki to najgorsze co może być.

W takiej sytuacji myślę, że programiści frontu są jak pingwinu lub też zwykłe kury - mają skrzydała (środki do wyrażania abstrakcji), ale jakoś latanie nie było szczególnie im pisane.

Dlatego w przypadku html odrzucam wiele pomysłów z abstrakcją i idę bardziej w kierunku programowania zorientowanego na dane.

  1. Ja komponenty nie odbieram jak element designu (łatwości wprowadzania zmian powiązanego z pomyślaną strategią), moim zdaniem bliżej im do elementu architektury, w tym przypadku kodu pisanego docelowo pod przeglądarki. I o ile koszty odkręcania decyzji na podłożu architektury są duże, o tyle nie obawiam się bycia zaskoczonym na podłożu przeglądarek.

Może architektura jest zła, bo nie jest tak sexy jak design, ale jak wiesz jaka masz architektura wtedy możesz sobie pozwolić na pisanie docelowo konkretnego kodu, a taki kod pisze się najszybciej i w sumie też najprościej jest go pojąć.

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