Systemy informatyczne powstają w odpowiedzi na zmieniające się wymagania biznesowe użytkowników. Każdy z nich jest owocem pracy inaczej myślącego zespołu pracowników. Nierzadko odmienne podejście do jakiegoś zagadnienia, np. poświęcenie elastyczności rozwiązania na rzecz dodatkowej wydajności lub odwrotnie, jest wyróżnikiem danego oprogramowania i stanowi o jego przewadze konkurencyjnej. Z tego też powodu nie można oczekiwać, aby dwa różne systemy odwzorowywały 1:1 to samo pojęcie czy proces biznesowy. Taka sytuacja jest korzystna dla biznesu, ponieważ ma on możliwość wybrania lub rozbudowania używanego systemu wedle swoich potrzeb i nie jest skrępowany żadnymi narzuconymi standardami. Jednak jeśli pojawia się konieczność komunikacji dwóch różnorodnych systemów między sobą, ta zaleta może stać się wyzwaniem. Tym trudniejsze, im różnice w zastosowanych podejściach są większe, i w im bardziej fundamentalnych pojęciach występują.

Wyrażenia wieloznaczne

Dla zobrazowania problemu przyjmijmy, że istnieje konieczność zasilania systemu A danymi klientów z systemu B. Jeśli różnice pomiędzy specyfikacjami tych systemów w obszarze klientów nie występują lub są niewielkie, powinno być to zadanie stosunkowo proste. Przykładowy proces realizujący te założenia mógłby wyglądać następująco:

$apiA = new \PlatformA\Api\ApiClient();
$apiB = new \PlatformB\Api\ApiClient();


$clients = $apiB->getClientList()->getData();
foreach ($clients as $client) {
    $apiA->createClient(
        $client->getFirstName() . ' ' . $client->getLastName(),
        $client->getStreetName() . ' ' . $client->getBuildingNumber()
    );
}

Rozwiązanie podobne do powyższego może sprawdzić się właśnie w prostych przypadkach. Jednak szansa na to, że wystąpi taka szczęśliwa sytuacja jest niewielka. Ponadto oba systemy mogą z czasem ewoluować w różne strony, a wtedy nawet te obszary, które wcześniej były ze sobą zgodne, mogą utracić wzajemną kompatybilność.

Wykorzystanie nieabstrakcyjnego, jednoetapowego mechanizmu synchronizacji wiąże się z szeregiem niedogodności, które objawią się, gdy proces integracyjny będzie trochę bardziej rozbudowany. Przykładowe sytuacje, które nieco komplikują sprawę i wymagają zastosowania dodatkowych etapów podczas migracji to:

  • W systemie B nr. NIP może się powtarzać między różnymi kontrahentami, a w systemie A jest on unikalny dla każdego kontrahenta.
  • Pojawia się konieczność obsłużenia kolejnego źródła danych C, w którym zakres przekazywanych informacji o klientach częściowo pokrywa się z B.
  • Pojęcie klienta z systemu B odpowiada w systemie A pojęciom pracownika oraz firmy, gdzie jedna encja firmy będzie współdzielona między jej pracowników.
  • Klient ma być usuwany z systemu A, jeśli przestanie być zwracany w odpowiedziach z API systemu B.
  • Hierarchia kategorii w systemie A jest opisana przez drzewo, w którym do kategorii przypisany jest identyfikator jej kategorii nadrzędnej, natomiast w systemie B jest opisana przez model zbiorów zagnieżdżonych, w którym do kategorii przypisane są jej wartości lewo/prawo.
  • Integracja jest oparta nie na synchronizacji kolejki zdarzeń, tylko na wymianie obecnego stanu encji, a mimo to zamówienie ma zostać utworzone w systemie A tylko w momencie wystąpienia określonej zmiany stanu zamówienia w systemie B.

Listę podobnych przypadków można mnożyć. Tego typu komplikacje sprawiają trudność zarówno przy mapowaniu samych danych, jak i powiązań między encjami. Kłopotliwe byłoby również wdrożenie takich ulepszeń jak np. zapewnienie możliwości walidacji danych, szczegółowego raportowania błędów czy możliwości powtarzania próby synchronizacji dla wybranych przypadków. Zaprezentowany kod stałby się wtedy bardzo zagmatwany i trudny do utrzymania.

Czy można zatem rozwiązać ten problem kompleksowo, tak aby uniknąć chaosu oraz zbędnego skomplikowania, a jednocześnie zadbać o wydajność i nie ograniczać możliwości przyszłej rozbudowy? Jak najbardziej, a do osiągnięcia pożądanego efektu wystarczą powszechnie znane i sprawdzone podejścia oraz wzorce.

Czego może nauczyć podejście DDD

W uporządkowaniu tego bałaganu może pomóc zastosowanie podejścia DDD (ang. domain-driven design). Przydatne okażą się szczególnie elementy projektowania strategicznego będące częścią tego rozwiązania.

Ważnymi elementami, na których opiera się DDD są m.in. język wszechobecny i kontekst ograniczony. Głównym założeniem opisanym za pomocą tych terminów jest utworzenie pewnego rodzaju słownika zrozumiałego przez wszystkie strony, gdzie do konkretnych nazw podmiotów lub czynności zostanie przypisana precyzyjna definicja, tak aby każde pojęcie było jednoznaczne w danym kontekście. Jeśli chodzi o omawiany przypadek migracji danych, modele pochodzące z dwóch różnych systemów, choćby w obu środowiskach miały identyczne nazwy i zestawy pól, mogą oznaczać zupełnie coś innego, a ich znaczeniem zarządzają odrębne organizacje. Nie można więc ich utożsamiać we wspólnym modelu, ponieważ wprowadziłoby to niejednoznaczności w zastosowanym w projekcie języku. Modele każdego z wykorzystywanych systemów powinny pozostawać oddzielone od siebie w osobnych kontekstach ograniczonych.

Innym przydatnym pojęciem wchodzącym w skład DDD jest mapowanie kontekstów. Skupia się ono na opisaniu relacji, jaka łączy dwa konteksty ograniczone. W zależności od typu aplikacji, kierunku przepływu danych oraz sposobu implementacji procesu migrującego, ta relacja może przybierać różną formę. Często spotykanym typem relacji jest np. relacja typu konformisty (ang. Conformist), gdzie konsument danych ma znikomy wpływ na przebieg komunikacji i musi dopasować się do wymagań stawianych mu przez ich producenta. W przypadku aplikacji webowych wymiana danych najczęściej odpowiada wzorcowi relacji z usługą otwartego hosta (ang. Open Host Service), gdzie rolę udokumentowanej usługi pośredniczącej w komunikacji pełni API.

Jeśli potraktować dwa systemy, między którymi dokonywana jest migracja danych jako dwa konteksty ograniczone, a proces migracji danych jako wzorzec relacji, która je łączy, powstanie mapa kontekstów. W takiej formie będzie ona bardzo przypominać inne dobrze opisane zagadnienie związane z integracją danych, jakim jest wzorzec architektury procesu ETL, co powinno pomóc w implementacji takiego rozwiązania.

Proces integracyjny ETL

Termin ETL (ang. Extract, Transform, Load) opisuje trzystopniowy proces przetwarzania, typowy dla przenoszenia danych pomiędzy różnymi systemami informatycznymi. Jest on szeroko stosowany ze względu na swoją elastyczność i prostotę, a co za tym idzie niezawodność.

  • Pierwszym etapem procesu jest pobranie danych z systemu źródłowego. Wykonywana jest także wstępna walidacja i ewentualny zapis w pośrednich zbiorach danych. Robi się to na potrzeby dalszych etapów przetwarzania. Jeśli w procesie bierze udział kilka źródeł danych, to tutaj dane mogą zostać ujednolicone do wspólnego formatu.
  • Drugim krokiem jest właściwa konwersja danych na potrzeby systemu docelowego. Na tym etapie wykonywane będą czynności takie jak filtrowanie rekordów, łączenie typów danych, mapowanie wartości słownikowych, wyliczanie wartości zależnych i tym podobne.
  • Trzecim etapem jest załadowanie gotowych danych do systemu docelowego. W zależności od wymagań może się to odbywać na różne sposoby, np. w sposób ciągły albo w regularnych interwałach czasowych, czy też w paczkach zgrupowanych ilościowo.

W przypadku omawianym na początku artykułu, podział na etapy mógłby wyglądać następująco:

class PlatformBExtractor
{
    private \PlatformB\Api\ApiClient $api;
    
    public function __construct(\PlatformB\Api\ApiClient $api)
    {
        $this->api = $api;
    }
    
    public function extract(): array
    {
        return $this->api->getClientList()->getData();
    }
}


class PlatformBTransformer
{
    public function transform(\PlatformB\Api\Model\Client $client): \ETL\Model\Client
    {
        return new \ETL\Model\Client(
            $client->getFirstName() . ' ' . $client->getLastName(),
            $client->getStreetName() . ' ' . $client->getBuildingNumber()
        );
    }
}


class PlatformALoader
{
    private \PlatformA\Api\ApiClient $api;


    public function __construct(\PlatformA\Api\ApiClient $api)
    {
        $this->api = $api;
    }


    public function load(\ETL\Model\Client $client): void
    {
        $this->api->createClient(
            $client->getName(),
            $client->getAddress()
        );
    }
}

Choć na tak prostym przykładzie może to wydawać się na pierwszy rzut oka niezbyt oczywiste, usystematyzowanie procesu synchronizacji i podzielenie go na etapy ma wiele zalet, wśród których można wymienić między innymi:

  • Dla synchronizacji w tej postaci poszczególne kroki można wykonywać nie tylko sekwencyjnie iterując encja po encji, ale także wsadowo, wykonując krok po kroku na całej paczce danych. Takie rozwiązanie w wielu wypadkach pozwala na zastosowanie dodatkowych optymalizacji, które w przeciwnym razie nie byłyby możliwe do wdrożenia.
  • Wykonywanie poszczególnych etapów dla różnych encji można przeprowadzać równolegle, co pozwala przyspieszyć całość procesu.
  • Wyznaczenie jasnych granic odpowiedzialności ułatwia tworzenie kolejnych integracji. Kod realizujący dowolny z etapów można rozwijać odrębnie od pozostałych części procesu dla każdego obsługiwanego systemu zewnętrznego, niezależnie, od tego czy ten system jest producentem, czy konsumentem danych. Daje to bardzo dużo swobody podczas implementacji i upraszcza logikę.
  • Każdy z kroków można rozbudować o niezależne formy walidacji, audytu i raportowania, co jest sporą wartością dodaną z punktu widzenia biznesu.

Jak zostało pokazane, wykorzystanie elementów podejścia DDD oraz procesu ETL pomaga zmierzyć się z wyzwaniami, jakie stawia integracja dwóch systemów informatycznych. Połączenie tych dwóch technik dobrze sprawdza się w rozwiązywaniu typowych problemów integracyjnych. Wynikowa architektura cechuje się dużą niezawodnością – jest łatwa w testowaniu i zapewnia elastyczność pod kątem rozbudowy, zarówno jeśli chodzi o dodawanie obsługi nowych systemów i źródeł danych, jak i wzbogacanie procesu synchronizacji o dodatkowe, poboczne funkcje.

Autorem tekstu jest Olaf Kryus