Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

Driving Design through Examples

577 views

Published on

Modelling by Example is a set of practices that combine BDD (Behaviour Driven Development) and DDD (Domain Driven Design) techniques to creat a workflow that directly drives code from a starting point of user requirements. We will see how a simple feature can be defined via conversation with stakeholders, captured as automatable requirements, and expressed directly in the object model using tools such as Behat and PhpSpec

Published in: Technology
  • Be the first to comment

Driving Design through Examples

  1. 1. Driving Design through Examples Ciaran McNulty at Dutch PHP Conference 2016
  2. 2. Modelling by Example Combining BDD and DDD concepts
  3. 3. Behaviour Driven Development
  4. 4. BDD helps with 1. Building things well 2. Building the right things 3. Building things for the right reason ... we will focus on 1 & 2
  5. 5. BDD is the art of using examples in conversations to illustrate behaviour — Liz Keogh
  6. 6. Why Examples?
  7. 7. Requirements as Rules We are starting a new budget airline flying between London and Manchester → Travellers can collect 1 point for every £1 they spend on flights → 100 points can be redeemed for £10 off a future flight → Flights are taxed at 20%
  8. 8. Rules are Ambiguous
  9. 9. Ambiguity → When spending points do I still earn new points? → Can I redeem more than 100 points on one flight? → Is tax based on the discounted fare or the original price of the fare?
  10. 10. Examples are Unambiguous
  11. 11. Examples If a flight from London to Manchester costs £50: → If you pay cash it will cost £50 + £10 tax, and you will earn 50 new points → If you pay entirely with points it will cost 500 points + £10 tax and you will earn 0 new points → If you pay with 100 points it will cost 100 points + £40 + £10 tax and you will earn 0 new points
  12. 12. Examples are Objectively Testable
  13. 13. Gherkin A formal language for examples
  14. 14. You do not have to use Gherkin
  15. 15. Feature: Earning and spending points on flights Rules: - Travellers can collect 1 point for every £1 they spend on flights - 100 points can be redeemed for £10 off a future flight Scenario: Earning points when paying cash Given ... Scenario: Redeeming points for a discount on a flight Given ... Scenario: Paying for a flight entirely using points Given ...
  16. 16. Gherkin steps → Given sets up context for a behaviour → When specifies some action → Then specifies some outcome Action + Outcome = Behaviour
  17. 17. Scenario: Earning points when paying cash Given a flight costs £50 When I pay with cash Then I should pay £50 for the flight And I should pay £10 tax And I should get 50 points Scenario: Redeeming points for a discount on a flight Given a flight costs £50 When I pay with cash plus 100 points Then I should pay £40 for the flight And I should pay £10 tax And I should pay 100 points Scenario: Paying for a flight entirely using points Given a flight costs £50 When I pay with points only Then I should pay £0 for the flight And I should pay £10 tax And I should pay 500 points
  18. 18. Who writes examples? Business expert Testing expert Development expert All discussing the feature together
  19. 19. When to write scenarios → Before you start work on the feature → Not too long before! → Whenever you have access to the right people
  20. 20. Refining scenarios → When would this outcome not be true? → What other outcomes are there? → But what would happen if...? → Does this implementation detail matter?
  21. 21. Scenarios are not Contracts
  22. 22. Scenarios → Create a shared understanding of a feature → Give a starting definition of done → Provide an objective indication of how to test a feature
  23. 23. Domain Driven Design
  24. 24. DDD tackles complexity by focusing the team's attention on knowledge of the domain — Eric Evans
  25. 25. Invest time in understanding the business
  26. 26. Ubiquitous Language → A shared way of speaking about domain concepts → Reduces the cost of translation when business and development communicate → Try to establish and use terms the business will understand
  27. 27. Modelling by Example
  28. 28. By embedding Ubiquitous Language in your scenarios, your scenarios naturally become your domain model — Konstantin Kudryashov (@everzet)
  29. 29. Principles → The best way to understand the domain is by discussing examples → Write scenarios that capture ubiquitous language → Write scenarios that illustrate real situations → Directly drive the code model from those examples
  30. 30. Directly driving code with Behat?
  31. 31. Layered architecture
  32. 32. UI testing with Behat
  33. 33. Testing through the UI → Slow to execute → Brittle → Makes you design the domain and UI at the same time
  34. 34. Test the domain first
  35. 35. Testing with real infrastructure → Slow to execute → Brittle → Makes you design the domain and infrastructure at the same time
  36. 36. Test with fake infrastructure first
  37. 37. Improving scenarios
  38. 38. Scenario: Earning points when paying cash Given a flight costs £50 When I pay with cash Then I should pay £50 for the flight And I should pay £10 tax And I should get 50 points Scenario: Redeeming points for a discount on a flight Given a flight costs £50 When I pay with cash plus 100 points Then I should pay £40 for the flight And I should pay £10 tax And I should pay 100 points Scenario: Paying for a flight entirely using points Given a flight costs £50 When I pay with points only Then I should pay £0 for the flight And I should pay £10 tax And I should pay 500 points
  39. 39. Add realistic details
  40. 40. Background: Given a flight from "London" to "Manchester" costs £50 Scenario: Earning points when paying cash When I fly from "London" to "Manchester" And I pay with cash Then I should pay £50 for the flight And I should pay £10 tax And I should get 50 points
  41. 41. Actively seek terms from the domain
  42. 42. → What words do you use to talk about these things? → Points? Paying? Cash Fly? → Is the cost really attached to a flight? → Do you call this thing "tax"? → How do you think about these things?
  43. 43. Get good at listening
  44. 44. Lessons from the conversation → Price belongs to a Fare for a specific Route → Flight is independently assigned to a Route → Some sort of fare listing system controls Fares → I get quoted a cost at the point I purchase a ticket This is really useful to know!
  45. 45. Background: Given a flight "XX-100" flies the "LHR" to "MAN" route And the current listed fare for the "LHR" to "MAN" route is £50 Scenario: Earning points when paying cash When I am issued a ticket on flight "XX-100" And I pay £50 cash for the ticket Then the ticket should be completely paid And the ticket should be worth 50 loyalty points
  46. 46. Driving the domain model with Behat
  47. 47. Configure a Behat suite default: suites: core: contexts: [ FlightsContext ]
  48. 48. Create a context class FlightsContext implements Context { /** * @Given a flight :arg1 flies the :arg2 to :arg3 route */ public function aFlightFliesTheRoute($arg1, $arg2, $arg3) { throw new PendingException(); } // ... }
  49. 49. Run Behat
  50. 50. Model values as Value Objects
  51. 51. class FlightsContext implements Context { /** * @Given a flight :flightnumber flies the :origin to :destination route */ public function aFlightFliesTheRoute($flightnumber, $origin, $destination) { $this->flight = new Flight( FlightNumber::fromString($flightnumber), Route::between( Airport::fromCode($origin), Airport::fromCode($destination) ) ); } // ... }
  52. 52. Transformations /** * @Transform :flightnumber */ public function transformFlightNumber($number) { return FlightNumber::fromString($number); } /** * @Transform :origin * @Transform :destination */ public function transformAirport($code) { return Airport::fromCode($code); }
  53. 53. /** * @Given a flight :flightnumber flies the :origin to :destination route */ public function aFlightFliesTheRoute( FlightNumber $flightnumber, Airport $origin, Airport $destination ) { $this->flight = new Flight( $flightnumber, Route::between($origin, $destination) ); }
  54. 54. > vendor/bin/behat PHP Fatal error: Class 'Flight' not found
  55. 55. Describe objects with PhpSpec
  56. 56. class AirportSpec extends ObjectBehavior { function it_can_be_represented_as_a_string() { $this->beConstructedFromCode('LHR'); $this->asCode()->shouldReturn('LHR'); } function it_cannot_be_created_with_invalid_code() { $this->beConstructedFromCode('1234566XXX'); $this->shouldThrow(Exception::class)->duringInstantiation(); } }
  57. 57. class Airport { private $code; private function __construct($code) { if (!preg_match('/^[A-Z]{3}$/', $code)) { throw new InvalidArgumentException('Code is not valid'); } $this->code = $code; } public static function fromCode($code) { return new Airport($code); } public function asCode() { return $this->code; } }
  58. 58. /** * @Given the current listed fare for the :arg1 to :arg2 route is £:arg3 */ public function theCurrentListedFareForTheToRouteIsPs($arg1, $arg2, $arg3) { throw new PendingException(); }
  59. 59. Model boundaries with Interfaces
  60. 60. interface FareList { public function listFare(Route $route, Fare $fare); }
  61. 61. Create in-memory versions for testing namespace Fake; class FareList implements FareList { private $fares = []; public function listFare(Route $route, Fare $fare) { $this->fares[$route->asString()] = $fare; } }
  62. 62. /** * @Given the current listed fare for the :origin to :destination route is £:fare */ public function theCurrentListedFareForTheToRouteIsPs( Airport $origin, Airport $destination, Fare $fare ) { $this->fareList = new FakeFareList(); $this->fareList->listFare( Route::between($origin, $destination), Fare::fromString($fare) ); }
  63. 63. Run Behat
  64. 64. /** * @When Iam issued a ticket on flight :arg1 */ public function iAmIssuedATicketOnFlight($arg1) { throw new PendingException(); }
  65. 65. /** * @When I am issued a ticket on flight :flight */ public function iAmIssuedATicketOnFlight() { $ticketIssuer = new TicketIssuer($this->fareList); $this->ticket = $ticketIssuer->issueOn($this->flight); }
  66. 66. > vendor/bin/behat PHP Fatal error: Class 'TicketIssuer' not found
  67. 67. class TicketIssuerSpec extends ObjectBehavior { function it_can_issue_a_ticket_for_a_flight(Flight $flight) { $this->issueOn($flight)->shouldHaveType(Ticket::class); } }
  68. 68. class TicketIssuer { public function issueOn(Flight $flight) { return Ticket::costing(Fare::fromString('10000.00')); } }
  69. 69. Run Behat
  70. 70. /** * @When I pay £:fare cash for the ticket */ public function iPayPsCashForTheTicket(Fare $fare) { $this->ticket->pay($fare); }
  71. 71. PHP Fatal error: Call to undefined method Ticket::pay()
  72. 72. class TicketSpec extends ObjectBehavior { function it_can_be_paid() { $this->pay(Fare::fromString("10.00")); } }
  73. 73. class Ticket { public function pay(Fare $fare) { } }
  74. 74. Run Behat
  75. 75. The model will be anaemicUntil you get to Then
  76. 76. /** * @Then the ticket should be completely paid */ public function theTicketShouldBeCompletelyPaid() { throw new PendingException(); }
  77. 77. /** * @Then the ticket should be completely paid */ public function theTicketShouldBeCompletelyPaid() { assert($this->ticket->isCompletelyPaid() == true); }
  78. 78. PHP Fatal error: Call to undefined method Ticket::isCompletelyPaid()
  79. 79. class TicketSpec extends ObjectBehavior { function let() { $this->beConstructedCosting(Fare::fromString("50.00")); } function it_is_not_completely_paid_initially() { $this->shouldNotBeCompletelyPaid(); } function it_can_be_paid_completely() { $this->pay(Fare::fromString("50.00")); $this->shouldBeCompletelyPaid(); } }
  80. 80. class Ticket { private $fare; // ... public function pay(Fare $fare) { $this->fare = $this->fare->deduct($fare); } public function isCompletelyPaid() { return $this->fare->isZero(); } }
  81. 81. class FareSpec extends ObjectBehavior { function let() { $this->beConstructedFromString('100.00'); } function it_can_deduct_an_amount() { $this->deduct(Fare::fromString('10'))->shouldBeLike(Fare::fromString('90.00')); } }
  82. 82. class Fare { private $pence; private function __construct($pence) { $this->pence = $pence; } // ... public function deduct(Fare $amount) { return new Fare($this->pence - $amount->pence); } }
  83. 83. class FareSpec extends ObjectBehavior { // ... function it_knows_when_it_is_zero() { $this->beConstructedFromString('0.00'); $this->shouldBeZero(); } function it_is_not_zero_when_it_has_a_value() { $this->beConstructedFromString('10.00'); $this->shouldNotBeZero(); } }
  84. 84. class Fare { private $pence; private function __construct($pence) { $this->pence = $pence; } // ... public function isZero() { return $this->pence == 0; } }
  85. 85. Run Behat
  86. 86. class TicketIssuerSpec extends ObjectBehavior { function it_issues_a_ticket_with_the_correct_fare(FareList $fareList) { $route = Route::between(Airport::fromCode('LHR'), Airport::fromCode('MAN')); $flight = new Flight(FlightNumber::fromString('XX001'), $route); $fareList->findFareFor($route)->willReturn(Fare::fromString('50')); $this->beConstructedWith($fareList); $this->issueOn($flight)->shouldBeLike(Ticket::costing(Fare::fromString('50'))); } }
  87. 87. class TicketIssuer { private $fareList; public function __construct(FareList $fareList) { $this->fareList = $fareList; } public function issueOn(Flight $flight) { return Ticket::costing($this->fareList->findFareFor($flight->getRoute())); } }
  88. 88. interface FareList { public function listFare(Route $route, Fare $fare); public function findFareFor(Route $route); }
  89. 89. class FareList implements FareList { private $fares = []; public function listFare(Route $route, Fare $fare) { $this->fares[$route->asString()] = $fare; } public function findFareFor(Route $route) { return $this->fares[$route->asString()]; } }
  90. 90. Run Behat
  91. 91. /** * @Then I the ticket should be worth :points loyalty points */ public function iTheTicketShouldBeWorthLoyaltyPoints(Points $points) { assert($this->ticket->getPoints() == $points); }
  92. 92. class FareSpec extends ObjectBehavior { function let() { $this->beConstructedFromString('100.00'); } // ... function it_calculates_points() { $this->getPoints()->shouldBeLike(Points::fromString('100')); } }
  93. 93. class TicketSpec extends ObjectBehavior { function let() { $this->beConstructedCosting(Fare::fromString("100.00")); } // ... function it_gets_points_from_original_fare() { $this->pay(Fare::fromString("50")); $this->getPoints()->shouldBeLike(Points::fromString('100')); } }
  94. 94. <?php class Ticket { private $revenueFare; private $fare; private function __construct(Fare $fare) { $this->revenueFare = $fare; $this->fare = $fare; } // ... public function getPoints() { return $this->revenueFare->getPoints(); } }
  95. 95. Run Behat
  96. 96. Where is our domain model?
  97. 97. Feature: Earning and spending points on flights Rules: - Travellers can collect 1 point for every £1 they spend on flights - 100 points can be redeemed for £10 off a future flight Background: Given a flight "XX-100" flies the "LHR" to "MAN" route And the current listed fare for the "LHR" to "MAN" route is £50 Scenario: Earning points when paying cash When I am issued a ticket on flight "XX-100" And I pay £50 cash for the ticket Then the ticket should be completely paid And I the ticket should be worth 50 loyalty points
  98. 98. > bin/phpspec run -f pretty Airport 10 ✔ can be represented as a string 16 ✔ cannot be created with invalid code Fare 15 ✔ can deduct an amount 20 ✔ knows when it is zero 26 ✔ is not zero when it has a value 31 ✔ calculates points FlightNumber 10 ✔ can be represented as a string Flight 13 ✔ exposes route Points 10 ✔ is constructed from string Route 12 ✔ has a string representation TicketIssuer 16 ✔ issues a ticket with the correct fare Ticket 15 ✔ is not completely paid initially 20 ✔ is not paid completely if it is partly paid 27 ✔ can be paid completely 34 ✔ gets points from original fare
  99. 99. End to End Testing
  100. 100. With the domain already modelled → UI tests do not have to be comprehensive → Can focus on intractions and UX → Actual UI code is easier to write!
  101. 101. default: suites: core: contexts: [ FlightsContext ] web: contexts: [ WebFlightsContext ] filters: { tags: @ui }
  102. 102. Feature: Earning and spending points on flights Scenario: Earning points when paying cash Given ... @ui Scenario: Redeeming points for a discount on a flight Given ... Scenario: Paying for a flight entirely using points Given ...
  103. 103. Modelling by Example → Focuses attention on use cases → Helps developers understand core business domains → Encourages layered architecture → Speeds up test suites
  104. 104. Use it when → Module is core to your business → You are likely to support business changes in the future → You can have conversations with stakeholders
  105. 105. Do not use when... → Not core to the business → Prototype or short-term project → It can be thrown away when the business changes → You have no access to business experts (but try and change this)
  106. 106. → Rate talk: https://joind.in/talk/304ff → Join us: http://bit.ly/inviqa-careers → Get help: http://bit.ly/inviqa-contact

×