BLOG

Cocoders design flow - specification and modelling by example


We would like to share our experience regarding the way we design application and modelling domain. Let’s start from business of course. We try to understand what problems our customers have and how we can help. Understanding “why” is very important, so communication techniques like “impact mapping” can be very helpful. In this stage we always start to discover language of our customer… and that is extremely important as well.

Our sample application

Let’s say we are working with client in medical industry. After discussions, meetings and some planning session with customer we created backlog. Let’s say that we have to implement such user story which is very valuable for our customer:

As a receptionist in the clinic,
I would like arrange a visit to a doctor without worrying about whether patient is insured so
I should be able to view and check such information in the system instead of calling insurance company

Presumptions and rules:
- Patient who is not insured can not have free visits (except emergencies)
- We can fetch information about insurance from national insurance database which is available for clinic

Illustrating using examples

Now we can illustrate our user story using examples. We can use gherkin format for that.

    Feature: As a receptionist in the clinic,
        I would like arrange a visit to a doctor without worrying about whether patient is insured so
        I should be able to view and check such information in the system instead of calling insurance company

    Presumptions and rules:
    - Patient who is not insured can not have free visits (except emergencies)
    - We can fetch information about insurance from national insurance database which is available for clinic

    Scenario: Checking information for patient without insurance during medical visit arrangement
    	Given I am receptionist in the clinic
        And patient does not have medical insurance
        When I am find and open that patient case
        Then I should see that the patient can not be scheduled for a medical visit because he is not insured

    Scenario: Checking information for patient with insurance during medical visit arrangement
    	Given I am receptionist in the clinic
        And patient has medical insurance
        When I am find and open that patient case
        Then I should see that the patient can be scheduled for a medical visit

As you can see, we added context to user story using examples, and we prepared two scenarios which can be important from the customer point of view. We did it in the customer language, so he should understand our examples and should be able to verify our assumptions about his business. Using a common language with our clients highly improves our communication and common understanding with clients.

Modelling by Example

This is a great technique, we are already using it from some time. We can use behat to build our model based on our examples. Basically we just need install behat and run vendor/bin/behat –append-snippets to generate step code snippets in Context class. We will start modelling from our “I am receptionist in the clinic” step.

In example we can see that we will need Clinic an Receptionist concepts in our application. We need to remember the big picture here as well. We suppose that not only Receptionists in Clinic will use the system. It is a good time to communicate with our customer. Let’s assume that Customer confirms that system should be used by all Clinic employees in the future, and we do not know yet how many employee types clinic can have - we just need some abstract Employee concept probably.

Ok so let’s start from Clinic concept. It can be something like $clinic = new Clinic() right? Wait a second… In our specific domain clinic cannot exists without information which are required by the law, like: clinic name, clinic address, medical services provided by clinic, tax identification number and number from register of national economy. How do we know? Again we fetch that knowledge from the Customer (or Domain expert).

We have all needed information, so we can start modelling by writing such code in our Context:

<?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')
            );
        }
    }

As you see, Clinic cannot be created without required information. We even want to create special classes for some of those information. We did it because of domain requirements we have - so for example Clinic should always have Address and this address should be valid.

We can pass address as string to Clinic object of course, but we need to add code to Clinic object constructor to make sure that address is valid. By providing new Address class we can be sure that invalid address will never exist in the system (cause Address class will care about it during creation in the constructor).

This is encapsulation and protecting object invariants. BTW Address, TaxIdentificationNumber and NationalEconomyRegisterNumber seems to be great candidate to stay value objects.

Ok, enough with theory. We can run vendor/bin/behat now… and we get such error:

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

Detailed domain model specification by phpspec

Yeah we are having an error cause we just want use object in behat context which is not created yet. This is an ideal time to start creating detailed specification for our Clinic object. Most of the time we are using phpspec for that. After installation of phpspec we can run vendor/bin/phpspec desc “Cocoders\MedicalClinic\Clinic”. It is a command which generates phpspec specification skeleton for our Clinic class. Let’s modify that skeleton to be able to create instance of Clinic object. After our changes, it should look like that:

<?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
            ]);
        }
    }

Running vendor/bin/phpspec run generates production class draft for us. Our job now is to make our code work according to the specification we just created. Now we can run behat again and we see that we need to write pending definition for “I am receptionist in the clinic” step. In this moment we can provide concept of Clinic Employee. We want to add any kind of employee to clinic, and we do not yet know how many sorts of employees clinic can have. We can model it like that:

<?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 I am receptionist in the clinic
         */
        public function iAmReceptionistInTheClinic()
        {
            $this->clinic->hireEmployee(
                new Receptionist(
                    $firstName = 'Jan',
                    $lastName = 'Kowalski',
                    $idNumber = '80081012345'
                )
            );
        }

Again we will get an error when we run behat – hireEmployee method and Receptionist class do not exist.

Let’s create specification in ClinicSpec.php for hireEmployee:

<?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);
        }

And production code created according to the specification in ‘Clinic.php`:

    <?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;
            }
        }

Of course now we can allow the Receptionist to be hired by Clinic using such code:

<?php

    class Receptionist extends Employee
    {}

Summary

Such iterative modelling process works for us very well. There is a great everzet article about it as well if you haven’t read it yet, please do it now http://everzet.com/post/99045129766/introducing-modelling-by-example

If you want to see working examples please check our github repository

Such approach is very useful in our case when we are working with legacy code as well. It seems to be really helpful with discovering domain model in such projects. We can isolate some stuff from legacy code base by providing interfaces. We can rely on those interfaces in the new concept we are adding to existing project.