BLOG

Cocoders flow - specyfikacja i projektowanie domeny poprzez przykłady


Chcielibyśmy podzielić się sposobem, którego używamy do projektowania i modelowania domeny biznesowej. Na początku zawsze staramy się zrozumieć potrzeby i cele biznesowe naszych klientów oraz zrozumieć w jaki sposób oprogramowanie które mamy dla nich stworzyć ma pomóc w ich realizacji. Dlatego bardzo pomocne tutaj okazują się wszystkie techniki które pomagają w komunikacji, czyli na przykład dostosowują język który używamy w kodzie do języka klienta. Przydatne są też techniki, gdzie w graficzny sposób możemy coś pokazać klientowi, na przykład zaplanować kto ma jaki wpływ na realizację celów biznesowych, poprzez konkretne funkcje w systemie (jest to tzw. Impact Mapping)

Nasza przykładowa aplikacja

Załóżmy, że tworzymy aplikację dla klienta który związany jest świadczeniem usług medycznych. Powiedzmy, że po kilku rozmowach i spotkaniach z klientem stworzyliśmy rejestr wymagań (backlog). Doszliśmy również do wniosku, że pierwszym bardzo istotnym zadaniem które mamy zrealizować będzie następująca historyjka:

Jako recepcjonista w przychodni,
Chciałbym umówić pacjenta na wizytę bez martwienia się czy jest on ubezpieczony, dlatego
powinienem być w stanie zobaczyć lub sprawdzić taką informacje w systemie zamiast narażać
przychodnię na stratę poprzez przyjęcie nieubezpieczonego pacjenta

Dodatkowe informacje:
- Pacjent który nie jest ubezpieczony nie może się leczyć w przychodni (poza nagłymi przypadkami)
- Przychodnia ma nadane konto w systemie eWuŚ

Rozwijanie znaczenia zadań poprzez ilustrowanie ich przykładami

Aby zilustrować naszą historyjkę przykładami używamy formatu gherkin:

    # language: pl

    Potrzeba biznesowa: Jako recepcjonista w przychodni,
      Chciałbym umówić pacjenta na wizytę bez martwienia się czy jest on ubezpieczony, dlatego
      powinienem być w stanie zobaczyć lub sprawdzić taką informacje w systemie zamiast narażać
      przychodnię na stratę poprzez przyjęcie nieubezpieczonego pacjenta

      Dodatkowe informacje:
      - Pacjent który nie jest ubezpieczony nie może się leczyć w przychodni (poza nagłymi przypadkami)
      - Przychodnia ma nadane konto w systemie eWuŚ

      Scenariusz: Informacja o tym, że pacjent nie jest ubezpieczony
        Zakładając że jestem recepcjonistą w przychodni
        I pacjent któremu rezerwuje wizytę nie ma ubezpieczenia
        Kiedy znajdę teczkę tego pacjenta w systemie
        Wtedy powinienem zobaczyć że dany pacjent nie może być umówiony na wizytę ponieważ nie jest ubezpieczony

      Scenariusz: Informacja o tym że pacjent jest ubezpieczony i może być przyjęty na wizytę lekarską
        Zakładając że jestem recepcjonistą w przychodni
        I pacjent któremu rezerwuje wizytę jest ubezpieczony
        Kiedy znajdę teczkę tego pacjenta w systemie
        Wtedy powinienem zobaczyć że dany pacjent może być umówiony na wizytę

Jak możecie zauważyć, dodaliśmy scenariusze do naszej historyjki. Scenariusze te są przykładami które dodają kontekst. Mogą też one być dobrą podstawą do dodatkowej dyskusji z klientem, ponieważ są czytelne i napisane w języku zrozumiałym dla klienta. W naszym wypadku wprowadzenie w kodzie i scenariuszach języka który jest zrozumiały dla klienta i nie jest też zbyt techniczny, bardzo polepszyło wzajemne zrozumienie oraz komunikacje.

Modelowanie domeny biznesowej poprzez przykłady

Jest to naprawdę wartościowa technika którą używamy już od jakiegoś czasu. Ogólnie polega to na tym, że możemy używać narzędzi typu behat do modelowania naszej domeny bazując na przykładach napisanych w języku klienta. Zaczynamy po prostu od instalacji behata po czym możemy go uruchomić poprzez polecenie vendor/bin/behat –append-snippets które wygeneruje nam strukturę oraz szkielet kodu, który będzie wykonywany dla każdego kroku naszego scenariusza. Zaczniemy od kroku “Zakładając że jestem recepcjonistą w przychodni”.

Widzimy tutaj, że w systemie będą nam potrzebne dwa koncepty. Koncept Przychodni oraz koncept Recepcjonisty który pracuje w tej przychodni. W czasie modelowania musimy pamiętać też o całości.W tej chwili widzimy, że będzie potrzeby koncept Recepcjonisty, jednak w czasie dyskusji z klientem dowiemy się, że wszyscy pracownicy przychodni będą używać naszego systemu, dlatego potrzebny jest bardziej abstrakcyjny koncept Pracownika Przychodni.

No dobra, spróbujemy wymodelować koncept Przychodni. Chcielibyśmy stworzyć obiekt przychodni w naszym systemie w taki sposób $clinic = new Clinic(). Tutaj powinniśmy się jednak wstrzymać i pomyśleć. Czy naprawdę możemy założyć przychodnię bez żadnych danych? Znowu musimy wrócić do klienta i co jest całkiem częste, nawet klient może nie znać odpowiedzi na nasze pytania. Jednak człowiek który zarządza jego placówką już może znać odpowiedzi. Ten człowiek jest to tak zwany ekspert domenowy.

Dobra, powiedzmy, że ekspert domenowy udzielił nam odpowiedzi. Do stworzenia przychodni zawsze trzeba podać następujące dane: nazwa przychodni, adres przychodni, NIP, REGON oraz usługi medyczne które przychodnia będzie świadczyła.

Uzbrojeni w taki informacje możemy zacząć modelować nasz obiekt przychodni, np. przez napisanie takiego kodu w klasie Context behata:

<?php

    use Cocoders\MedicalClinic\Clinic;
    use Cocoders\MedicalClinic\Clinic\Address;
    use Cocoders\MedicalClinic\Clinic\Service;
    use Cocoders\MedicalClinic\Clinic\TaxIdentificationNumber;
    use Cocoders\MedicalClinic\Clinic\NationalEconomyRegisterNumber;

    class MedicalClinicContext implements Context, SnippetAcceptingContext
    {
        private $clinic;

        public function __construct()
        {
            $this->clinic = new Clinic(
                'Clinic name',
                new Address($postalCode = '80-283', $city = 'Gdańsk', $street = 'Królewskie Wzgórze 21/9'),
                $servicesProvidedByClinic = [
                    new Service('MRI'),
                    new Service('CT')
                ],
                new TaxIdentificationNumber('123-456-32-18'),
                new NationalEconomyRegisterNumber('123456785')
            );
        }
    }

Jak widzicie nasz model nie obejmuje tylko klasy Przychodni. Dodaliśmy też dodatkowe klasy dla Adresu, Serwisu medycznego, oraz numerów NIP i REGON. Zrobiliśmy tak ponieważ znamy pewne reguły, których będzie łatwiej pilnować w tych poszczególnych obiektach. Na przykład wiemy, że przychodnia zawsze musi mieć adres i adres powinien być poprawny, co oznacza, że powinien zawierać kod pocztowy, miasto oraz ulicę.

Oczywiście możemy przekazać do obiektu przychodni napis z adresem, jednak logika walidacji oraz wyodrębnienia poszczególnych części z napisu nie jest tego warta. Przez wprowadzenie nowego obiektu adresu to on może zadbać o to, że jest poprawny (przy tworzeniu wszystkie wymagane dane muszą być przekazane do konstruktora). To samo z numerami NIP oraz REGON, mogą one zawierać walidacje swojej poprawności (ostatnia cyfra w tych numerach jest numerem kontrolnym dzięki temu możemy sprawdzić poprawność takiego numeru)

Tak na prawdę nie jest to nic innego jak enkapsulacja oraz ochrona obiektu przed niechcianym stanem. Co jest ciekawe, klasy Address, TaxIdentificationNumber oraz NationalEconomyRegisterNumber są idealnymi kandydami na Value Objects.

Dobra, może skończmy z tą teorią! Możemy uruchomić teraz polecenie vendor/bin/behat i dostać następujący błąd:

PHP Fatal error:  Class 'Cocoders\MedicalClinic\Clinic' not found in features/bootstrap/MedicalClinicContext.php on line 20

Szczegółowa specyfikacja dla obiektów naszej domeny przy użyciu narzędzia phpspec

Co się stało? Po prostu chcemy utworzyć obiekt danej klasy, która nie jest jeszcze utworzona. Jest to idealny moment na stworzenie specyfikacji jednostkowej dla naszej klasy przychodni. Najczęściej do tego celu używamy phpspec. Po jego zainstalowaniu możemy uruchomić następującą komendę vendor/bin/phpspec desc “Cocoders\MedicalClinic\Clinic”. Polecenie to utworzy nam szkielet, w którym będziemy mogli zdefiniować specyfikację dla naszej klasy. Zmodyfikujmy specyfikację w następujący sposób:

<?php

    namespace spec\Cocoders\MedicalClinic;

    use PhpSpec\ObjectBehavior;
    use Prophecy\Argument;

    class ClinicSpec extends ObjectBehavior
    {
        /**
         * @param Cocoders\MedicalClinic\Clinic\Address $address
         * @param Cocoders\MedicalClinic\Clinic\Service $service
         * @param Cocoders\MedicalClinic\Clinic\TaxIdentificationNumber $taxNumber
         * @param Cocoders\MedicalClinic\Clinic\NationalEconomyRegisterNumber $nationalEconomyRegisterNumber $economyRegisterNumber
         */
        function let($address, $service, $taxNumber, $nationalEconomyRegisterNumber)
        {
            $this->beConstructedWith(
                'Clinic name',
                $address,
                $servicesProvidedByClinic = [
                    $service
                ],
                $taxNumber,
                $nationalEconomyRegisterNumber
            );
        }

        function it_is_initializable()
        {
            $this->shouldHaveType('Cocoders\MedicalClinic\Clinic');
        }

        /**
         * @param Cocoders\MedicalClinic\Clinic\Address $address
         * @param Cocoders\MedicalClinic\Clinic\TaxIdentificationNumber $taxNumber
         * @param Cocoders\MedicalClinic\Clinic\NationalEconomyRegisterNumber $nationalEconomyRegisterNumber $economyRegisterNumber
         */
        function it_cannot_be_initialized_without_at_last_one_service(
            $address,
            $taxNumber,
            $nationalEconomyRegisterNumber
        )
        {
            $this->shouldThrow('\InvalidArgumentException')->during('__construct', [
                'Clinic name',
                $address,
                $servicesProvidedByClinic = [],
                $taxNumber,
                $nationalEconomyRegisterNumber
            ]);
        }
    }

Co najlepsze w phpspecu, gdy uruchomimy polecenie vendor/bin/phpspec run, phpspec wygeneruje nam naszą produkcyjną klasę oraz jej interfejs, którego spodziewamy się w danej specyfikacji. Teraz naszym zdaniem jest zaimplementowanie kodu w taki sposób aby spełnił stworzoną przez nas specyfikację.

W tej chwili możemy uruchomić znowu behata, gdzie zobaczymy, że błąd zniknął. Musimy jednak stworzyć implementację dla kroku “Zakładając że jestem recepcjonistą w przychodni” Możemy to zrobić na przykład w taki sposób:

<?php

    use Cocoders\MedicalClinic\Clinic;
    use Cocoders\MedicalClinic\Clinic\Address;
    use Cocoders\MedicalClinic\Clinic\Service;
    use Cocoders\MedicalClinic\Clinic\TaxIdentificationNumber;
    use Cocoders\MedicalClinic\Clinic\NationalEconomyRegisterNumber;

    class MedicalClinicContext implements Context, SnippetAcceptingContext
    {
        //...

        /**
         * @Given że jestem recepcjonistą w przychodn
         */
        public function iAmReceptionistInTheClinic()
        {
            $this->clinic->hireEmployee(
                new Receptionist(
                    $firstName = 'Jan',
                    $lastName = 'Kowalski',
                    $idNumber = '80081012345'
                )
            );
        }

Znowu po uruchomieniu behata dostaniemy błąd, ponieważ metoda hireEmployee oraz klasa Receptionist jeszcze nie istnieją.

Nasza specyfikacja phpspecu dla pliku ClinicSpec.php dla metody hireEmployee może wyglądać następująco:

<?php

    namespace spec\Cocoders\MedicalClinic;

    use PhpSpec\ObjectBehavior;
    use Prophecy\Argument;

    class ClinicSpec extends ObjectBehavior
    {

        function it_allows_for_the_employment(Employee $employee)
        {
            $this->hasEmployee($employee)->shouldBe(false);
            $this->hireEmployee($employee);
            $this->hasEmployee($employee)->shouldBe(true);
        }

a kod do tej metody w pliku ‘Clinic.php` może wyglądać w ten sposób:

    <?php

    class Clinic
    {
        private $employess = [];

        public function hasEmployee(Employee $employee)
        {
            return false !== array_search($employee, $this->employees);
        }

        public function hireEmployee(Employee $employee)
        {
            if (!$this->hasEmployee($employee)) {
                $this->employees[] = $employee;
            }
        }

Jak widzimy pozwoliliśmy zatrudniać pracowników w przychodni. Jednak żeby pozwolić w przychodni zatrudniać recepcjonistów musimy zrobić jeszcze jedną rzecz:

<?php

    class Receptionist extends Employee
    {}

Podsumowanie

Taki iteracyjny proces modelowania aplikacji działa bardzo dobrze w naszym wypadku. Oczywiście nie gwarantujemy, że będzie działać w Waszym. Zależy to od środowiska, klienta oraz pewnie jeszcze kilku rzeczy.

Jeżeli jeszcze nie czytaliście, polecamy post everzeta o podobnym temacie http://everzet.com/post/99045129766/introducing-modelling-by-example

Jeżeli chcecie zobaczyć cały przykład z tego posta zapraszamy na nasze repozytorium na githube

Ciekawe jest to, że takie podejście sprawdziło się w naszym wypadku również przy pracy w tak zwanym kodem “Legacy” - czyli ogólnie dość brzydkim kodem ;) Technika ta okazała się bardzo przydatna w odkrywaniu domeny w takich projektach oraz odkrywania przy pomocy klienta jak pewne rzeczy powinny działać - często z kodu jest to nie do odczytania, lub po prostu kod działa źle. Pomaga to też zdefiniować model w taki sposób, żeby można było go w łatwy sposób wyizolować od kodu “legacy”, poprzez różnego rodzaju adaptery i interfejsy.