TPP Topic 23: Design by Contract
Tags: thepragmaticprogrammer, book, programming, education
Published: 2019-09-19 10:58:42

---
categories:
- The Pragmatic Programmer
published: true
tags:
- The Pragmatic Programmer
- book
- programming
- education
title: 'TPP Topic 23: Design by Contract'
---

> This post originally appeared on [steadbytes.com](https://steadbytes.com/blog/the-pragmatic-programmer-20th/topic-23-exercises/) 
<div></div>
> See the [first post](https://dev.to/steadbytes/the-pragmatic-programmer-20th-anniversary-edition-series-1e2l) in The Pragmatic Programmer 20th Anniversary Edition series for an introduction.

## Challenge 1

> Points to ponder: If DBC is so powerful, why isn’t it used more widely? Is it hard to come up with the contract? Does it make you think about issues you’d rather ignore for now? Does it force you to THINK!? Clearly, this is a dangerous tool!

Design by contract leads to similar arguments as the great dynamic vs static typing debate:

- Code conciseness
  - Dynamic languages _can_ be more concise
- Speed of iteration
  - Dynamic languages is often quicker (though there is a limit)
  - Static languages require more _up front_ consideration
- Ease of creating _generic_ code
  - Often easier in dynamic languages

I emphasise that none of the above points are _absolute_ truths; there are certainly exceptions to both sides of each and nor are they necessarily _my views_.

For DBC to be effective, the constraints of a system need to be known _and_ considered. Often, one or both of these is not exercised during development. Furthermore, I think there is a **notion** that (similar to static types) DBC 'cements' the implementation of code and thus prevents agile, iterative development. If DBC is implemented carefully ,however, this is not the case. The implementation should be decoupled from the constraints imposed by the contracts and changing contracts as the requirements of
software changes should be a normal practice. Achieving this however does require more up front work.

## Exercise 14

> Design an interface to a kitchen blender. It will eventually be a web-based, IoT-enabled blender, but for now we just need the interface to control it. It has ten speed settings (0 means off). You can’t operate it empty, and you can change the speed only one unit at a time (that is, from 0 to 1, and from 1 to 2, not from 0 to 2).
>
> Here are the methods. Add appropriate pre- and postconditions and an invariant.

```
int getSpeed()
void setSpeed(int x)
boolean isFull()
void fill()
void empty()
```

See [dbc_blender](https://github.com/SteadBytes/study/tree/master/the-pragmatic-programmer-20th/23/exercises/dbc-blender) for full code and tests.

I've chosen to implement this in Clojure due to it's built in support for pre- and postconditions provided by [condition maps](https://clojure.org/reference/special_forms#_fn_name_param_condition_map_expr) and the excellent [`spec`](https://clojure.org/guides/spec#_specing_functions) which is ideal for imposing constraints on data (also because I like Clojure).

To better fit the functional approach of Clojure, I have made a few changes to the proposed OOP style interface:

- The blender is modelled as a [`map`](https://clojure.org/reference/data_structures#Maps) instead of a class
  ```clojure
  => {::speed 5 ::full true}
  ```
- `getSpeed` and `isFull` are not implemented as the keys of the blender `map` can be used directly
  ```clojure
  ; getSpeed
  => (::speed {::speed 5 ::full true})
  5
  ; isFull
  => (::full {::speed 5 ::full true})
  true
  ```
- `setSpeed`, `fill` and `empty` functions are not `void`; instead returning a full blender
  `map` updated to reflect the desired changes in order to avoid mutation of state

Before diving into any implementations, here's the namespace definition that any code
examples will be working within:

```clojure
(ns dbc-blender.core
  (:gen-class)
  (:require [clojure.spec.alpha :as s]
            [clojure.math.numeric-tower :as math]))
```

First off, a valid range for the blender speed needs to be determined. The question states that
speed must increment one unit at a time; suggesting that an `integer` data type makes sense (though
further enforcement is required). Furthermore, a speed cannot be negative and (at least on all blenders I've ever seen) there is a sensible limit to the speed value.

```clojure
; turn it up to 11!
(def max-speed 11)

; speed is an integer and in correct range
(s/def ::speed (s/and int? #(and (>= % 0) (<= % max-speed))))
```

Next, the blender `map` has a few constraints:

- Requires two keys `::speed` and `::full`
- `::full` must be of `boolean` type
- The blender should only be on when full. This is not explicitly stated in the exercise, but it is a sensible design constraint that I've chosen to apply

```clojure
(s/def ::full boolean?)
(s/def ::blender (s/and (s/keys :req [::speed ::full])
                        #(if (> (::speed %) 0) ; should only be on when full
                           (::full %)
                           true)))
```

`set-speed` constraints:

- Take as input a valid `::blender` and a valid `::speed`
- Return a new valid `::blender` with an updated `::speed` value
- `::speed` can only be increased by one unit

```clojure
(defn set-speed [blender x]
  {:pre [(s/valid? ::blender blender)
         (s/valid? ::speed x)
         (::full blender)
         (= (math/abs (- (::speed blender) x)) 1)] ; increase in single increments
   :post [(= (::speed %) x) ; speed was set
          (s/valid? ::blender %)]}
  (assoc blender ::speed x))
```

`fill` constraints:

- Take as input a valid `::blender`
- Return a new valid `::blender` that is full
- Blender cannot already be full
- Blender cannot be switched on
  - Potentially dangerous
  - Make a mess

```clojure
(defn fill [blender]
  {:pre [(s/valid? ::blender blender)
         (= (::speed blender) 0) ; can't fill a spinning blender
         (not (::full blender))] ; can't fill an already full blender
   :post [(s/valid? ::blender %)
          (::full %)]} ; blender was filled
  (assoc blender ::full true))
```

`empty` constraints:

- Take as input a valid `::blender`
- Return a new valid `::blender` that is empty
- Blender cannot already be empty
- Blender cannot be switched on (same reasoning as for `fill`)

```clojure
(defn empty [blender]
  {:pre [(s/valid? ::blender blender)
         (= (::speed blender) 0) ; can't empty a spinning blender
         (::full blender)] ; can't empty an empty blender
   :post [(s/valid? ::blender %)
          (not (::full %))]} ; blender was emptied
  (assoc blender ::full false))
```

Take a look at the [tests](https://github.com/SteadBytes/study/blob/master/the-pragmatic-programmer-20th/23/exercises/dbc-blender/test/dbc_blender/core_test.clj) for comprehensive examples of correct and incorrect usage
of the interface and how they are enforced by the specified contracts.

## Exercise 15

> How many numbers are in the series 0, 5, 10, 15, …, 100?

21

I was slightly confused by this question at first; what has a simple series got
to do with DBC? However, it's purpose is to challenge the reader to consider the precise
specification of the series. From the solution provided by the authors:

> If you said 20, you just experienced a fence post error (not knowing whether to
> count the fenceposts or the spaces in between them)

An answer of 20 implies a series _exclusive_ of either the start or end values:

- 0, 5, 10, 15, ..., 95
- 5, 10, 15, ..., 100

Here's some Python to demonstrate each of these, remember that the `range` function
is _start inclusive_ and _end exclusive_:

```python
>>> len(range(0, 105, 5)) # correct
21

>>> len(range(0, 100, 5)) # incorrect: excludes the final element 100
20

>>> len(range(5, 105, 5)) # incorrect: excludes the starting element 0
20
```