Feature toggles w teorii i praktyce
W podejściu continuous delivery powinniśmy dostarczać kod w krótkich cyklach, a każdy commit powinien zakończyć się wdrożeniem kodu na serwer produkcyjny. Jednak jak to osiągnąć? Z pomocą przychodzą nam feature toggles nazywane inaczej feature flags.
Wyobraź sobie taką sytuację. Pracujesz nad aplikacją, która łączy się z zewnętrznym API. Dostawca tego API informuje, że za kilka miesięcy zostanie wyłączona wersja, z której korzystasz, ale można już nawiązać połączenie z nową wersją API. Co mógłbyś w takiej sytuacji zrobić? Rozwiązaniem, które jako pierwsze pojawia się w myślach (przynajmniej u mnie) to po prostu pozbycie się lub dostosowanie kodu. To rozwiązanie ma jedną, ale istotną wadę. Jak coś będzie nie tak to naprawa błędu zajmie trochę czasu. Ten problem pomogą rozwiązać feature toggles.
Czym są feature toggles?
Załóżmy, że posiadamy w systemie metodę, która odpowiada za połączenie z API, o którym wspomniałem we wstępie do tego wpisu. Wygląda ona w uproszczeniu tak:
function apiSaveOrder() { // implementacja }
Implementacją w tym przypadku mogłoby być utworzenie obiektu, który zostanie przesłany do API.
Feature toggle są swego rodzaju przełącznikami. Jeżeli chcielibyśmy utworzyć feature toggle dla powyższej metody to w najprostszej formie wyglądałby tak:
function apiSaveOrder() { const useNewAPI = false; if (useNewAPI) { return apiNewSaveOrder(); } else { return apiOldSaveOrder(); } }
W tym przypadku opakowaliśmy metodę tak, aby wywołać odpowiednią akcję w zależności od wartości stałej (bądź flagi) useNewAPI. Zastosowanie takiej sztuczki pozwoli nam na przełączenie się z powrotem na starą wersję API zmieniając wartość flagi. Teraz gdy okaże się, że popełniliśmy błąd w implementacji nowego API, możemy szybko powrócić do działającej wersji i spokojnie poprawić kod.
Zarządzanie stanem przełączników
Kluczową kwestią będzie miejsce przechowywania stanu feature toggles. W przykładzie była to stała w kodzie. Bardzo proste, ale raczej ciężko w ten sposób tym zarządzać. Lepszym pomysłem będzie przechowywanie informacji o stanie przełącznika w bazie danych, konfiguracji aplikacji, zmiennej środowiskowej. Zarządzania stanem flag może odbywać się również przy pomocy zewnętrzny narzędzi. Oto niektóre z nich:
Dostępne są również biblioteki, które pomogą w zarządzaniu feature toggles w różnych technologiach:
- Python:
- JavaScript:
- C#
Kategorie przełączników
Feature toggle można podzielić ze względu na funkcje, jakie spełniają oraz ich własności. Wyróżniamy cztery kategorie przełączników.
Release toggles
Są to feature toggles wykorzystywane w Continuous Delivery. Pozwalają one wdrażać kod, który nie został jeszcze ukończony. Możesz zastanawiać się, po co to robić – już wyjaśniam.
W podejściu Continuous Delivery powinno zależeć nam na jak najczęstszym dostarczaniu kodu na serwer produkcyjny. Idealnie byłoby, gdyby każdy programista pracował nad swoją funkcjonalnością, nie psując przy okazji kodu nad którym pracują inne osoby (mam tu na myśli konflikty). Jeżeli udałoby się nam pracę ułożyć w taki sposób to każdy mógłby posyłać swój kod na mastera, a ten automatycznie lądowałby na naszej produkcji. W jaki sposób skonfigurować Gitlaba aby tak się stało możecie przeczytać w innym naszym wpisie: https://ermlab.com/blog/technicznie/konfiguracja-gitlab-ci-python-django.
Może wydawać się to z początku nieco dziwne. Przecież zawsze coś może pójść nie tak i produkcja się wyłoży – no tak, ale przecież piszemy testy, aby do tego nie doszło. Podejście to obecnie jest dość powszechne. Amazon wypracował ten proces do takiego poziomu, że już w 2011 roku był w stanie dostarczać nową wersję aplikacji co 11.6s
Release toggles cechują się krótkim czasem życia, a ich przełączanie następuje wraz z wdrożeniem. Ile wynosi krótki czas życia? Dokładnie tyle ile czasu minie od wdrożenia do wdrożenia.
Experiment toggles
Kolejnym zastosowaniem feature toggles są testy A/B. Przykładowo może wyglądać to tak:
Mamy grono użytkowników. Postanowiliśmy zmienić układ menu w naszej aplikacji tak, aby (według nas) wygodniej się po nim poruszać. Tylko jak ta zmiana wpłynie na użytkowników?
W takim przypadku wybieramy grupę użytkowników, na których przetestujemy nowy układ menu. Mogą to być na przykład osoby, które wyraziły chęć do korzystania z najświeższej wersji aplikacji (tzw. insidersi) lub po prostu użytkownicy wybierani w sposób losowy. Taki przełącznik powinien żyć tyle czasu ile potrzebujemy aby wyciągnąć rezultaty z eksperymentu. Zmiana ich stanu następuje z każdym zapytaniem do serwera. Wyjaśnię to w części wpisu dotyczącej wzorców projektowych.
Permission toggles
Są to przełączniki bardzo podobne do experiment toggles. Oba z nich służą do rozszerzenia dostępu do funkcjonalności aplikacji dla pewnej grupy użytkowników. Permission toggles są wykorzystywane do zarządzania uprawnieniami użytkowników. Często są stosowane do zarządzania funkcjonalnościami premium i wczesnego dostępu. W tym przypadku grupa użytkowników, dla której są stosowane te przełączniki jest znana.
Permission toggles charakteryzują się długim czasem życia. Wynosi on średnio kilka lat, ale bardzo często taki przełącznik pozostaje w systemie tyle czasu ile żyje sam system. Zmiana stanu flagi następuje, tak jak w przypadku experiment toggles, przy każdym zapytaniu do serwera.
Ops toggles
Przeznaczone głównie dla osób zarządzających aplikacją. Mogą służyć między innymi do wyłączania pewnych funkcji, które nie są niezbędne do prawidłowego funkcjonowania aplikacji, podczas dużego obciążenia serwera. Dla przykładu:
Zarządzamy aplikacją e-commerce. Posiada ona funkcjonalność, która pokazuje użytkownikowi w czasie rzeczywistym stany magazynowe produktów na liście. Tak się składa, że z racji na częste odpytywanie serwera o stan magazynowy dla danego produktu potrzebuje ona dodatkowych zasobów sprzętowych. Nadchodzi ciężki dzień dla wielu systemów e-commerce – Black friday. Klienci zaczynają atakować nasz sklep, a ilość dostępnych zasobów powoli się wyczerpuje. W takim przypadku można tymczasowo wyłączyć funkcjonalność, o której wspominałem. Nie wpłynie to na prawidłowe funkcjonowanie aplikacji – użytkownicy nadal będą mogli bez problemu dokonać zakupu. Dzięki takiemu zabiegowi użytkownicy nie doświadczą opóźnień w działaniu aplikacji pomimo zwiększonego ruchu.
Ops toggles charakteryzują się długim czasem życia. Zmiana stanu takiego przełącznika jest wykonywana zazwyczaj przez człowieka w trakcie działania aplikacji.
Kategorie przełączników – podsumowanie
Wymienione kategorie przełączników zostały oznaczone na grafice poniżej. Rozmieszono je na podstawie czasu życia oraz dynamiki zmian przełączników.

Źródło https://martinfowler.com/articles/feature-toggles.html
Wzorce projektowe
Do tej pory pracowaliśmy na prostych przykładach. Posiadają one jednak dużą wadę – aby zmienić stan przełącznika musimy wykonać kolejne wdrożenie. Zajmie więc to trochę czasu, a poza tym nie widzimy (na pierwszy rzut oka), które z przełączników są aktywne. Na nasze szczęście powstało kilka koncepcji jak feature toggle powinno się implementować. W tej sekcji zaprezentuję najpopularniejsze rozwiązania.
Stała w kodzie
Jest to rozwiązanie, które już się pojawiło we wpisie. Jego implementacja wygląda następująco:
function apiSaveOrder() { const useNewAPI = false; if (useNewAPI) { return apiNewSaveOrder(); } else { return apiOldSaveOrder(); } }
Nie będziemy go po raz kolejny omawiać.
Toggle Router
Kolejnym prostym w implementacji rozwiązaniem jest wykorzystanie prostego Toggle Routera. Toggle Router jest miejscem decyzyjnym w kodzie. Zatem logika decydująca o tym, czy dany przełącznik ma być aktywowany, zostaje wyodrębniona z funkcji/metody, która z przełącznika korzysta.
Sprawdzenie, czy dany przełącznik jest włączony wygląda podobnie jak w poprzednim przykładzie. W naszym kodzie znajduje się instrukcja warunkowa sprawdzająca wartość zwracaną przez metodę featureIsEnabled
var toggleRouter = createToogleRouter(sampleFeatureConfig); function myFunc(){ if(toggleRouter.featureIsEnabled("use-new-api") { return myNewFunc(); }else{ return myOldFunc(); } }
Implementacja Toggle Router’a nie należy do skomplikowanych. Tworzymy funkcję createToggleRouter przyjmującą jako parametr aktualną konfigurację zapisaną np. w formie słownika. Funkcja ta zwracać będzie dwie funkcję: jedną do ustawiania przełączników, a drugą do sprawdzania ich stanu. Jeżeli nie znasz ES6 poniższy kod może być ciężki do zrozumienia.
function createToggleRouter(featureConfig){ return { setFeature(featureName,isEnabled){ featureConfig[featureName] = isEnabled; }, featureIsEnabled(featureName){ return featureConfig[featureName]; } }; }
Oddzielenie logiki od punktów decyzyjnych
Najczęstszym błędem w implementacji przełączników jest wymieszanie logiki z punktem decyzyjnym. Problem ten występuje we wszystkich przykładach, które pojawiły się do tej pory. Jak na razie stan przełącznika był ustalany na podstawie wartości zapisanej w stałej/słowniku. Feature toggle jednak często opierają się na bardziej rozbudowanej logice. Jeżeli nie umieścimy jej w osobnym miejscu to w pewnym momencie zauważymy, że napisaliśmy piękny spaghetti code :). Ten wzorzec projektowy pozwoli na oddzielenie decyzji czy przełącznik ma być aktywny od logiki. Dla przykładów, które teraz się pojawią zmienimy przypadek użycia na trochę bardziej skomplikowany. Załóżmy, że nowe API może zwrócić odnośnik do anulowania zamówienia. Do tej pory wysyłaliśmy wiadomość e-mail do klienta, który utworzył zamówienie. Wiadomość ta zawierała fakturę do zamówienia. Teraz chcemy dodać do wiadomości odnośnik do anulowania zamówienia.
const features = fetchFeatureTogglesFromSomewhere(); function generateInvoiceEmail(){ const baseEmail = buildEmailForInvoice(this.invoice); if (features.isEnabled("next-gen-ecomm") ){ return addOrderCancellationContentToEmail(baseEmail); } else { return baseEmail; } }
Tak wyglądałby kod, który nie jest napisany zgodnie z tym wzorcem. Wystarczy wykonać małą zmianę, aby kod stał się bardziej utrzymywalny.
const features = fetchFeatureTogglesFromSomewhere(); const featureDecisions = createFeatureDecisions(features); function generateInvoiceEmail(){ const baseEmail = buildEmailForInvoice(this.invoice); if (featureDecisions.includeOrderCancellationInEmail()){ return addOrderCancellationContentToEmail(baseEmail); } else { return baseEmail; } }
Zmieniła się linia, w której znajduje się instrukcja warunkowa. Teraz nie sprawdzamy wartości zapisanej w słowniku (na sztywno). Właściwie w tym miejscu nie widzimy, kiedy przełącznik jest aktywny. Stan flagi zwracany jest przez metodę featureDecisons.includeOrderCancellationInEmail(). Metoda ta w tym momencie jest dla nas czarną skrzynką – nie wiemy co się w niej znajduje ale wiemy jakie wartości powinna zwracać. Mogłaby ona wyglądać następująco:
function createFeatureDecisions(features){ return { includeOrderCancellationInEmail(){ return features.isEnabled("next-gen-ecomm"); } // ... additional decision functions also live here ... }; }
Odwrócenie decyzji
W przypadku feature toggles trudno jest stworzyć dobrze testowalny kod. Zastanówmy się zatem jak przetestować kod z poprzedniego przykładu. Nasz test powinien sprawdzić, czy wiadomość e-mail zawiera odnośnik do anulowania zamówienia, gdy przełącznik jest włączony i nie zawiera odnośnika, gdy przełącznik jest wyłączony. No dobrze toooo …. No właśnie – co? Może mockujemy fetchFeatureTogglesFromSomewhere()? Trochę z tym zachodu i może będzie działać. Tylko co w kolejnym kroku? Trzeba by uwzględnić całą logikę, która mieści się w createFeatureDecisions, a co za tym idzie nasze testy będą bardzo skomplikowane.
Problem ten rozwiązuje kolejny ze wzorców projektowych.
function createInvoiceEmailler(config){ return { generateInvoiceEmail(){ const baseEmail = buildEmailForInvoice(this.invoice); if( config.includeOrderCancellationInEmail ){ return addOrderCancellationContentToEmail(email); }else{ return baseEmail; } }, // ... other invoice emailler methods ... }; }
W tym przypadku do createInvoiceEmailler przekazujemy jakiś config. Jak on może wyglądać? A no na przykład tak:
{ includeOrderCancellationContentInEmail: true, useNewApi: true, useSomeDepracatedMethod: false, }
Voilà! Oto wybawienie w testach. Teraz możemy zmieniać tylko nasz config. Nie interesuje nas podczas testowania cała logika odpowiadająca za ustawianie przełącznika. Warto jednak zaprezentować co tam może się dziać:
const features = fetchFeatureTogglesFromSomewhere(); const featureDecisions = createFeatureDecisions(features); function createFeatureAwareFactoryBasedOn(featureDecisions){ return { invoiceEmailler(){ return createInvoiceEmailler({ includeOrderCancellationInEmail: featureDecisions.includeOrderCancellationInEmail() }); }, // ... other factory methods ... }; }
Jak widzisz dalej wykorzystujemy featuresDecisions z poprzedniego wzorca. W linii 7 wywołujemy metodę zajmującą się generowaniem wiadomości e-mail przekazując jej jako parametr określoną konfigurację.
Unikanie instrukcji warunkowych
Nasz kod można jeszcze bardziej poprawić pozbywając się z niego większości instrukcji warunkowych.
function createInvoiceEmailler(additionalContentEnhancer){ return { generateInvoiceEmail(){ const baseEmail = buildEmailForInvoice(this.invoice); return additionalContentEnhancer(baseEmail); }, // ... other invoice emailler methods ... }; }
Ten kod już wygląda prościej. Skoro wygląda prościej to łatwiej go też zrozumieć. Zatem teraz do createInvoiceEmailler przekazujemy jako parametr coś, co według nazwy zajmuje się rozszerzaniem naszej wiadomości e-mail. createInvoiceEmailler zwraca za to metodę zajmującą się generowaniem wiadomości e-mail, która tworzy podstawową wiadomość e-mail i zwraca wynik metody additionalContentEnhancer z wykorzystaniem baseEmail. Niby proste, ale dlaczego zawsze dodajemy link do anulowania zamówienia do wiadomości? Właśnie, że nie zawsze. Nazwa parametru nazwą, a to, co jest w środku to już inna sprawa. Przyjrzyjmy się więc czym jest additionalContentEnhancer:
function identityFn(x){ return x; } function createFeatureAwareFactoryBasedOn(featureDecisions){ return { invoiceEmailler(){ if (featureDecisions.includeOrderCancellationInEmail()){ return createInvoiceEmailler(addOrderCancellationContentToEmail); } else { return createInvoiceEmailler(identityFn); } }, // ... other factory methods ... }; }
Z powyższego kodu mozna wywnioskować, że przekazywany parametr jest metodą addOrderCancellationContentToEmail lub identityFn. Pierwsza z tych metod zajmuje się dodaniem odnośnika do anulowania zamówienia. Druga z nich natomiast to metoda, która zwraca to, co otrzymała w parametrze. Zatem jeżeli przełącznik będzie w stanie nieaktywny to do wiadomości nie zostanie nic dopisane, gdyż identityFn(baseEmail) zwróci dokładnie baseEmail.
To był już ostatni ze wzorców projektowych stosowanych przy feature toggles.
Wady i zalety
Pozostało już tylko podsumować wiedzę na temat feature toggles. W tej części omówię wady i zalety przełączników.
Wady
Wady związane są głównie z utrzymaniem kodu
- Przyczyna długu technologicznego – Jeżeli korzystamy z feature toggles to powinniśmy poświęcać trochę czasu aby nie pogrążyć się w długu technologicznym. Powstanie on, gdy nie będziemy pozbywać się fragmentów kodu, które już nigdy nie zostaną użyte. Odwołam się tutaj ponownie do przykładu z nowym i starym API. Gdy finalnie przełączymy się na korzystanie z nowej wersji API to kodu odpowiadającego za starą wersję należy się pozbyć.
- Mniej testowalny kod – Jak już zauważyłeś/aś trzeba trochę się pogłowić, aby testować funkcjonalności, które korzystają z feature toggles. Każdy przełącznik wymusza na nas napisanie testu zarówno dla stanu aktywnego, jak i nieaktywnego. Każdy przełącznik będzie zatem rozdzielał główną ścieżkę testów. Problem w testowaniu jest jeszcze bardziej odczuwalny, gdy przełączniki zaczynają mieć między sobą zależności. Zauważ, że w naszych przykładach metoda dodająca odnośnik do anulowania przesyłki działa tylko z nowym API (stare API nie umożliwiało anulowania). Ostatecznie nasze testy można zobrazować w następujący sposób:
Źródło https://martinfowler.com/articles/feature-toggles.html
Zalety
- Możliwość testowania na produkcji – Dzięki przełącznikom możemy szybko zmienić zachowanie aplikacji. Dzięki temu możemy wykorzystać je do sprawdzenia, czy nowa wersja funkcjonalności działa lepiej od poprzedniej, czy może użytkownikom lepiej się z niej korzysta, jest bardziej intuicyjna, optymalizuje działanie aplikacji itd.
- Pozwala na Continuous Delivery – Ustawiając przełącznik możemy sprawić, że kod nie będzie wykonany, dopóki na to nie pozwolimy. W ten sposób możemy pisać nową funkcjonalność nie dając możliwości uruchomienia jej dopóki nie będzie gotowa. Co za tym idzie – możemy wdrażać takie zmiany na serwer produkcyjny.
- Można powrócić do wcześniejszej wersji funkcjonalności w razie problemów z nową – Przełączniki pozwalają nam na szybką zmianę zachowania aplikacji. Gdy okaże się, że z nową funkcjonalnością coś jest nie tak możemy bardzo szybko wrócić do korzystania ze sprawdzonego rozwiązania.
- Umożliwiają implementację funkcji premium – Stan przełączników możemy ustalać na podstawie dowolnie wybranych przez nas kryteriów. Może to posłużyć do włączenia funkcji premium dla określonych użytkowników.
- Pomagają w radzeniu sobie z sytuacjami awaryjnymi – Ten punkt można powiązać z punktem 3. Rozwinę go tylko o Ops Toggles, które pozwalają nam np. na wyłączenie funkcjonalności, która wymaga dużo zasobów, w sytuacjach krytycznych (np. wielu aktywnych użytkowników jednocześnie).
Podsumowanie
W ten sposób zakończyliśmy wywód na temat feature toggles. Omówiliśmy czym są przełączniki, jakie są typy przełączników i do czego można je wykorzystać oraz jak je implementować. Jest to potężne narzędzie pomimo średniej trudności implementacji. Flagi można znaleźć w każdej większej aplikacji. Zazwyczaj korzysta się z nich w małym stopniu, ale jak widzisz można na nich oprzeć całą aplikację.