W świecie aplikacji legacy warto zastanowić się nad porządnym refaktoringiem. Jednak często ciężko przekonać biznes do takiego zabiegu. Załóżmy, że nam się ta sztuka udała, więc możemy rozpocząć przepisywanie projektu na boku. Mieliśmy mocne argumenty, które przekonały do tego osoby decyzyjne. Oczywiście jest jedno “ale”. Biznes zezwala na takie działania, ale chce mieć możliwość szybkiego przełączania pomiędzy starym a nowym modelem rozwiązania. Nie chce, aby to wymagało restartowania aplikacji czy też angażowania osób technicznych. Chcą oni w dowolnym momencie dokonać tego sami. Uznajemy ich warunki, ponieważ w głowie kołuje nam się już jedno konkretne rozwiązanie. Trzeba zastosować wzorzec Feature Toggle!

Czym jest wzorzec Feature Toggle?

Wzorzec Feature Toggle to nic innego jak prosty warunek pozwalający przełączać się pomiędzy dwoma rozwiązaniami albo też włączać czy wyłączać nową funkcję biznesową. Jest on banalny w swojej konstrukcji, ale niesie ze sobą wielkie możliwości. Dzięki odpowiedniej konfiguracji w uruchomionej aplikacji możemy po prostu np. klikać guzik w graficznym interfejsie użytkownika, aby zmieniać jej działanie. Najczęściej przedstawia się go w postaci flagi.

Zobrazowanie działania wzorca Feature Toggle
Zobrazowanie działania wzorca Feature Toggle

Dobrą praktyką jest, aby zweryfikować działanie wzorca Feature Toggle nawet jak jest to najprostsza instrukcja warunkowa. Ten prosty warunek ma często niebagatelne znaczenie biznesowe, więc naprawdę warto poświęcić chwilę i napisać dwa krótkie testy jednostkowe.

Po co wymyślać koło na nowo…

Jeśli w tym momencie chciałaś lub chciałeś zabrać się do kodowania tego jakże prostego wzorca to wstrzymaj konie. Nie ma potrzeby, aby wymyślać koło na nowo. Istnieje gotowe rozwiązanie, które znacznie przyspieszy dewelopment Twojej aplikacji wymagającej skorzystania z Feature Toggle. Jest nim biblioteka Togglz. Jej twórcom przyświecała idea, aby właśnie stworzyć generyczne rozwiązanie do włączania i wyłączania nowych funkcji biznesowych w dowolnym projekcie opartym na Javie. I trzeba przyznać, że im się to udało.

Dzięki Togglz jesteśmy w stanie wprowadzać wzorzec Feature Toggle do projektu, ale w bardziej rozbudowanej formie. Tutaj “flaga” ma trzy stany. Może być wyłączona, włączona albo aktywna. Pierwszy przypadek jest jasny, ale czym różni się stan włączony od aktywnego? Jeśli włączymy dany feature to nie do końca oznacza, że jest on aktywny. Możemy sprawić, że nowe rozwiązanie będzie działało tylko pod pewnymi warunkami. Załóżmy, że chcemy uruchomić nową funkcjonalność, ale tylko dla wybranego użytkownika albo danej puli odbiorców np. dla 20% z nich. Nie ma również przeszkód, aby ustawić datę aktywacji danej funkcji. Oznacza to, że jest ona włączona, ale będzie aktywna dopiero np. za dwa tygodnie. Oczywiście takie przypadki można mnożyć w nieskończoność. Po więcej zapraszam na stronę Togglz do zakładki Activation Strategies. Przejdźmy teraz do użycia tej biblioteki w przykładowym projekcie.

Konfiguracja aplikacji

Postaramy się stworzyć prostą aplikację opartą o Spring Boota, która będzie zwracała różne komunikaty w zależności od włączonej flagi. Wchodzimy więc na Spring Initializr, wybieramy moduł Web i generujemy projekt. Następnie do pom.xml dodajemy niezbędne zależności do Togglz.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  <dependencies>
    //...

    <dependency>
      <groupId>org.togglz</groupId>
      <artifactId>togglz-spring-boot-starter</artifactId>
      <version>3.1.2</version>
    </dependency>
    <dependency>
      <groupId>org.togglz</groupId>
      <artifactId>togglz-console</artifactId>
      <version>3.1.2</version>
    </dependency>
  </dependencies>
</project>

Pierwsza zależność jest konieczna jeśli piszemy aplikację w Spring Boot. Oczywiście możemy zaciągnąć każdą paczkę pojedynczo, ale po co skoro możemy się ograniczyć tylko do jednego wpisu w POM. Dzięki tej zależności cała konfiguracja jest praktycznie zrobiona za nas. Natomiast org.togglz.togglz-console pozwala nam na automatyczne wygenerowanie UI do zarządzania feature flagami. Jednak, aby graficzna konsola nam się wyświetlała musimy jeszcze w application.properties dodać następującą linijkę: togglz.console.secured=false. Nie będziemy się zajmować w tym wpisie kwestią bezpieczeństwa, więc po prostu wyłączamy zabezpieczenia. Wygenerowana konsola wyświetli się nam na domyślnym adresie http://localhost:8080/togglz-console i zaraz do niej przejdziemy, ale najpierw…

Definiujemy pierwszy endpoint wraz z przełącznikiem

Musimy stworzyć przykładowy kontroler, który będzie obsługiwał ścieżkę /feature. Wstrzykniemy do niego instancję klasy FeatureManager, dzięki której będziemy w stanie sprawdzić czy dana flaga jest aktywna czy też nie. Jeśli już mówimy o flagach to pora na zdefiniowanie pierwszej z nich. Będzie ona nosiła nazwę FORMAL_GREETING i jeśli ją aktywujemy to zamiast nieformalnego przywitania będziemy generować np. “Dzień dobry, Cezary”. Tylko jak stworzyć taką flagę? Istnieje możliwość zrzucenia tej odpowiedzialności na Springa. Wystarczy, że stworzymy zmienną i zapiszemy jej nazwę w application.properties w konwencji togglz.features.{FEATURE} podając jedną z właściwości np. enabled. Czyli dla naszej flagi jest to togglz.features.FORMAL_GREETING.enabled=true.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package pl.devcezz.togglz;

import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.togglz.core.Feature;
import org.togglz.core.manager.FeatureManager;
import org.togglz.core.util.NamedFeature;

@RestController
@RequestMapping("/feature")
class FeatureToggleController {

  static final Feature FORMAL_GREETING = new NamedFeature("FORMAL_GREETING");

  private final FeatureManager featureManager;

  FeatureToggleController(FeatureManager featureManager) {
    this.featureManager = featureManager;
  }

  //... endpoints

}
togglz.console.secured=false

togglz.features.FORMAL_GREETING.enabled=true

Stwórzmy teraz metodę obsługującą jedno z żądań, która będzie w sobie wykorzystywała naszą nową flagę. Następnie sprawdźmy jej działanie przy użyciu wiersza poleceń, aby później wejść do konsoli Togglz i ją wyłączyć.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package pl.devcezz.togglz;

import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.togglz.core.Feature;
import org.togglz.core.manager.FeatureManager;
import org.togglz.core.util.NamedFeature;

@RestController
@RequestMapping("/feature")
class FeatureToggleController {

  static final Feature FORMAL_GREETING = new NamedFeature("FORMAL_GREETING");

  private final FeatureManager featureManager;

  FeatureToggleController(FeatureManager featureManager) {
    this.featureManager = featureManager;
  }

  @PostMapping("/spring")
  String checkSpringFeature(@RequestBody String name) {
    if (featureManager.isActive(FORMAL_GREETING)) {
      return "Dzień dobry, " + name;
    }
    return "Cześć, " + name;
  }
}

Jeśli teraz zrobimy zapytanie do naszej aplikacji na wystawiony endpoint powinniśmy otrzymać przykładowy komunikat “Dzień dobry, Czarek”. Wchodząc na http://localhost:8080/togglz-console naszym oczom ukaże graficzny interfejs Togglz. Jest on naprawdę prosty i przejrzysty. Wystarczy wybrać przycisk Enabled, aby przełączyć nasza flagę w stan wyłączenia.

Przykład działania z włączoną flagą FORMAL_GREETING
Przykład działania z włączoną flagą FORMAL_GREETING

Wygląd konsoli Togglz
Wygląd konsoli Togglz z włączoną flagą FORMAL_GREETING

Po tym zabiegu ponowne wykonanie zapytania zwróci już odpowiedź “Cześć, Czarek”. Natomiast jeśli nie podoba nam się nazwa FORMAL_GREETING w konsoli to możemy jej nadać naszą własną. Wystarczy w application.properties dodać wpis togglz.features.FORMAL_GREETING.label=Formal greeting, aby napis przy fladze był przyjemniejszy dla oka.

Wyłaczona flaga FORMAL_GREETING z przyjemniejszą nazwą
Wyłaczona flaga FORMAL_GREETING z przyjemniejszą nazwą

Wykorzystanie enuma jako flagi

Klasycznym podejściem do tworzenia flag w Togglz jest stworzenie enuma, który implementuje interfejs Feature. W nim definiujemy kilka opcji, które będą reprezentowały Feature Toggle. Nie musimy już ich zapisywać w application.properties. Jeśli chcemy sprawić, aby dana flaga była domyślnie włączona to używamy adnotacji @EnabledByDefault. Natomiast, aby nadać lepszą nazwę dla flagi w UI wykorzystujemy @Label.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package pl.devcezz.togglz;

import org.togglz.core.Feature;
import org.togglz.core.annotation.EnabledByDefault;
import org.togglz.core.annotation.Label;

enum PoetFeature implements Feature {

  @EnabledByDefault
  @Label("First verse")
  FIRST_VERSE("Nic dwa razy się nie zdarza"),

  @EnabledByDefault
  @Label("Second verse")
  SECOND_VERSE("i nie zdarzy. Z tej przyczyny"),

  @EnabledByDefault
  @Label("Third verse")
  THIRD_VERSE("zrodziliśmy się bez wprawy"),

  @EnabledByDefault
  @Label("Fourth verse")
  FOURTH_VERSE("i pomrzemy bez rutyny.");

  private final String verse;

  PoetFeature(String verse) {
    this.verse = verse;
  }

  String getVerse() {
    return verse;
  }
}

Następnie musimy nauczyć Springa, aby wiedział jakie flagi ma mieć w swoim arsenale. Nadpisujemy, więc autogenerowany bean naszą własną implementacją. Rejestrujemy EnumBasedFeatureProvider jako implementację interfejsu FeatureProvider w dowolnej klasie konfiguracyjnej.

1
2
3
4
@Bean
FeatureProvider featureProvider() {
  return new EnumBasedFeatureProvider(PoetFeature.class);
}

Pozostaje nam stworzenie kolejnego endpointu i wykonanie żądania.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package pl.devcezz.togglz;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.togglz.core.Feature;
import org.togglz.core.manager.FeatureManager;
import org.togglz.core.util.NamedFeature;

@RestController
@RequestMapping("/feature")
class FeatureToggleController {

  static final Feature FORMAL_GREETING = new NamedFeature("FORMAL_GREETING");

  private final FeatureManager featureManager;

  FeatureToggleController(FeatureManager featureManager) {
    this.featureManager = featureManager;
  }

  //... previous endpoint

  @GetMapping("/enum")
  String checkEnumFeature() {
    StringBuilder builder = new StringBuilder();
    if (featureManager.isActive(PoetFeature.FIRST_VERSE)) {
      builder.append(PoetFeature.FIRST_VERSE.getVerse()).append('\n');
    }
    if (featureManager.isActive(PoetFeature.SECOND_VERSE)) {
      builder.append(PoetFeature.SECOND_VERSE.getVerse()).append('\n');
    }
    if (featureManager.isActive(PoetFeature.THIRD_VERSE)) {
      builder.append(PoetFeature.THIRD_VERSE.getVerse()).append('\n');
    }
    if (featureManager.isActive(PoetFeature.FOURTH_VERSE)) {
      builder.append(PoetFeature.FOURTH_VERSE.getVerse()).append('\n');
    }
    return builder.toString();
  }
}

Dostępne cztery flagi z enuma PoetFeature
Dostępne cztery flagi z enuma PoetFeature

Odpowiedź przy włączonych wszystkich czterech flagach PoetFeature
Odpowiedź przy włączonych wszystkich czterech flagach PoetFeature

Oczywiście jeśli wyłączymy wybrane flagi to odpowiedź się jak najbardziej zmieni. Na co należy zwrócić uwagę to fakt, że brakuje w konsoli graficznej wcześniej zdefiniowanej flagi FORMAL_GREETING. To właśnie konsekwencja nadpisania domyślnej konfiguracji Togglz dla Springa. Twórcy przewidzieli taki bieg wydarzeń. Aby to naprawić musimy na początku usunąć nasz bean konfiguracyjny, a następnie w application.properties dodać kolejny wpis - togglz.feature-enums=pl.devcezz.togglz.PoetFeature. Właśnie pod tą właściwością można wypisywać po przecinku kolejne flagi, które zdefiniujemy w postaci enuma.

Widzimy flagi znaleziona przez Springa oraz te zdefiniowane jako enum
Widzimy flagi znaleziona przez Springa oraz te zdefiniowane jako enum

Pamiętaj o teście!

Naprawdę ważne jest, aby napisać test weryfikujący działanie wzorca Feature Toggle. Pomimo prostej logiki ma ona dużą władzę nad działaniem aplikacji. Dlatego nie możemy dopuścić, aby coś nie zadziałało prawidłowo. Załączmy, więc niezbędną bibliotekę testową dla Togglz. W naszym przypadku będzie to wsparcie dla JUnit5.

1
2
3
4
5
6
7
8
9
10
11
  <dependencies>
    //...

    <dependency>
      <groupId>org.togglz</groupId>
      <artifactId>togglz-junit</artifactId>
      <version>3.1.2</version>
      <scope>test</scope>
    </dependency>
  </dependencies>
</project>

Napiszmy teraz test dla flagi FORMAL_GREETING. Musimy na start przygotować testowego menadżera do sterowania jej aktywnością. Dokonamy tego przy pomocy klasy TestFeatureManager.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package pl.devcezz.togglz;

import org.junit.jupiter.api.Test;
import org.togglz.testing.TestFeatureManager;

import static org.junit.jupiter.api.Assertions.assertEquals;

class FeatureToggleControllerTest {

  private final TestFeatureManager featureManager = new TestFeatureManager(FeatureToggleController.FORMAL_GREETING.getClass());

  private final FeatureToggleController controller = new FeatureToggleController(featureManager);

  @Test
  void should_print_formal_greeting() {
    featureManager.enable(FeatureToggleController.FORMAL_GREETING);

    String result = controller.checkSpringFeature("Czarek");

    assertEquals("Dzień dobry, Czarek", result);
  }
}

Jeśli uruchomimy powyższy test dostaniemy wyjątek java.lang.IllegalArgumentException: This feature manager currently only works with feature enums. Wychodzi na to, że w wersji 3.1.2 twórcy jeszcze nie wspierają testowania jednostkowego flag niezdefiniowanych jako enum. Dlatego lepiej korzystać ze standardowego podejścia jakim jest typ wyliczeniowy.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package pl.devcezz.togglz;

import org.junit.jupiter.api.Test;
import org.togglz.testing.TestFeatureManager;

import static org.junit.jupiter.api.Assertions.assertEquals;

class FeatureToggleControllerTest {

  private final TestFeatureManager featureManager = new TestFeatureManager(PoetFeature.class);

  private final FeatureToggleController controller = new FeatureToggleController(featureManager);

  @Test
  void should_print_poem() {
    featureManager.enableAll();

    String result = controller.checkEnumFeature();

    assertEquals("""
        Nic dwa razy się nie zdarza
        i nie zdarzy. Z tej przyczyny
        zrodziliśmy się bez wprawy
        i pomrzemy bez rutyny.
        """, result);
  }

  @Test
  void should_print_poem_without_first_verse() {
    featureManager.enableAll();
    featureManager.disable(PoetFeature.FIRST_VERSE);

    String result = controller.checkEnumFeature();

    assertEquals("""
        i nie zdarzy. Z tej przyczyny
        zrodziliśmy się bez wprawy
        i pomrzemy bez rutyny.
        """, result);
  }
  
  //... rest of tests
}

Dla enuma testy już przechodzą bez najmniejszego ale. W sekcji given musieliśmy włączyć wszystkie flagi pomimo tego, że mają one adnotację @EnabledByDefault. Jest to spowodowane tym, że korzystamy z TestFeatureManager. Można spróbować skorzystać z dostępnych adnotacji @AllEnabled oraz @AllDisabled, ale niestety mi ta sztuka się nie udała. Jeśli wiesz jak to zrobić to podziel się tym w komentarzu.

Podsumowanie

Mam nadzieję, że po tym wpisie znajdziesz zastosowanie dla Togglz w swoim projekcie. Jak wspomniałem wcześniej, jest to prosta biblioteka dająca sporą władzę, którą kocha biznes. Oczywiście warto zwrócić uwagę, że jeśli zrestartujemy aplikację to ustawienia flag powrócą do tych domyślnych. Aby tego uniknąć zapraszam do tej zakładki znajdującej się na stronie Togglz.

Link do GitHub: https://github.com/cezarysanecki/code-from-blog