Efekty Uboczne

  • by
woman sitting painting

To co tu jest opisane wydarzyło się naprawdę 😎

Dawno temu podczas jedno z wielu code review jakie się robi pewien deweloper postanowił w funkcji, która zwraca opis ścieżki dla jednego tylko przypadku otworzyć przeglądarkę na urządzeniu. Aby to nie było takie hipotetyczne powiedzmy, że metoda wyglądała tak:

func fakeFunction(_ input: Input) -> [Step] {
    switch input {
        case ...: return [.step1, .step2]

        case someInputCase(let assosiatedUrl):
            UIApplication.shared.open(assosiatedUrl) // <-- side effect!

            return [.step1, .step2]


        case ...: return [.step1, .step2]
    }
}

Będąc czepliwym można powiedzieć, że to nie jest dobry kod ponieważ funkcja odpowiada za więcej niż jedną rzecz. Dodatkowo otwieranie tego URL-a było robione dla tylko tego przypadku, więc nawet nie była to konwencja w tej metodzie.

Widząc coś takiego mogłem zostawić tylko taki komentarz (mniej więcej taki):

This is a side effect. You should not do this in this method.

W odpowiedzi dostałem:

No it’s not.

Pozostawiając bez komentarza zostawiony komentarz do mojego komentarza nasuwa się bardzo dobre pytanie.

Czym jest efekt uboczny w programowaniu?

Wszystko to co zmienia świat zewnętrzny jest efektem ubocznym. W przypadku funkcji fakeFunction jej światem są argumenty wejściowe i wszystkie zmienne/stałe lokalne, które ona tworzy. W chwili gdy fakeFunction otwiera przeglądarkę to świat zewnętrzny się zmienia dosyć mocno. Co więcej tak trywialna rzecz jak print w konsoli też jest efektem ubocznym. Ponieważ świat zewnętrzny zmienił swój stan.

Jeżeli dalej nie wierzysz mi, że takie zachowanie to jest efekt uboczny to mam jeszcze jeden argument. Taki, który też lepiej pokaże kiedy w kodzie występują efekty uboczne.

Funkcja pozbawiona efektów ubocznych dla tych samych argumentów wejściowych zwraca tą samą wartość. Jest to bardzo pożądana cecha przy pisaniu każdego oprogramowania ponieważ pozwala mentalnie zamienić wywołanie funkcji z jej zwracaną wartością.

I to jest właśnie clue. Gdybym zamienił wywołanie funkcji na jej zwracaną wartość czyli tablice zawierającą kroki [.step1, .step2] to zachowanie aplikacji by się zmieniło! Strona by się nie otworzyła!

Dlaczego to jest takie ważne?

Chcemy aby kod działał zawsze i do tego zawsze tak samo a nie inaczej np. w piątek o 16:00 niż przez resztę tygodnia. To pozwala myśleć o kodzie lokalnie. Mam ten mały świat, który teraz muszę ogarnąć i tylko tyle. Jest to o wiele łatwiejsze niż trzymanie w głowie wielu zmiennych, które wpływają na działanie kawałka kodu.

Drugą pożądaną cechą jest łatwiejsze testowanie takich funkcji. Wrzucamy argumenty wejściowe i sprawdzamy asercją czy wynik jest ten co trzeba. W pewnych sytuacjach można nawet pokusić się o testy snapshotowe takich funkcji.

Czyste funkcje, pozbawione efektów ubocznych, komponują się bardzo dobrze. Funkcje z efektami ubocznymi już nie! Klasycznym przykładem jest współdzielony zasób. Działanie jednej funkcji może wpływać na wynik drugiej. Co gorsze tylko od czasu do czasu!

To na co komu te efekty uboczne?

Skoro są takie złe to może trzeba pisać takie programy, które są ich pozbawione?

Nie do końca. Efekty uboczne to jest to po co uruchamiamy programy. Chcemy aby coś się pojawiło na ekranie, otworzyła się strona, powstał zapis w bazie danych etc. Sztuka polega na tym aby wypchnąć je jak najdalej od głównej logiki i zostawić gdzieś na peryferiach.

Jak jeszcze rozpoznać funkcje mające efekty uboczne w kodzie?

Całe szczęście jest to dosyć łatwe. Jeżeli jakaś funkcja zwraca Void to znaczy, że wykonuje jakiś efekt uboczny. Nie zawsze to jest złe i nie zawsze trzeba wytaczać wielkie armaty. Zawsze warto po prostu o tym pamiętać i być tego świadomym.

To co robie osobiście i polecam każdemu to dodanie takiego prostego snippeta:

typealias SideEffect = Void

Teraz wystarczy wszędzie tam gdzie korci użycie Void napisać func someFunction() -> SideEffect aby mieć przypomnienie, że coś jednak magicznego się dzieje 😎

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *