Po co rozdzielamy funkcje?
Ostatnio obejrzałam na youtubie film Object-Oriented Programming is Bad. Jedną z porad, których udziela Brian Will, jest: Don’t be afraid of long functions. A ja wszędzie w trakcie mojej nauki programowania słyszę, że rozdzielanie funkcji jest dobre i absolutnie trzeba to robić. Jasne, sam film w tytule zakłada krytykę niektórych zasad OOP, ale rozdzielanie funkcji nie dotyczy jedynie OOP. Dlatego zdecydowałam się pochylić nad tematem, trochę się pozastanawiać i coś z tego kontrastu opinii wyciągnąć. Czego rezultat tutaj prezentuję.
Ważna uwaga: Nie mam komercyjnego doświadczenia. Nadal się uczę, mam za sobą kilka umiarkowanie skomplikowanych projektów i zaledwie jedną kontrybucję do projektu open source. Także w żadnym wypadku nie wypowiadam się z pozycji autorytetu, a jedynie przedstawiam swoje przemyślenia.
O co chodzi z rozdzielaniem funkcji?
Każdy, kto zacznie się uczyć programowania, w pewnym momencie usłyszy, że funkcje należy rozdzielać na mniejsze. Jeśli zrobimy to dobrze, otrzymamy konretne korzyści:
- kod będzie znacznie bardziej czytelny
- nie będziemy powtarzać kodu
- nie będziemy mieszać różnych poziomów abstrakcji
Zdecydowanie tych korzyści może być więcej, ale te są dla mnie, jako początkującej, najbardziej istotne.
Przykład
Oto kod, który napisałam około półtora roku temu:
@action(methods=['post'], detail=True)
def revise(self, request, *args, **kwargs):
topic = Topic.objects.filter(pk=kwargs['pk'])[0]
field = Field.objects.filter(pk=kwargs['field_pk'])[0]
self.set_last_reviewed_today([topic, field])
revision = self.create_revision(field)
data = TopicSerializer(topic).data
return Response(data)
def set_last_reviewed_today(self, objects):
for o in objects:
o.last_reviewed = datetime.date.today()
o.save()
def create_revision(self, field):
date = self.get_revision_date(field)
revision = Revision.objects.create(date=date, field=field)
revision.save()
return revision
def get_revision_date(self, field):
interval = datetime.timedelta(days=field.review_frequency)
date = datetime.date.today() + interval
return date
revise
to metoda, która definiuje nowy endpoint dla API napisanego przy użyciu django-rest-framework. Co tu można zauważyć? Funkcja ma tylko kilka linii, ale i tak można ją rozdzielić. Topic.objects.filter(pk=kwargs['pk'])[0]
oraz podobne wyrażenie w kolejnej linii aż kłują w oczy. Wystarczą dwie krótkie funkcje, za którymi ukryjemy warstwę abstrakcji:
def __get_topic(self, pk):
return Topic.objects.filter(pk=pk)[0]
def __get_field(self, pk):
return Field.objects.filter(pk=pk)[0]
Dzięki ich zastosowaniu kod będzie znacznie bardziej czytelny:
@action(methods=['post'], detail=True)
def revise(self, request, *args, **kwargs):
topic = self.__get_topic(kwargs['pk'])
field = self.__get_field(kwargs['field_pk'])
self.set_last_reviewed_today([topic, field])
revision = self.create_revision(field)
data = TopicSerializer(topic).data
return Response(data)
Oczywiście kod ten bez wątpienia wymaga dalszej refaktoryzacji. Również bez wątpienia rozdzielenie go na mniejsze funkcje pomaga w jego zrozumieniu. Zatem o co chodzi Brianowi Willowi?
Don’t be afraid of long functions
W swoim materiale Brian Will posługuje się przykładem podobnym do poniższego:
Umówmy się, że mamy funkcję, która wykonuje pewną spójną sekwencję wyrażeń. Kod ten nie powtarza się nigdzie indziej w całym programie. Sama funkcja ma kilkanaście linijek.
Czy dobrym pomysłem jest rozbicie jej na mniejsze funkcje, na przykład tak?:
def func():
doSomething()
doMoreThings()
andMore()
lastThingToDo()
Co na tym zyskujemy? Spójny kod został rozrzucony na cztery różne funkcje. Myślę, że może to wręcz utrudnić jego zrozumienie. Kod i tak już wcześniej się w programie nie powtarzał. Poza tym, jeśli to spójna sekwencja wyrażeń, to i tak korzystanie z niej zawsze będzie zakładać wywołanie dokładnie tych czterech funkcji w tej samej kolejności.
Świadomie nie wspominam tu o rozdzieleniu warstw abstrakcji, bo też Brian Will w swoim przykładzie o tym nie mówi.
Wnioski
Efekt moich rozmyślań to dość prosty wniosek: rozdzielanie funkcji jest kolejnym (tylko i aż) narzędziem w przyborniku programisty. Jest użyteczne, przydatne i ma ogromne spektrum zastosowań. Jednak myślę, że nie powinno się z niego korzystać automatycznie. Należy to robić świadomie.
Dlatego też tak ważne są inne zasady dotyczące czystego kodu. Pamiętajmy o YAGNI (You Aren’t Gonna Need It). Przecież może się okazać, że z tych małych funkcji nie skorzystamy poza tym jednym przypadkiem. Nie będą nam potrzebne. Za to po napisaniu kodu możemy go rozwinąć w przyszłości.
Jak zawsze będę, zachęcam do pozostawienia feedbacku, zwrócenia uwagi na moje pomyłki i zdrowej, konstruktywnej krytyki. Ten post, tak samo jak cały blog, powstał jako moje narzędzie do nauki, więc na pewno spodziewam się popełniania przeze mnie błędów.