A presentation by @stuherbert
for @GanbaroDigital
Type Integrity
The Software Engineering
Behind Stricter Typing
Slide 2
Slide 2 text
@GanbaroDigital
This is a follow-up
to the January 2020 talks
@PHPSW
by Rob Allen
and Dave Liddament
Slide 3
Slide 3 text
@GanbaroDigital
Although the examples
are in PHP,
the underlying principles
apply to
all programming languages.
Slide 4
Slide 4 text
@GanbaroDigital
What's
The Problem?
Slide 5
Slide 5 text
@GanbaroDigital
A Worked Example
Slide 6
Slide 6 text
@GanbaroDigital
A very simple example,
that calculates
the sales tax / VAT owed.
Slide 7
Slide 7 text
@GanbaroDigital
function calculateVat($amount, $rate)
{
return ($amount/100) * $rate;
}
Slide 8
Slide 8 text
@GanbaroDigital
calculateVat()
is not robust
and not always correct.
Slide 9
Slide 9 text
@GanbaroDigital
function calculateVat($amount, $rate)
{
return ($amount/100) * $rate;
}
Slide 10
Slide 10 text
@GanbaroDigital
function calculateVat($amount, $rate)
{
if (!is_int($amount)) {
throw new Exception(...);
}
return ($amount/100) * $rate;
}
Slide 11
Slide 11 text
@GanbaroDigital
function calculateVat($amount, $rate)
{
if (!is_int($amount)) {
throw new Exception(...);
}
if (!is_int($rate)) {
throw new Exception(...);
}
return ($amount/100) * $rate;
}
Slide 12
Slide 12 text
@GanbaroDigital
We've gone from
1 line of code
to 5 lines of code ...
... and we're just
getting started!
Slide 13
Slide 13 text
@GanbaroDigital
Without type hints,
someone has to write
every single check.
Slide 14
Slide 14 text
@GanbaroDigital
Every check you add
is executed every time
the function / method is called.
Slide 15
Slide 15 text
@GanbaroDigital
Every line of code
that you add
needs its own unit test.
Slide 16
Slide 16 text
@GanbaroDigital
We live in a world
where the time it takes
to create and ship working code
is often the biggest cost
for a project / org / business.
Slide 17
Slide 17 text
@GanbaroDigital
Some of these runtime checks
can be replaced
with type-hinting ...
Slide 18
Slide 18 text
@GanbaroDigital
function calculateVat($amount, $rate)
{
if (!is_int($amount)) {
throw new Exception(...);
}
if (!is_int($rate)) {
throw new Exception(...);
}
return ($amount/100) * $rate;
}
Slide 19
Slide 19 text
@GanbaroDigital
declare(strict_types=1);
function calculateVat(
int $amount,
int $rate
)
{
return ($amount/100) * $rate;
}
Slide 20
Slide 20 text
@GanbaroDigital
... but calculateVat()
still isn't robust
and still isn't always correct.
Slide 21
Slide 21 text
@GanbaroDigital
Remaining Issues Include ...
• Generating negative values
• Locale-specific rules on rounding up / down
• Accepting the wrong currency
• Accepting invalid VAT rates
Slide 22
Slide 22 text
@GanbaroDigital
Every legal value of
$amount and $rate
is an integer.
Not every integer
is a legal value
of $amount and $rate.
@GanbaroDigital
CartAmountToTax and VatRate
are value types.
Slide 47
Slide 47 text
@GanbaroDigital
In many languages
(including PHP),
the only way
to define a value type
is to define a class.
Slide 48
Slide 48 text
@GanbaroDigital
declare(strict_types=1);
class CartAmountToTax {
public constructor(???) { ... }
}
class VatRate {
public constructor(???) { ... }
}
Slide 49
Slide 49 text
@GanbaroDigital
??
??
What do we need to know
to create these values?
Slide 50
Slide 50 text
@GanbaroDigital
If you're not sure
where to start,
start with the primitive types
that you are replacing.
Slide 51
Slide 51 text
@GanbaroDigital
declare(strict_types=1);
class CartAmountToTax
{
public constructor(
int $amount
) {
....
}
}
Slide 52
Slide 52 text
@GanbaroDigital
declare(strict_types=1);
class VatRate
{
public constructor(
string $jurisdiction,
int $rate
) {
...
}
}
Slide 53
Slide 53 text
@GanbaroDigital
??
??
What work will
these constructors do?
Slide 54
Slide 54 text
@GanbaroDigital
Every legal value of
$amount and $rate
is an integer.
Not every integer
is a legal value
of $amount and $rate.
Slide 55
Slide 55 text
@GanbaroDigital
Type refinement
takes a wider data type
(like an int)
and reduces it
to a narrower data type
(like a VatRate).
Slide 56
Slide 56 text
@GanbaroDigital
Type refinement
is done by
smart constructors.
Slide 57
Slide 57 text
@GanbaroDigital
Smart Constructors
Slide 58
Slide 58 text
@GanbaroDigital
??
??
What work will
these constructors do?
Slide 59
Slide 59 text
@GanbaroDigital
Smart constructors
enforce the data constraints
for their value type.
Slide 60
Slide 60 text
@GanbaroDigital
A constraint
is a non-negotiable condition
that must be met.
Slide 61
Slide 61 text
@GanbaroDigital
??
??
What are the constraints
of the CartAmountToTax
value type?
Slide 62
Slide 62 text
@GanbaroDigital
??
??
What pre-conditions
must be met?
Slide 63
Slide 63 text
@GanbaroDigital
class CartAmountToTax
{
public constructor(
int $amount
) {
// robustness!
if ($amount < 0) {
throw new Exception(...);
}
}
}
Slide 64
Slide 64 text
@GanbaroDigital
class VatRate
{
public constructor(
string $jurisdiction,
int $rate
) {
// robustness!
if (!this->isValidVatRate(...)) {
throw new Exception(...);
}
}
}
Slide 65
Slide 65 text
@GanbaroDigital
Smart constructors
prevent the creation
of illegal values.
Slide 66
Slide 66 text
@GanbaroDigital
??
??
Haven't we simply added
defensive programming
to our class constructors?
Slide 67
Slide 67 text
@GanbaroDigital
Values built by smart constructors
are guaranteed to be legal.
Slide 68
Slide 68 text
@GanbaroDigital
We move defensive programming
from everywhere we use data
to everywhere we create typed data.
Slide 69
Slide 69 text
@GanbaroDigital
Data Guards
Slide 70
Slide 70 text
@GanbaroDigital
class VatRate
{
public constructor(
string $jurisdiction,
int $rate
) {
// robustness!
if (!this->isValidVatRate(...)) {
throw new Exception(...);
}
}
}
Slide 71
Slide 71 text
@GanbaroDigital
class VatRate
{
public function isValidVatRate(...): boolean {
// inspect params here
}
}
Slide 72
Slide 72 text
@GanbaroDigital
Data guards
are functions / methods
that return TRUE
if a data constraint has been met.
Slide 73
Slide 73 text
@GanbaroDigital
Each data guard
checks
one data constraint,
and ONLY one.
Slide 74
Slide 74 text
@GanbaroDigital
A specification
is a data guard
that calls other data guards.
Slide 75
Slide 75 text
@GanbaroDigital
Data guards
never throw Exceptions.
Slide 76
Slide 76 text
@GanbaroDigital
Data Guarantees
Slide 77
Slide 77 text
@GanbaroDigital
class VatRate
{
public constructor(
string $jurisdiction,
int $rate
) {
// robustness!
if (!this->isValidVatRate(...)) {
throw new Exception(...);
}
}
}
Slide 78
Slide 78 text
@GanbaroDigital
??
??
Who checks
the return values
from function / method calls
all the time?
Slide 79
Slide 79 text
@GanbaroDigital
You can't rely on developers
checking return values
from function calls.
Never use return values
to report an error.
Slide 80
Slide 80 text
@GanbaroDigital
class VatRate
{
public constructor(
string $jurisdiction,
int $rate
) {
// robustness!
mustBeValidVatRate(...);
}
}
Slide 81
Slide 81 text
@GanbaroDigital
function mustBeValidVatRate(...) {
if (isValidVatRate(...)) {
return;
}
throw new Exception(...);
}
Slide 82
Slide 82 text
@GanbaroDigital
function mustBeValidVatRate(...) {
if (isValidVatRate(...)) {
return;
}
throw new Exception(...);
}
Slide 83
Slide 83 text
@GanbaroDigital
Data guarantees
are built from
data guards.
Slide 84
Slide 84 text
@GanbaroDigital
We don't have to
repeat the unit tests,
because
we are not repeating the code
(the input validation).
Slide 85
Slide 85 text
@GanbaroDigital
function mustBeValidVatRate(...) {
if (isValidVatRate(...)) {
return;
}
throw new Exception(...);
}
Slide 86
Slide 86 text
@GanbaroDigital
Data guarantees throw Exceptions
if a data constraint
has not been met.
Slide 87
Slide 87 text
@GanbaroDigital
??
??
Should data guarantees
and data checks
be methods, or functions?
Slide 88
Slide 88 text
@GanbaroDigital
Data guards and guarantees
are more of
a modular programming style
than pure OOP.
Slide 89
Slide 89 text
@GanbaroDigital
??
??
Why did we call it
CartAmountToTax
and not something like
Currency or Money?