Czy to dobry wzór: zastąpienie długiej funkcji serią lambdów?

14

Niedawno wpadłem na następującą sytuację.

class A{
public:
    void calculate(T inputs);
}

Po pierwsze, Areprezentuje obiekt w świecie fizycznym, co jest silnym argumentem za nierozdzielaniem klasy. Teraz calculate()okazuje się dość długą i skomplikowaną funkcją. Widzę trzy możliwe struktury:

  • napisz to jako ścianę tekstu - zalety - wszystkie informacje są w jednym miejscu
  • pisz privatefunkcje użytkowe w klasie i używaj ich w calculateciele - wady - reszta klasy nie zna / nie dba / nie rozumie tych metod
  • napisz calculatew następujący sposób:

    void A::calculate(T inputs){    
        auto lambda1 = () [] {};    
        auto lambda2 = () [] {};    
        auto lambda3 = () [] {};
    
        lambda1(inputs.first_logical_chunk);
        lambda2(inputs.second_logical_chunk);
        lambda3(inputs.third_logical_chunk);
    }
    

Czy można to uznać za dobrą lub złą praktykę? Czy takie podejście ujawnia jakieś problemy? Podsumowując, czy powinienem uznać to za dobre podejście, gdy znów mam do czynienia z tą samą sytuacją?


EDYTOWAĆ:

class A{
    ...
public:
    // Reconfiguration of the algorithm.
    void set_colour(double colour);
    void set_density(double density);
    void set_predelay(unsigned long microseconds);
    void set_reverb_time(double reverb_time, double room_size);
    void set_drywet(double left, double right);
    void set_room_size(double value);;

private:
    // Sub-model objects.
    ...
}

Wszystkie te metody:

  • uzyskać wartość
  • oblicz inne wartości bez użycia stanu
  • wywołać niektóre z „obiektów podmodelu”, aby zmienić ich stan.

Okazuje się, że poza set_room_size()tymi metodami po prostu przekazują żądaną wartość do podobiektów. set_room_size()z drugiej strony wykonuje kilka ekranów niejasnych wzorów, a następnie (2) wykonuje pół ekranu wywoływania seterów podobiektów w celu zastosowania różnych uzyskanych wyników. Dlatego podzieliłem funkcję na dwie lambdy i wywołałem je na końcu funkcji. Gdybym był w stanie podzielić to na bardziej logiczne części, izolowałbym więcej lambdów.

Niezależnie od tego, celem obecnego pytania jest ustalenie, czy taki sposób myślenia powinien się utrzymywać, czy w najlepszym razie nie stanowi wartości dodanej (czytelność, łatwość konserwacji, zdolność debugowania itp.).

Vorac
źródło
2
Jak myślisz, co przy użyciu lambdas sprawi, że wywołania funkcji nie będą?
Blrfl
1
Firstly, A represents an object in the physical world, which is a strong argument for not splitting the class up.Z pewnością Areprezentują dane o obiekcie, który mógłby istnieć w świecie fizycznym. Możesz mieć instancję Abez rzeczywistego obiektu i prawdziwy obiekt bez instancji A, więc traktowanie ich tak, jakby były jednym i tym samym, jest nonsensowne.
Doval
@Blrfl, enkapsulacja - nikt nie calculate()będzie wiedział o tych podfunkcjach.
Vorac
Jeśli wszystkie te obliczenia dotyczą tylko A, to prowadzi to do skrajności.
Blrfl
1
„Po pierwsze, Areprezentuje obiekt w świecie fizycznym, co jest mocnym argumentem za tym, aby nie dzielić klasy”. Niestety powiedziano mi to, kiedy zaczynałem programować. Lata zajęło mi uświadomienie sobie, że jest to hokej na trawie. To okropny powód do grupowania rzeczy. Nie potrafię wyartykułować, jakie dobre powody, by grupować rzeczy (przynajmniej dla mojej satysfakcji), ale ten właśnie należy odrzucić teraz. Koniec końców „dobry kod” polega na tym, że działa poprawnie, jest względnie łatwy do zrozumienia i stosunkowo łatwy do zmiany (tj. Zmiany nie mają dziwnych skutków ubocznych).
jpmc26

Odpowiedzi:

13

Nie, ogólnie nie jest to dobry wzór

To, co robisz, polega na rozbiciu funkcji na mniejsze funkcje za pomocą lambda. Istnieje jednak znacznie lepsze narzędzie do dzielenia funkcji: funkcji.

Lambdy działają, jak widzieliście, ale znaczą o wiele, wiele więcej, niż zwykłe rozbicie funkcji na lokalne elementy. Lambdas:

  • Domknięcia. Możesz używać zmiennych w zakresie zewnętrznym wewnątrz lambda. Jest to bardzo potężne i bardzo skomplikowane.
  • Zmiana przypisania Podczas gdy twój przykład utrudnia wykonanie zadania, zawsze należy zwrócić uwagę na pomysł, że kod może zamieniać funkcje w dowolnym momencie.
  • Funkcje pierwszej klasy. Możesz przekazać funkcję lambda do innej funkcji, robiąc coś zwanego „programowaniem funkcjonalnym”.

Gdy tylko włączysz lambdy do miksu, następny programista, który zajrzy do kodu, musi natychmiast załadować mentalnie wszystkie te reguły, przygotowując się do zobaczenia, jak działa Twój kod. Nie wiedzą, że nie będziesz korzystać z całej tej funkcjonalności. Jest to bardzo kosztowne w porównaniu do alternatyw.

To tak, jakbyś używał motyki do ogrodnictwa. Wiesz, że używasz go tylko do kopania małych dziur na tegoroczne kwiaty, ale sąsiedzi się denerwują.

Pomyśl, że wszystko, co robisz, to grupowanie kodu źródłowego wizualnie. Kompilator tak naprawdę nie dba o to, abyś curry rzeczy z lambdas. W rzeczywistości spodziewałbym się, że optymalizator natychmiast cofnie wszystko, co właśnie zrobiłeś podczas kompilacji. Obsługujesz wyłącznie kolejnego czytelnika (Dziękujemy za zrobienie tego, nawet jeśli nie zgadzamy się co do metodologii! Kod jest czytany znacznie częściej niż jest napisany!). Wszystko, co robisz, to funkcja grupowania.

  • Funkcje umieszczone lokalnie w strumieniu kodu źródłowego działałyby równie dobrze, bez wywoływania lambda. Ponownie liczy się tylko to, że czytelnik może to przeczytać.
  • Komentarze u góry funkcji mówią „dzielimy tę funkcję na trzy części”, po których następują duże długie linie // ------------------między każdą częścią.
  • Możesz także umieścić każdą część obliczeń we własnym zakresie. Ma to tę zaletę, że natychmiast dowodzi ponad wszelką wątpliwość, że nie ma podziału zmiennych między częściami.

EDYCJA: Patrząc na twoją edycję z przykładowym kodem, pochylam się, aby notacja komentarza była najczystsza, z nawiasami, aby wymusić granice sugerowane przez komentarze. Jednak jeśli którakolwiek z funkcji może być ponownie użyta w innych funkcjach, zalecam użycie funkcji zamiast tego

void A::set_room_size(double value)
{
    {
        // Part 1: {description of part 1}
        ...
    }
    // ------------------------
    {
        // Part 2: {description of part 2}
        ...
    }
    // ------------------------
    {
        // Part 3: {description of part 3}
        ...
    }
}
Cort Ammon
źródło
Nie jest to więc liczba obiektywna, ale subiektywnie twierdziłbym, że sama obecność funkcji lambda sprawia, że ​​zwracam około 10 razy większą uwagę na każdą linię kodu, ponieważ mają one tak duży potencjał, że mogą się niebezpiecznie skomplikować, szybko i tak dalej niewinnie wyglądająca linia kodu (jak count++)
Cort Ammon
Świetny punkt Widzę to jako nieliczne zalety podejścia z kodem lambdas - (1) lokalnie sąsiadującym i z zasięgiem lokalnym (byłoby to utracone przez funkcje na poziomie plików) (2) kompilator zapewnia, że ​​żadne zmienne lokalne nie są współużytkowane między segmentami kodu. Tak więc zalety te można zachować, dzieląc je calculate()na {}bloki i deklarując udostępnione dane w calculate()zakresie. Myślałem, że widząc, że lambdas nie schwytają, czytelnik nie będzie obciążony mocą lambdas.
Vorac
„Myślałem, że widząc, że lambdas nie schwytają, czytelnik nie byłby obciążony mocą lambdas”. To właściwie uczciwe, ale kontrowersyjne stwierdzenie, które uderza w sedno językoznawstwa. Słowa zwykle mają konotacje, które wykraczają poza ich znaczenia. To, czy moja konotacja lambdajest niesprawiedliwa, czy też niegrzecznie zmuszasz ludzi do przestrzegania ścisłej denotacji, nie jest łatwym pytaniem. W rzeczywistości może być całkowicie akceptowalne, abyś używał lambdatego w swojej firmie i zupełnie nie do zaakceptowania w mojej firmie, i żadne z nich nie musi się mylić!
Cort Ammon
Moja opinia na temat konotacji lambda wynika z tego, że dorastałem na C ++ 03, a nie C + 11. Spędziłem lata, doceniając konkretne miejsca, w których brak C ++ został zraniony lambda, takie jak for_eachfunkcja. W związku z tym, gdy widzę, lambdaże nie pasuje do jednego z tych łatwych do wykrycia problemów, pierwsze założenie, że doszedłem do wniosku, to, że prawdopodobnie będzie on używany do programowania funkcjonalnego, ponieważ inaczej nie był potrzebny. Dla wielu programistów programowanie funkcjonalne jest zupełnie innym sposobem myślenia niż programowanie proceduralne lub programowanie OO.
Cort Ammon
Dziękuję za wyjaśnienie. Jestem początkującym programistą i cieszę się, że zapytałem - zanim nawyk się rozwinął.
Vorac
20

Myślę, że źle przyjęłeś:

Po pierwsze, A reprezentuje obiekt w świecie fizycznym, co jest silnym argumentem za tym, aby nie dzielić klasy.

Nie zgadzam się z tym. Na przykład, gdybym miał klasę reprezentującą samochód, zdecydowanie chciałbym go podzielić, ponieważ z pewnością chcę, aby mniejsza klasa reprezentowała opony.

Tę funkcję należy podzielić na mniejsze funkcje prywatne. Jeśli naprawdę wydaje się oddzielony od innej części klasy, może to oznaczać, że klasa powinna być oddzielona. Oczywiście trudno powiedzieć bez wyraźnego przykładu.

W tym przypadku nie widzę korzyści z używania funkcji lambda, ponieważ tak naprawdę nie czyści kodu. Zostały one stworzone, aby wspomóc programowanie w stylu funkcjonalnym, ale to nie wszystko.

To, co napisałeś, przypomina trochę zagnieżdżone obiekty funkcyjne w stylu Javascript. Co znowu jest znakiem, że należą do siebie ściśle. Czy jesteś pewien, że nie powinieneś tworzyć dla nich osobnej klasy?

Podsumowując, nie sądzę, że jest to dobry wzór.

AKTUALIZACJA

Jeśli nie widzisz żadnego sposobu na zawarcie tej funkcjonalności w znaczącej klasie, możesz utworzyć funkcje pomocnicze o zasięgu pliku, które nie są członkami twojej klasy. To w końcu C ++, projektowanie OO nie jest koniecznością.

Gábor Angyal
źródło
Brzmi rozsądnie, ale trudno mi sobie wyobrazić wdrożenie. Klasa zakresu plików metodami statycznymi? Klasa zdefiniowana w środku A(może nawet funktor z wszystkimi innymi metodami prywatnymi)? Klasa zadeklarowana i zdefiniowana w środku calculate()(wygląda to bardzo podobnie do mojego przykładu lambda). Jako wyjaśnienie, calculate()jest jedną z rodziny metod ( calculate_1(), calculate_2()itp.), Z których wszystkie są proste, tylko ta jest 2 ekranami formuł.
Vorac
@Vorac: Dlaczego jest calculate()o wiele dłuższy niż wszystkie inne metody?
Kevin
@Vorac Naprawdę trudno jest nie widzieć kodu. Czy możesz to również opublikować?
Gábor Angyal
@Kevin, wzory na wszystko są podane w wymaganiach. Kod wysłany.
Vorac
4
Głosowałbym za tym 100 razy, gdybym mógł. „Modelowanie obiektów w prawdziwym świecie” jest początkiem spirali śmierci związanej z projektowaniem obiektów. To gigantyczna czerwona flaga w dowolnym kodzie.
Fred the Magic Wonder Dog