Gdy słyszałem słowo kontrakt w kontekście programowania, to automatycznie otwierała mi się w mózgu szufladka z Javowym konstruktem jakim jest interfejs (interface). Nieważne teraz czy słusznie czy nie. W wielu źródłach interfejs jest tłumaczony jako pewnego rodzaju kontrakt. I tak zacząłem się zastanawiać… Czy to, że kompilator każe mi zaimplementować metodę zadeklarowaną w interfejsie, to znaczy, że wypełniłem kontrakt? Przecież to są dwa kliknięcia myszką albo skrót klawiszowy i po wszystkim. To byłoby chyba za proste. Słowo kontrakt brzmi super serio.
Załóżmy, że szukam pracy jako handlowiec. Udało mi się już po pierwszej rozmowie kwalifikacyjnej. Kontrakt na miesięczny okres próbny podpisany. Po miesiącu zostanę rozliczony ze swojej pracy. Dostanę pensję podstawową oraz prowizję, której wysokość zależy od liczby sprzedanych produktów.
Pre-conditions
Aby zacząć w ogóle próbę sprzedaży, muszę znaleźć klienta. W miesiącu próbnym moje kroki kieruję zatem wprost do ukochanej Babci. Samodzielne znalezienie klienta jest warunkiem koniecznym do sprzedaży produktu. W języku DbC będzie to mój pre-condition. Czyli warunek, który musi zaistnieć, aby proces sprzedaży mógł się rozpocząć. Bez klienta, nie wyjdę w ogóle z domu, nic nie sprzedam i kontrakt zostanie zerwany. O umowie na czas nieokreślony będę mógł tylko pomarzyć.
Invariants
W czasie trwania okresu próbnego nie mogę podjąć pracy w konkurencyjnej firmie. Jest to niezmiennik, który obowiązuje przez cały okres trwania okresu próbnego. Złamanie tego niezmiennika powoduje, że kontrakt między mną a pracodawcą jest natychmiast zrywany. W języku DbC będzie to tzw. invariant. Aby móc podpisać umowę na czas nieokreślony ten warunek musi być zawsze prawdziwy. Zarówno przed rozpoczęciem procesu sprzedaży, w trakcie, jak i po zakończeniu.
Post-conditions
Po zakończeniu okresu próbnego, zostanie sprawdzony rezultat mojej pracy. Przełożony sprawdzi, czy mój sukces sprzedażowy jest większy bądź równy 1. Będzie to post-condition, czyli warunek, który musi być prawdziwy po udanym przetworzeniu jakiejś funkcji. U nas tą funkcją będzie okres próbny dla potencjalnych pracowników.
Design By Contract
Wróćmy do programowania. Pisząc program chcemy, aby był on jak najbardziej niezawodny i pozbawiony luk. Niezawodny tzn., żeby działał według specyfikacji, przynosił wartość biznesową oraz aby reagował odpowiednio na sytuacje niepożądane. Programy składają się zazwyczaj z kilku modułów, które komunikują się ze sobą. Aby uzyskać niezawodność naszego oprogramowania stosuje się różne techniki, między innymi Design By Contract, który pomoże nam w zrozumieniu i wyspecyfikowaniu i zweryfikowaniu wymagań biznesowych. Fajne zdanie na stronie Eiffela umieścili:
“(…) note that it we don’t state what a module should do, there is little likelihood that it will do it. (The law of excluded miracles.)”
Abstrahując od wad i zalet DbC warto myśleć o kodzie w ujęciu kontraktowym (ktoś tak mądrze powiedział, tylko nie mogę znaleźć źródła, chyba M.Fowler).
Wspomniałem o modułach, które ze się ze sobą komunikują. Weźmy za przykład proces przelewu bankowego, w którym moduł A (client) komunikuje się z modułem B (service provider / supplier).
interface TransferContract {
void transfer(BigDecimal transferAmount, BigDecimal balance, String fromAccountNumber, final String toAccountNumber);
}
Spróbujmy ustalić najpierw pre-conditions. Nieformalny zapis pre-conditions mógłby wyglądać tak:
transferAmount > 0 &&
fromAccountNumber is not empty && fromAccountNumber.length == 26 &&
toAccountNumber is not empty && toAccountNumber.length == 26 &&
balance>=transferAmount
Great. Kto ma zagwarantować prawdziwość warunków wstępnych (pre-conditions)? Klient, provider, obaj? Kto ma zrobić walidację tych danych. To już pewnie zależy od wielu czynników, między innymi od tego czy i jak błędne dane mogą wpłynąć na stan aplikacji, czy i jak jesteśmy w stanie obsłużyć problem, czy i jak to wpłynie na user experience. To wszystko zależy od nas i od kontekstu. Możesz o tym przeczytać tu i tu. Pisze poniekąd o tym również pan Vernon w książce Implementing Domain-Driven Design.
Klient oczekuje, że jego przelew dojdzie do skutku. Konto właściciela konta zostało obciążone. Przelew poszedł. W naszym przypadku, post-condition zdefiniujemy jako:
new balance == (old balance - transfer amount)
Dla przypomnienia, post-condition to warunek, który jest zawsze prawdziwy, gdy funkcja (tu: transfer) wykona się bez przeszkód.
Pozostają nam do zdefiniowania jeszcze niezmienniki invariants. Inwarianty w przeciwieństwie do {pre,post}conditions definiuje się dla całej klasy. W tej klasie może istnieć wiele metod, które będą miały swoje {pre,post}conditions. Jednak wspólne dla nich będzie to, że warunek umieszczony jako invariants musi być zawsze spełniony, abyśmy nie naruszyli stanu naszego obiektu. Naszym inwariantem będzie warunek niezmienności numeru konta odbiorcy, żeby żaden bandzior w kominiarce nam go nie podmienił.
Przykład
Rzuć okiem na githuba, umieściłem tam uproszczoną, nieformalną implementację podejścia / sposobu myślenia DbC.
Tylko…
Ciężko zobrazować coś, co nie jest technicznie by default w pełni wspierane przez język programowania (tu: Java). Możesz użyć zewnętrznych bibliotek/narzędzi takich jak np.: iContract (chyba płatne) lub rozwiązania od Googli, które wspierają definiowanie oraz weryfikację warunków oraz inwariantów w run-time’ie. Możemy też użyć Guavy, która udostępnia API umożliwiające wygodniejsze sprawdzanie wyodrębnionych warunków. Albo zajrzyj po prostu na wikipiedię, o tu.
Oczywiście DbC nie aplikuje się jedynie do interfejsów czy klas abstrakcyjnych. Nie w tym rzecz. Jest to ogólny koncept, który pomaga w wyspecyfikowaniu i weryfikacji reguł, które rządzą zachowaniami również Twoich encji biznesowych (zerknij tu – wykorzystano tu zewnętrzną bibliotekę w celu implementacji DbC w encjach). Swoistą specyfikacją wszelkich zachowań modelu będzie również dobrze napisany kod i stosowanie poprawnego nazewnictwa.
Czy i jak się ma DbC do Liskov Principle, niebawem.
Więcej:
https://archive.eiffel.com/doc/manuals/technology/contract/
https://www.oracle.com/technetwork/articles/javase/javapch06.pdf
https://www.martinfowler.com/bliki/SpecificationByExample.html
https://eiffel.com/values/design-by-contract/