Mateusz Mazurek – programista z pasją

Python, architektura, ciekawostki ze świata IT

Felietony/inne Inżynieria oprogramowania

Zworki funkcyjne

Cześć.

Dzisiaj chciałbym Ci przedstawić zworki funkcyjne.

Zworki to wygodna koncepcja, pozwalająca na łatwe zarządzanie funkcjonalnością naszego systemu. Jeśli zdarzało Ci się komentować lub ukrywać bloki kodu za dziwnym if-em:

1
if (false) {}

To ten artykuł jest dla Ciebie.

Problem

Rozważmy aplikację, która służy do informowania klientów o dostępności produktów w sklepie. W podstawowej wersji powiadomienia wysyłane są tylko za pomocą wiadomości email. W kolejnych sprintach biznes zleca nam dodanie kolejnego rodzaju powiadomienia, czyli wysyłki SMS. Chcielibyśmy mieć możliwość uruchomienia tej funkcjonalności tylko dla wybranych użytkowników (CanaryRelease) lub tylko dla naszego konta testowego. Rozwiązanie musi być ukryte do momentu zakończenia się procesu przygotowywania produktu przez biznes. A czasem po prostu chcemy ukryć funkcjonalność, która nie jest jeszcze gotowa. Oczywiście możemy pójść po linii najmniejszego oporu i zrobić tak:

1
2
$this->sendMailNotification();
//$this->sendSMSNotification();

Trochę lepsze rozwiązanie

Trochę lepszym rozwiązaniem będzie wprowadzenie na “sztywno” w kodzie odpowiedniego warunku. Prosty IF będzie już realizacją koncepcji zworki funkcyjnej.

1
2
3
4
$this->sendMailNotification();
if ($user->getId() === 212) {
    $this->sendSMSNotification();
}

Zanim ogłosimy sukces zastanówmy się przez chwile jakie problemy mogą się pojawić, gdy zastosujemy takie rozwiązanie.

Po pierwsze, taki kod jest kompletnie niezrozumiały dla innych. Kolejna osoba analizująca ten fragment będzie się zastanawiała dlaczego dodano taki warunek. Nie mówi on jasno dlaczego tylko do tego użytkownika wysyłamy SMS. Oczywiście możemy dodać odpowiedni komentarz, ale wtedy… będziemy mieli komentarz. Osobiście uważam, że powinno się unikać komentarzy w kodzie, o ile nie jest to absolutnie konieczne.

Po drugie, jak namierzyć takiego if-a w momencie, gdy będziemy chcieli usunąć warunek (tak wiem, można po komentarzu…).

Kolejny problem pojawia się, gdy będziemy musieli takiego if-a dodać w kilku miejscach. Każda zmiana działania (włączenie/wyłączenie) będzie wymuszała modyfikację każdego warunku. Diametralnie zwiększa to prawdopodobieństwo pomyłki, co ciągnie za sobą trudne do określenia konsekwencje, które w końcu zmuszą nas do żmudnego debuggowania. W przypadku serwerów produkcyjnych/testowych będzie to wymagało deploy’a nowej wersji kodu.

Rozwiązanie

Modelowym rozwiązaniem naszego problemu jest zworka funkcyjna, czyli strategicznie umieszczone rozgałęzienie w przebiegu sterowania naszej aplikacji. Najprostszą implementacją jest najczęściej prosty if, który na podstawie konfiguracji przełączy wykonanie naszego kodu na inną ścieżkę. Zworkę możemy porównać do zwrotnicy na torach kolejowych, która w zależności od tego jak jest ustawiona, pokieruje pociąg na odpowiednie miejsce. Przejdźmy zatem do realizacji naszej zworki.

Wymagania

Zacznijmy od zebrania wymagań. Po kilkugodzinnej dyskusji zespołu z PMem dochodzimy do ustalenia tego, co powinno sterować naszą zworką. Przede wszystkim powinniśmy mieć możliwość włączenia funkcjonalności dla poszczególnych klientów i całkowitego wyłączenia na podstawie konfiguracji.
Dodatkowo podczas rozmowy pojawiły się też kolejne przypadki warunkujące nasza funkcjonalność:

  • użytkownik
  • adres IP

Kierując się zasadą YAGNI przygotujemy implementację tylko dla tych przypadków, które są nam aktualnie potrzebne.

Implementacja

Implementacje zaczniemy od przygotowania kontraktu na podstawie wymagań zebranych przed chwilą. Da nam to pewność, że nasza klasa realizuje dokładnie te wymagania, które przed chwilą zdefiniowaliśmy. Wykorzystamy do tego TDD. Każdy test zdefiniuje nam konkretną ścieżkę biznesową.
Połączenie TDD i wspomnianej wcześniej YAGNI da nam też stuprocentowe pokrycie kodu testami. Nazwy testów opisują nasze zastosowanie biznesowe.

1
2
3
4
5
function featureIsActiveCheckByCustomerTest(){};
function featureIsNotActiveCheckByCustomerTest(){};
function featureIsActiveCheckByConfigTest(){};
function featureIsNotActiveCheckByConfigTest(){};
function featureNotExistTest(){};

Gdy ktoś będzie chciał rozszerzyć funkcjonalności naszej klasy, wystarczy że przygotuje odpowiedni test i dopisze implementacje.

Teraz możemy przygotować prostą implementację:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class FeatureManager
{
   public function isActive(string $featureName): bool
   {
       if ($this->checkGlobalConfig($featureName) || $this->checkUserConfig($featureName)) {
           return true;
       }
       return false;
   }
   protected function checkGlobalConfig(string $featureName): bool
   {
       return true;
   }
   protected function checkUserConfig(string $featureName): bool
   {
       return true;
   }
}

A następnie zmodyfikować IF-a o wykorzystanie managera.

1
2
3
if ($featureMenager->isActive('SMSNotification')) {
   $this->sendSMSNotification();
}

Nasza zworka jest gotowa. Możemy już łatwo sterować aktywnymi funkcjonalnościami.

Wady

Niestety zworki nie są cudownym rozwiązaniem i mają też swoje wady, o których musimy pamiętać.
Pierwszym problemem jest wprowadzenie dodatkowej złożoności do logiki biznesowej. Dodanie kilku zworek, a co gorsze zagnieżdżenie ich, może doprowadzić do sytuacji, w której będziemy musieli przetestować wiele ścieżek przejścia. Niestety nigdy nie możemy mieć pewność, że nasz kod nie wpływa na inne części systemu.

Kolejną rzeczą, na którą musimy zwrócić uwagę jest kompatybilność danych. Czy po wyłączeniu zworki klient będzie mógł dalej pracować? Niestety w niektórych miejscach nie będziemy mogli użyć zworki lub będzie to wymagało dopisania mappera, tak, żeby dane miały odpowiednią postać.

Zakończenie

Jak widać zworki funkcyjne ułatwiają nam sterowanie działaniem konkretnych funkcjonalności. Pomimo kilku minusów, o których musimy pamiętać, jest to proste i skuteczne rozwiązanie. Mam nadzieję, że z jego wykorzystaniem już nigdy nie będziecie musieli komentować kodu, by wyłączyć jakiś jego fragment.    

Autorem wpisu jest Łukasz Krawczyk, programista z kilkuletnim doświadczeniem, specjalizujący się w integracji i optymalizacji systemów informatycznych. A nade wszystko – czujny obserwator procesu dostarczania oprogramowania.

Dzięki za wizytę,
Mateusz Mazurek

A może wolisz nowości na mail?

Subskrybuj
Powiadom o
guest

Witryna wykorzystuje Akismet, aby ograniczyć spam. Dowiedz się więcej jak przetwarzane są dane komentarzy.

3 komentarzy
Inline Feedbacks
View all comments
Marek

Cześć, przekaż Łukaszowi podziękowania za fajny artykuł :)

Artur

GE-NIAL-NE!

Nie słyszałem o tym koncepcie, a po przeczytaniu artykułu wygląda to na oczywiste!
Analogia do zwrotnicy też jest tutaj wybitna!

Z YANIGI i TDD dopiero się zaznajamiam ale to połączenie tutaj wygląda genialnie i na autouzupełniające się!

Więć powtórzę: GE-NIAL-NE!

Mateusz M.

Cieszę się, że się podoba! :D