Już dostępny

Program Szkoleniowy Java Developer dostępny 🔥💪 tylko TERAZ za 1299 zł  Sprawdź szczegóły i agendę

Zakres

Monitoring • Apache Kafka • Clean Code Testowanie • Hibernate • Systemy kolejkowe Sprawdź szczegóły i agendę

Zakres

14 modułów  /  ponad 40h nagrań  /  230 lekcji  /  dożywotni dostęp  /  Sprawdź szczegóły i agendę

Jak uzyskać elastyczność mikroserwisów na poziomie komunikacji?

utworzone przez 25 października 2020Java, Mikroserwisy i Integracje

[Szybkie info]: Startujemy z IV edycją Programu Szkoleniowego Java Developera 🚀. To MEGA piguła wiedzy o Java 🔥💪

  • 14 tygodniowy program szkoleniowy online,
  • 230 lekcji w formie video (40 godzin materiału)
  • z dożywotnim dostępem
  • Case Studies, masz dostęp do kodu i obrazów Dockerowych
  • zamknięta grupa mentorzy + uczestnicy i webinary na żywo

W agendzie znajdziesz: Mikroserwisy, Systemy kolejnowe, Apache Kafka, Caching, Hibernate/MyBatis/Spring Data, techniki efektywnych Testów kodu, Clean Code i Maven.

Tylko teraz dołączysz z 50% rabatem to 2699 zł 1299 zł (+VAT). I nigdy już nie będzie taniej. Poniżej dowiesz się więcej:

Zobacz więcej

A teraz przechodzimy do artykułu:

Jedną z korzyści zorientowania architektury na dobrze wydzielone, skupione na wycinku domeny mikrousługi jest ich niezależność, a tym samym zwinność.

W tym wpisie podzielę się swoimi przemyśleniami dotyczącymi tego, jak niepoprawnie skonfigurowane rozwiązania uczestniczące w komunikacji pomiędzy mikroserwisami mogą skutecznie ograniczyć niezależność oraz opowiem o:

  • niezależności zespołów,
  • prawie Conway’a,
  • w jaki sposób niezależność zespołów odzwierciedlić w komunikacji pomiędzy mikroserwisami na przypadkach:
    • wprowadzania nowej funkcjonalności,
    • modyfikacji funkcjonalności,
    • zastępowaniem funkcjonalności.

Niezależność zespołów

Właścicielstwo komponentów oprogramowania (rozwój, utrzymanie) przypisywane jest zespołom, które pracując w zwinnych metodykach, mają pewną niezależność od siebie. Polega ona na posiadaniu swoich własnych priorytetów w postaci backloga wykonywanych zadań i dostarczanie inkrementu pod koniec cyklu pracy.

Przy złożonych projektach wymagających zmian w kilku komponentach, zespoły współpracują ze sobą, aby osiągnąć cel dla organizacji, ale każdy z nich może pracować w swoim tempie. Niewykluczone, że zespół dostarcza funkcjonalności wielu interesariuszom priorytetuzując swoje zadania. Zespół chce wydawać swoje oprogramowanie często, aby zaspokoić potrzeby i uniknąć „Big Bang” release – czyli „wszystko albo nic, w jednym czasie”.

Dlaczego w kontekście mikroserwisów tak mocno podkreślam czynnik ludzki, czyli zespoły? Dlatego, że produkowany software jest wypadkową współpracy poszczególnych zespołów w organizacji odpowiedzialnych za ich obszary.

Prawo Conway’a

Prawo Conwaya (Conway’s law) mówi:

Any organization that designs a system (defined broadly) will produce a design whose structure is a copy of the organization’s communication structure.

Melvin E. Conway

Czyli: Każda organizacja, która projektuje system (zdefiniowany szeroko), stworzy projekt, którego struktura jest kopią struktury komunikacyjnej organizacji.

To socjologiczna obserwacja. Zostawię Ciebie z nią, a odpowiedzi o słuszność szukaj w swojej organizacji i usługach, które powstały. Zatrzymaj się na chwilę, zrób krok w tył i sprawdź, czy u Ciebie też tak to wygląda.

Z moich osobistych obserwacji wynika, że mikrousługi mają tendencję do bycia podobnymi do usług oferowanych przez działy w organizacji (zgodnie z ich obszarem odpowiedzialności). Projekty IT mają swoich fundatorów, którzy oczekują od działów(!) w organizacji stworzenia procesu, które wspierają ich w działaniach. Nic więc dziwnego, że oprogramowanie wygląda tak, jak struktura organizacji i procesy w niej zachodzące.

Po drugie: dobrze zgrane zespoły, które gromadzą potrzebne kompetencje i wiedzę, znacznie łatwiej radzą sobie z rozwiązywaniem problemów wewnątrz swojego zespołu, niż przy współpracy z innymi. To jest zaleta, ale również może być to dysfunkcja. Dysfunkcję mam na myśli, gdy przy bieżącej potrzebie, łatwiej jest zrobić coś samemu tu i teraz, mając wszystko pod kontrolą, niż ustalać coś z innym zespołem wymagania, które mają zadowolić również inne zespoły. To prowadzi do tworzenia „lokalnych optymalizacji” i rozwiązania tego samego problemu w wielu miejscach. Doskonale wpisuje się to w prawo Conway’a – czyli zespoły nie komunikują się ze sobą.

Niezależność mikroserwisów

Jak to się ma do niezależności mikroserwisów? Przez niezależność mam na myśli to, że:

  • Mogą mieć niezależny cykl wydawniczy.
    Gdy jeden zespół zakończy pracę nad funkcjonalnością, usługę można wydać bez konieczności zmiany w komponentach, które z niej korzystają. Staramy się unikać zależności wydawniczych redukując wdrożenia „wszystko albo nic” do zera. API jest zarówno wstecznie kompatybilne, jak i wprzód kompatybilne.
  • Może używać różnych stosów technologicznych, rozwiązując problemy różnych kategorii. Komunikują się przez API w formacie/protokole wspieranym pomiędzy technologiami.
  • Ma jasno określone właścicielstwo – dany zespół opiekuje się usługą.

Psst… Interesujący artykuł?

Jeżeli podoba Ci się ten artykuł i chcesz takich więcej – dołącz do newslettera. Nie ominą Cię materiały tego typu.

.

Przypadki z życia wzięte

W cyklu życia mikrousługi chcę skupić się na trzech przypadkach:

  • wprowadzenie nowych funkcjonalności,
  • modyfikacja istniejącej funkcjonalności,
  • zastępowanie funkcjonalności.

W komunikacji między usługami stoi technologia. Jednym z etapów jest przygotowanie wiadomości w wybranym protokole, np. JSON w komunikacji HTTP.

Zastanów się, jak wykorzystywana w Twoim projekcie technologia pozwala Ci uzyskać niezależność, o której cały czas mówimy.

Jako przykład, użyjmy Object Mappera Jackson, który konwertuje JSON do obiektów Java i odwrotnie. Obiektem niech będzie komenda CreateOrderCommand tworząca nowe zamówienie w systemie. Informacje, które zawiera to:

  • productId (string) – id produktu
  • quantity (int) – ilość całkowita
  • shiping – sposób dostawy (enum: SELF_COLLECT (odbiór osobisty) i SHIPPING (dostarczenie do klienta przewoźnikiem)).

Komendę systemu reprezentuje obiekt:

@Value
@Builder
public class CreateOrderCommand {
    private final String productId;
    private final int quantity;
    private final ShippingType shipping;

    @JsonCreator
    public CreateOrderCommand(@JsonProperty("productId") String productId,
                              @JsonProperty("quantity") int quantity,
                              @JsonProperty("shipping") ShippingType shipping) {
        this.productId = productId;
        this.quantity = quantity;
        this.shipping = shipping;
    }

    public enum ShippingType {
        SELF_COLLECT,
        SHIPPING,
    }
}

Reprezentacja JSON to:

{
  "productId": "12345",
  "quantity": 10,
  "shipping": "SELF_COLLECT"
}

Tworzymy test dla weryfikacji, czy Jackson potrafi obsłużyć ten format:

class JacksonTest {

    private static ObjectMapper MAPPER = new ObjectMapper();

    @Test
    void convertsPayload() throws JsonProcessingException {
        String payload = "{ \"productId\": \"12345\", \"quantity\": 10, \"shipping\": \"SELF_COLLECT\" }";

        CreateOrderCommand convertedPayload = MAPPER.readValue(payload, CreateOrderCommand.class);

        assertThat(convertedPayload)
                .isEqualTo(CreateOrderCommand.builder()
                        .productId("12345")
                        .quantity(10)
                        .shipping(ShippingType.SELF_COLLECT)
                        .build());
    }
}

Wszystko działa poprawnie, mamy stan zero.

Wprowadzenie nowej funkcjonalności

Jako nową funkcjonalność wyobraź sobie wprowadzenie przez klienta dodatkowego pola na kuponu rabatowego, który zostanie uwzględniony przy złożeniu zamówienia oraz zaznaczony jako wykorzystany. Pole nazwiemy coupon (string).

Dajmy na to, że istnieją dwa zespoły realizujące to zadanie. Frontendowcy pracujący z JavaScript’em, opiekujący się stroną sprzedażową oraz Backendowcy, którzy opiekują się systemem obsługującym zamówienia.

Istnieją dwa scenariusze:

  1. Zespół backendowy zaimplementuje zmianę jako pierwszy. Pole nie będzie nigdy wysyłane. Jeżeli pole jest opcjonalne, wtedy jest w porządku.
  2. Zespół frontendowy skończy feature szybciej i zacznie wysyłać pole, o którego istnieniu nie wie backend.

Rozważmy drugi przypadek. Obecnie nasz payload będzie wyglądał:

{
  "productId": "12345",
  "quantity": 10,
  "shipping": "SELF_COLLECT",
  "coupon": "COUPON12345"
}

Przekładam ten format na kolejny test:

    @Test
    void convertsWithUnknownProperty() throws JsonProcessingException {
        String payload = "{ \"productId\": \"12345\", \"quantity\": 10, \"shipping\": \"SELF_COLLECT\", \"coupon\": \"COUPON12345\" }";

        CreateOrderCommand convertedPayload = MAPPER.readValue(payload, CreateOrderCommand.class);

        assertThat(convertedPayload)
                .isEqualTo(CreateOrderCommand.builder()
                        .productId("12345")
                        .quantity(10)
                        .shipping(ShippingType.SELF_COLLECT)
                        .build());
    }

Pojawia się błąd Unrecognized field „coupon”:

Unrecognized field „coupon” (class pl.softwareskill.example.microservices.CreateOrderCommand), not marked as ignorable (3 known properties: „productId”, „quantity”, „shipping”])
at [Source: (String)”{ „productId”: „12345”, „quantity”: 10, „shipping”: „SELF_COLLECT”, „coupon”: „COUPON12345″ }”; line: 1, column: 80] (through reference chain: pl.softwareskill.example.microservices.CreateOrderCommand[„coupon”])
com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException: Unrecognized field „coupon” (class pl.softwareskill.example.microservices.CreateOrderCommand), not marked as ignorable (3 known properties: „productId”, „quantity”, „shipping”])
at [Source: (String)”{ „productId”: „12345”, „quantity”: 10, „shipping”: „SELF_COLLECT”, „coupon”: „COUPON12345″ }”; line: 1, column: 80] (through reference chain: pl.softwareskill.example.microservices.CreateOrderCommand[„coupon”])
at com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException

Zachodzi pytanie – czy dodanie tego pola w komunikacji powinno zatrzymać działania całej funkcjonalności?

Jeżeli klient wprowadzałby jakąś bardzo istotną informację, która by przepadła – może okazać się to kłopotem. Dajmy na to: dodawalibyśmy komentarz, który miałby być uwzględniony przy realizacji zamówienia. Gdybyśmy go zignorowali – mógłby być problem. Natomiast tego typu problemy możemy rozwiązać na poziomie organizacyjnym, a nie technicznym. Na przykład, nie rozdając jeszcze żadnego kuponu, albo ukrywając pole za feature toggle – czyli przełącznikiem (ustawieniem) włączającym funkcjonalność.

Z drugiej strony, jeżeli konsumujemy dane innego serwisu i pojawi się nowe pole – prawdopodobnie nie jesteśmy nim zainteresowani (skoro nie wiemy o jego istnieniu).

Aby uniknąć tego problemu zmieniamy ustawienia Jackson, aby ignorował nowe, nieznane pola adnotując obiekt:

@JsonIgnoreProperties(ignoreUnknown=true)

lub wyłączając globalne ustawienie:

private static ObjectMapper MAPPER = new ObjectMapper();

static {
    MAPPER.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
}

Pierwsze ustawienie jest bardziej granularne, bo dostosowane pod konkretny obiekt (przypadek użycia), a drugie globalne dla całego ObjectMapper’a. Decyzja należy do Waszego zespołu.

Modyfikacja istniejącej funkcjonalności

Wyobraź sobie, że zaczynamy wspierać nowy typ dostarczenia zamówienia: PRIORITY, czyli dostarczenie z priorytetem. Nasz payload modyfikujemy w następujący sposób:

{
  "productId": "12345",
  "quantity": 10,
  "shipping": "PRIORITY"
}

Uruchamiam test:

    @Test
    void convertsWithUnknownEnum() throws JsonProcessingException {
        String payload = "{ \"productId\": \"12345\", \"quantity\": 10, \"shipping\": \"PRIORITY\" }";

        CreateOrderCommand convertedPayload = MAPPER.readValue(payload, CreateOrderCommand.class);

        assertThat(convertedPayload)
                .isEqualTo(CreateOrderCommand.builder()
                        .productId("12345")
                        .quantity(10)
                        .shipping(null)
                        .build());
    }

Test kończy się błędem mówiącym o nierozpoznanym typie wartości wcześniej zdefiniowanego enum’a:

Cannot deserialize value of type pl.softwareskill.example.microservices.CreateOrderCommand$ShippingType from String „PRIORITY”: not one of the values accepted for Enum class: [SHIPPING, SELF_COLLECT]
at [Source: (String)”{ „productId”: „12345”, „quantity”: 10, „shipping”: „PRIORITY” }”; line: 1, column: 53] (through reference chain: pl.softwareskill.example.microservices.CreateOrderCommand[„shipping”])
com.fasterxml.jackson.databind.exc.InvalidFormatException: Cannot deserialize value of type pl.softwareskill.example.microservices.CreateOrderCommand$ShippingType from String „PRIORITY”: not one of the values accepted for Enum class: [SHIPPING, SELF_COLLECT]
at [Source: (String)”{ „productId”: „12345”, „quantity”: 10, „shipping”: „PRIORITY” }”; line: 1, column: 53] (through reference chain: pl.softwareskill.example.microservices.CreateOrderCommand[„shipping”])
at com.fasterxml.jackson.databind.exc.InvalidFormatException

Zachodzi pytanie – czy to poprawne zachowanie?

W tym wypadku, gdyby zgubić wymaganie klienta dotyczące sposobu zamówienia zapewne nie jest dopuszczalne.

Jeżeli jednak istniałby serwis, który konsumuje enuma i dla specyficznych wartości wykonuje logikę, należałoby zadać pytanie: czy wszystkie wartości muszą być obsłużone, czy tylko niektóre – specyficzne? Jeżeli niektóre, wtedy nie mamy możliwości skonsumowania payloadu. Aby uniknąć tego problemu ustawiamy na JsonMapper ustawienie:

MAPPER.enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL);

Ustawienie umożliwia traktowanie nieznanych wartości ENUM jako null. Po stronie aplikacji sprawdzamy, z jaką daną mamy do czynienia, zważając na możliwość otrzymania wartości pustej.

Zastępowanie funkcjonlaności

Innym przypadkiem jest zastępowanie funkcjonalności. Dajmy na to, że system obsługi zamówień czerpał informację na temat ceny produktu z Systemu A, który będzie zastąpiony przez System B.

Istnieją dwa zespoły:

  1. Zespół rozwijający System Zamówień
  2. Zespół wprowadzający nowy system cen produktów: System B

Aby rozpocząć implementację po stronie systemu obsługi zamówień związaną z pobieraniem informacji o cenie, a jednocześnie nie zatrzymywać prac związanych z innymi zakresami tego serwisu, możemy zacząć programować od razu. W konfiguracji serwisu możemy wykorzystać feature toggle, czyli przełącznik, który włączy pobieranie cen z Systemu B, wyłączając pobieranie z systemu A.

Kiedy zespół przygotowujący System B będzie już gotowy, możemy włączyć funkcjonalność. Jeżeli z Systemem B będzie się działo coś niedobrego, możemy z powrotem wrócić na System A.

Daje to pewną niezależność, pomiędzy tempem prac w zespole. Może okazać się, że zespół rozwijający system zamówień skończy pracę wcześniej, ponieważ uzgodnili z drugim zespołem kontrakt na początku prac (więcej o Consumer-Driven Contract).

Wpis który czytasz to zaledwie fragment wiedzy zawartej w Programie szkoleniowym Java Developera od SoftwareSkill. Mamy do przekazania sporo usystematyzowanej wiedzy z zakresu kluczowych kompetencji i umiejętności Java Developera. Program składa się z kilku modułów w cotygodniowych dawkach wiedzy w formie video.

Podsumowanie

To, co chciałem przekazać to, że wykorzystana technologia ma wpływ na niezależność zespołów. Nawet gdy mamy wrażenie, że ta niezależność została osiągnięta za pomocą architektury mikroserwisów, może okazać się, że biblioteki komunikacji skutecznie ją ograniczają.

Sprawdź ustawienia swojej biblioteki. Czy jest to Jackson (JSON), JiBX (XML), Protobuf (binary) – bądź zaznajomiony z tym, jakie są domyślne ustawienia i co można poustawiać, aby być bardziej niezależnym.

Podoba Ci się ten artykuł? Weź więcej.

Jeżeli uważasz ten materiał za wartościowy i chcesz więcej treści tego typu – nie przegap ich i otrzymuj je prosto na swoją skrzynkę. Nawiążmy kontakt.

.

Gdybyś potrzebował jeszcze więcej:

Jesteś Java Developerem?

Przejdź na wyższy poziom wiedzy 
„Droga do Seniora” 🔥💪

Jesteś Team Leaderem? Masz zespół?

Podnieś efektywność i wiedzę swojego zespołu 👌

Obraz: Ludzie plik wektorowy utworzone przez pch.vector – pl.freepik.com

Dyskusja