Slide 1

Slide 1 text

A presentation by @stuherbert
 for @GanbaroDigital Railway-Oriented Programming A Functional Approach In PHP

Slide 2

Slide 2 text

Industry veteran: architect, engineer, leader, manager, mentor F/OSS contributor since 1994 Talking and writing about PHP since 2004 Chief Software Archaeologist Building Quality @GanbaroDigital About Stuart

Slide 3

Slide 3 text

Follow me I do tweet a lot about non-tech stuff though :) @stuherbert

Slide 4

Slide 4 text

@GanbaroDigital I am not a functional programmer.

Slide 5

Slide 5 text

@GanbaroDigital https://flic.kr/p/bWVcA1 I’m On A Quest

Slide 6

Slide 6 text

@GanbaroDigital My interest is making code more reusable.

Slide 7

Slide 7 text

@GanbaroDigital My second interest is making code easier to reason about.

Slide 8

Slide 8 text

@GanbaroDigital My third interest is making code more robust.

Slide 9

Slide 9 text

@GanbaroDigital RRR Development • Reusable • Reason about it • Robust ... without sacrificing performance!

Slide 10

Slide 10 text

@GanbaroDigital https://flic.kr/p/iH2v8F Am I Tilting At Windmills?

Slide 11

Slide 11 text

@GanbaroDigital Why Look At Functional Programming?

Slide 12

Slide 12 text

@GanbaroDigital “ PHP is an imperative language ... ... with a Java-ish OOP identity.

Slide 13

Slide 13 text

@GanbaroDigital “ Reuse in OO is, always has been, and always will be disappointing.

Slide 14

Slide 14 text

@GanbaroDigital Reuse Challenges • Side effects • High coupling • Bitrot / fragility to change

Slide 15

Slide 15 text

@GanbaroDigital Classes and objects normally* wrap state in behaviour

Slide 16

Slide 16 text

@GanbaroDigital Object

Slide 17

Slide 17 text

@GanbaroDigital Object Data

Slide 18

Slide 18 text

@GanbaroDigital Object Data Methods

Slide 19

Slide 19 text

@GanbaroDigital Object Data Methods

Slide 20

Slide 20 text

@GanbaroDigital Object Data Methods

Slide 21

Slide 21 text

@GanbaroDigital Object Data Methods

Slide 22

Slide 22 text

@GanbaroDigital That’s not the whole picture.

Slide 23

Slide 23 text

@GanbaroDigital Object Data Methods

Slide 24

Slide 24 text

@GanbaroDigital Object

Slide 25

Slide 25 text

@GanbaroDigital Object

Slide 26

Slide 26 text

@GanbaroDigital Object

Slide 27

Slide 27 text

@GanbaroDigital Object

Slide 28

Slide 28 text

@GanbaroDigital Object Data Methods

Slide 29

Slide 29 text

@GanbaroDigital What We Can See What We Can’t See

Slide 30

Slide 30 text

@GanbaroDigital What We Can See What We Can’t See

Slide 31

Slide 31 text

@GanbaroDigital What We Can See What We Can’t See

Slide 32

Slide 32 text

@GanbaroDigital “ When there’s high-coupling, there’s high sensitivity to change.

Slide 33

Slide 33 text

@GanbaroDigital “A change in one of the things we can’t see can break the one thing we can see.

Slide 34

Slide 34 text

@GanbaroDigital ?? ?? Do we notice the breakage a) at all? b) before we ship?

Slide 35

Slide 35 text

@GanbaroDigital ?? ?? How long does it take to find the cause of the breakage?

Slide 36

Slide 36 text

@GanbaroDigital “ It’s the nature of OOP to create highly-coupled code.

Slide 37

Slide 37 text

@GanbaroDigital “ Breaking large objects / methods into smaller ones does nothing to reduce coupling.

Slide 38

Slide 38 text

@GanbaroDigital That’s why I’m interested in other programming paradigms to help me on my quest.

Slide 39

Slide 39 text

@GanbaroDigital Your quest will be different, and just as valid. I’m here to learn from you too!

Slide 40

Slide 40 text

@GanbaroDigital Railway-Oriented Programming

Slide 41

Slide 41 text

@GanbaroDigital Scott Wlaschin
 https://fsharpforfunandprofit.com/

Slide 42

Slide 42 text

@GanbaroDigital https://vimeo.com/113707214

Slide 43

Slide 43 text

@GanbaroDigital What Is Railway-Oriented Programming?

Slide 44

Slide 44 text

@GanbaroDigital

Slide 45

Slide 45 text

@GanbaroDigital input

Slide 46

Slide 46 text

@GanbaroDigital input output

Slide 47

Slide 47 text

@GanbaroDigital “The Tunnel of Transformation” input output

Slide 48

Slide 48 text

@GanbaroDigital strtoupper()

Slide 49

Slide 49 text

@GanbaroDigital strtoupper()

Slide 50

Slide 50 text

@GanbaroDigital strtoupper() string

Slide 51

Slide 51 text

@GanbaroDigital strtoupper() string string

Slide 52

Slide 52 text

@GanbaroDigital strtoupper() apple

Slide 53

Slide 53 text

@GanbaroDigital strtoupper() apple APPLE

Slide 54

Slide 54 text

@GanbaroDigital “ One thing goes in, a new thing comes out. The thing that went in remains unchanged.

Slide 55

Slide 55 text

@GanbaroDigital

Slide 56

Slide 56 text

@GanbaroDigital

Slide 57

Slide 57 text

@GanbaroDigital

Slide 58

Slide 58 text

@GanbaroDigital

Slide 59

Slide 59 text

@GanbaroDigital { Composability

Slide 60

Slide 60 text

@GanbaroDigital Scott’s interest in ROP started from error handling.

Slide 61

Slide 61 text

@GanbaroDigital ?? ?? What happens when things go wrong?

Slide 62

Slide 62 text

@GanbaroDigital validateEmail() string string

Slide 63

Slide 63 text

@GanbaroDigital validateEmail() string string error

Slide 64

Slide 64 text

@GanbaroDigital We could throw an Exception instead.

Slide 65

Slide 65 text

@GanbaroDigital https://flic.kr/p/fArumY Pyramid of Dooom!

Slide 66

Slide 66 text

@GanbaroDigital public function validateEmail($email) { if (preg_match(..., $email)) { if (not_blacklisted($email)) { if (mailbox_exists($email)) { return $email; } } } throw new Exception(...); }

Slide 67

Slide 67 text

@GanbaroDigital public function validateEmail($email) { if (preg_match(..., $email)) { if (not_blacklisted($email)) { if (mailbox_exists($email)) { return $email; } } } throw new Exception(...); }

Slide 68

Slide 68 text

@GanbaroDigital All Joking Aside ... • Nested ‘if’ adds testing complexity • Becomes fragile over time, as rules change

Slide 69

Slide 69 text

@GanbaroDigital All Joking Aside ... • Nested ‘if’ adds testing complexity • Becomes fragile over time, as rules change

Slide 70

Slide 70 text

@GanbaroDigital Can we replace the Pyramid of Doom with ROP?

Slide 71

Slide 71 text

@GanbaroDigital public function validateEmail($email) { if (preg_match(..., $email)) { if (not_blacklisted($email)) { if (mailbox_exists($email)) { return $email; } } } throw new Exception(...); }

Slide 72

Slide 72 text

@GanbaroDigital public function validateEmail($email) { if (preg_match(..., $email)) { if (not_blacklisted($email)) { if (mailbox_exists($email)) { return $email; } } } throw new Exception(...); }

Slide 73

Slide 73 text

@GanbaroDigital emailFormat() notBlacklisted() mailboxExists()

Slide 74

Slide 74 text

@GanbaroDigital emailFormat() notBlacklisted() mailboxExists()

Slide 75

Slide 75 text

@GanbaroDigital emailFormat() notBlacklisted() mailboxExists()

Slide 76

Slide 76 text

@GanbaroDigital emailFormat() notBlacklisted() mailboxExists()

Slide 77

Slide 77 text

@GanbaroDigital These 1 in, 2 out functions aren’t composable.

Slide 78

Slide 78 text

@GanbaroDigital emailFormat() string string error

Slide 79

Slide 79 text

@GanbaroDigital ?? ?? How would you fix that?

Slide 80

Slide 80 text

@GanbaroDigital emailFormat() string string error error

Slide 81

Slide 81 text

@GanbaroDigital emailFormat() notBlacklisted() mailboxExists()

Slide 82

Slide 82 text

@GanbaroDigital emailFormat() notBlacklisted() mailboxExists()

Slide 83

Slide 83 text

@GanbaroDigital We start off without an error. Conceptually, the error parameter is optional.

Slide 84

Slide 84 text

@GanbaroDigital validateEmail() is built from functions composed together.

Slide 85

Slide 85 text

@GanbaroDigital validateEmail() string string

Slide 86

Slide 86 text

@GanbaroDigital validateEmail() string string

Slide 87

Slide 87 text

@GanbaroDigital ?? ?? Who decides to stop calling the function chain?

Slide 88

Slide 88 text

@GanbaroDigital emailFormat() notBlacklisted() mailboxExists()

Slide 89

Slide 89 text

@GanbaroDigital I hate submitting a web form, and getting back 1 error at a time

Slide 90

Slide 90 text

@GanbaroDigital “ Because there are no side-effects we can safely run the whole pipeline where that makes sense.

Slide 91

Slide 91 text

@GanbaroDigital emailFormat() notBlacklisted() mailboxExists()

Slide 92

Slide 92 text

@GanbaroDigital ROP Input Parameters • Primary data parameter • Optional existing error(s) parameter

Slide 93

Slide 93 text

@GanbaroDigital ROP Function Actions • Apply logic to input data • Produce new output data • Input data remains unchanged

Slide 94

Slide 94 text

@GanbaroDigital ROP Function Outputs • The (possibly transformed) data • (Aggregated) error reporting

Slide 95

Slide 95 text

@GanbaroDigital ROP Function Composition • Maybe we want to short-circuit on error • Maybe we want to aggregate errors • This is policy - it belongs in the calling code

Slide 96

Slide 96 text

@GanbaroDigital ?? ?? What would ROP look like in PHP?

Slide 97

Slide 97 text

@GanbaroDigital PHP is an imperative language. It lacks things that functional programmers take for granted.

Slide 98

Slide 98 text

@GanbaroDigital FL Advantages • Type system • Compile-time checks* • Monads & monoids

Slide 99

Slide 99 text

@GanbaroDigital ?? ?? Should we simply emulate monads in PHP?

Slide 100

Slide 100 text

@GanbaroDigital We can emulate the flow logic of Monads. We can’t emulate the type system that makes them practical.

Slide 101

Slide 101 text

@GanbaroDigital Let’s revisit this question after the code demos.

Slide 102

Slide 102 text

@GanbaroDigital ROP in PHP

Slide 103

Slide 103 text

@GanbaroDigital Starting Requirements • Data input, optional error input • Data output, error output • Input data treated as immutable

Slide 104

Slide 104 text

@GanbaroDigital Design Constraints • Functions must be composable • Short-circuit logic sits outside composed functions

Slide 105

Slide 105 text

@GanbaroDigital https://flic.kr/p/cwK7UN

Slide 106

Slide 106 text

@GanbaroDigital Measured Outcomes • Reasonability • Reusability • Robustness

Slide 107

Slide 107 text

@GanbaroDigital Example 1: Simple ROP Functions

Slide 108

Slide 108 text

@GanbaroDigital

Slide 109

Slide 109 text

@GanbaroDigital

Slide 110

Slide 110 text

@GanbaroDigital

Slide 111

Slide 111 text

@GanbaroDigital

Slide 112

Slide 112 text

@GanbaroDigital

Slide 113

Slide 113 text

@GanbaroDigital

Slide 114

Slide 114 text

@GanbaroDigital This is the simplest solution I’ve been able to find to date.

Slide 115

Slide 115 text

@GanbaroDigital The Simple Approach • Arg 1 - always the data to transform • Arg 2 - optional failure data • Returns 2-element array

Slide 116

Slide 116 text

@GanbaroDigital Why Return An Array? • Arrays outperform objects in PHP • No need to define any classes, or pull in any third-party packages

Slide 117

Slide 117 text

@GanbaroDigital The output of one ROP function becomes the input of the next one.

Slide 118

Slide 118 text

@GanbaroDigital

Slide 119

Slide 119 text

@GanbaroDigital Use the ‘...’ token to compose the ROP functions.

Slide 120

Slide 120 text

@GanbaroDigital

Slide 121

Slide 121 text

@GanbaroDigital

Slide 122

Slide 122 text

@GanbaroDigital The ‘...’ token allows us to nest calls ...

Slide 123

Slide 123 text

@GanbaroDigital

Slide 124

Slide 124 text

@GanbaroDigital ... but readability suffers* as the nesting deepens 
 * ends up looking functional

Slide 125

Slide 125 text

@GanbaroDigital

Slide 126

Slide 126 text

@GanbaroDigital “ Most people find linear code easier to read.

Slide 127

Slide 127 text

@GanbaroDigital

Slide 128

Slide 128 text

@GanbaroDigital

Slide 129

Slide 129 text

@GanbaroDigital Reasonability: Reusability: Robustness: ✓

Slide 130

Slide 130 text

@GanbaroDigital “ Standardisation encourages interoperability and reusability.

Slide 131

Slide 131 text

@GanbaroDigital “ A standardised approach allows us to reuse logic to create new logic.

Slide 132

Slide 132 text

@GanbaroDigital

Slide 133

Slide 133 text

@GanbaroDigital

Slide 134

Slide 134 text

@GanbaroDigital Reasonability: Reusability: Robustness: ✓ ½

Slide 135

Slide 135 text

@GanbaroDigital Some Observations • Failure is currently a placeholder • Return values aren’t typed

Slide 136

Slide 136 text

@GanbaroDigital We’ll come back to handling failure shortly.

Slide 137

Slide 137 text

@GanbaroDigital Some Observations • Failure is currently a placeholder • Return values aren’t typed

Slide 138

Slide 138 text

@GanbaroDigital ?? ?? How do we feel about the return types?

Slide 139

Slide 139 text

@GanbaroDigital

Slide 140

Slide 140 text

@GanbaroDigital Example 2: Business Domain

Slide 141

Slide 141 text

@GanbaroDigital Let’s model a very simple* e-commerce order.

Slide 142

Slide 142 text

@GanbaroDigital

Slide 143

Slide 143 text

@GanbaroDigital

Slide 144

Slide 144 text

@GanbaroDigital

Slide 145

Slide 145 text

@GanbaroDigital

Slide 146

Slide 146 text

@GanbaroDigital

Slide 147

Slide 147 text

@GanbaroDigital

Slide 148

Slide 148 text

@GanbaroDigital

Slide 149

Slide 149 text

@GanbaroDigital “ The business model is logic that is applied to the data model.

Slide 150

Slide 150 text

@GanbaroDigital “Separate the business model from the data model for flexibility and long-term stability.

Slide 151

Slide 151 text

@GanbaroDigital

Slide 152

Slide 152 text

@GanbaroDigital

Slide 153

Slide 153 text

@GanbaroDigital Both of our example functions return new Orders. The original Order is left unchanged.

Slide 154

Slide 154 text

@GanbaroDigital If anything goes wrong*, we still have the original Order. * and it will :-)

Slide 155

Slide 155 text

@GanbaroDigital ?? ?? How do we make these functions composable?

Slide 156

Slide 156 text

@GanbaroDigital

Slide 157

Slide 157 text

@GanbaroDigital

Slide 158

Slide 158 text

@GanbaroDigital

Slide 159

Slide 159 text

@GanbaroDigital Having standalone functions that you then wrap can be easier to unit test.

Slide 160

Slide 160 text

@GanbaroDigital

Slide 161

Slide 161 text

@GanbaroDigital

Slide 162

Slide 162 text

@GanbaroDigital Use a function that returns a function to make things composable AND avoid hard-coding parameters.

Slide 163

Slide 163 text

@GanbaroDigital

Slide 164

Slide 164 text

@GanbaroDigital

Slide 165

Slide 165 text

@GanbaroDigital

Slide 166

Slide 166 text

@GanbaroDigital

Slide 167

Slide 167 text

@GanbaroDigital ?? ?? How do you feel about lambda functions in PHP?

Slide 168

Slide 168 text

@GanbaroDigital Rule of thumb: any function that takes 1 parameter and isn’t built from composed functions assume it has something hard-coded in there until you prove otherwise!

Slide 169

Slide 169 text

@GanbaroDigital

Slide 170

Slide 170 text

@GanbaroDigital

Slide 171

Slide 171 text

@GanbaroDigital

Slide 172

Slide 172 text

@GanbaroDigital

Slide 173

Slide 173 text

@GanbaroDigital

Slide 174

Slide 174 text

@GanbaroDigital

Slide 175

Slide 175 text

@GanbaroDigital

Slide 176

Slide 176 text

@GanbaroDigital

Slide 177

Slide 177 text

@GanbaroDigital With the ROP approach, you might end up building a lot of lambda functions.

Slide 178

Slide 178 text

@GanbaroDigital These builders are creating partial functions.

Slide 179

Slide 179 text

@GanbaroDigital We can create a reusable partial function builder.

Slide 180

Slide 180 text

@GanbaroDigital

Slide 181

Slide 181 text

@GanbaroDigital Partial Function Builder • Reduces amount of code to write • Requires main data to be first parameter of the wrapped function • Convenience over runtime performance • No type-safety

Slide 182

Slide 182 text

@GanbaroDigital Partial Function Builder • Reduces amount of code to write • Requires main data to be first parameter of the wrapped function • Convenience over runtime performance • No type-safety

Slide 183

Slide 183 text

@GanbaroDigital Partial Function Builder • Reduces amount of code to write • Requires main data to be first parameter of the wrapped function • Convenience over runtime performance • No type-safety

Slide 184

Slide 184 text

@GanbaroDigital Partial Function Builder • Reduces amount of code to write • Requires main data to be first parameter of the wrapped function • Convenience over runtime performance • No type-safety

Slide 185

Slide 185 text

@GanbaroDigital “ A standardised business domain can adapt to change.

Slide 186

Slide 186 text

@GanbaroDigital Let’s add discount codes to our worked example.

Slide 187

Slide 187 text

@GanbaroDigital Requirements • Applied after all other costs • Show the discount to make the customer feel the value

Slide 188

Slide 188 text

@GanbaroDigital

Slide 189

Slide 189 text

@GanbaroDigital Requirements • Applied after all other costs • Show the discount to make the customer feel the value

Slide 190

Slide 190 text

@GanbaroDigital

Slide 191

Slide 191 text

@GanbaroDigital

Slide 192

Slide 192 text

@GanbaroDigital A purely imperative approach is readable but has a larger change surface.

Slide 193

Slide 193 text

@GanbaroDigital

Slide 194

Slide 194 text

@GanbaroDigital A composable / ROP approach has a smaller change surface but requires more mental space.

Slide 195

Slide 195 text

@GanbaroDigital

Slide 196

Slide 196 text

@GanbaroDigital Introducing discounts hasn’t touched our gross calculation at all!

Slide 197

Slide 197 text

@GanbaroDigital The composable / ROP approach is very suited to switching business logic at runtime.

Slide 198

Slide 198 text

@GanbaroDigital It’s one way to adopt the DDD “specifications” concept.

Slide 199

Slide 199 text

@GanbaroDigital Reasonability: Reusability: Robustness: ✓ ¾

Slide 200

Slide 200 text

@GanbaroDigital Scott talked about ROP as an approach to error handling. We can’t put it off any longer :-)

Slide 201

Slide 201 text

@GanbaroDigital Example 3: Failure

Slide 202

Slide 202 text

@GanbaroDigital

Slide 203

Slide 203 text

@GanbaroDigital

Slide 204

Slide 204 text

@GanbaroDigital

Slide 205

Slide 205 text

@GanbaroDigital

Slide 206

Slide 206 text

@GanbaroDigital

Slide 207

Slide 207 text

@GanbaroDigital

Slide 208

Slide 208 text

@GanbaroDigital ?? ?? How can we handle the missing VAT code?

Slide 209

Slide 209 text

@GanbaroDigital

Slide 210

Slide 210 text

@GanbaroDigital get_vat_rate() silently hides an error from elsewhere

Slide 211

Slide 211 text

@GanbaroDigital get_vat_rate() is propagating an error that we cannot see

Slide 212

Slide 212 text

@GanbaroDigital ?? ?? How can we handle the missing VAT code?

Slide 213

Slide 213 text

@GanbaroDigital Option 1: throw an exception

Slide 214

Slide 214 text

@GanbaroDigital

Slide 215

Slide 215 text

@GanbaroDigital

Slide 216

Slide 216 text

@GanbaroDigital InvalidArgumentException is one of those things I wish we could uninvent.

Slide 217

Slide 217 text

@GanbaroDigital Option 1 Consequences • We don’t need $failure at all (not ROP!) • Generic exceptions are a huge time sink when investigating faults • Specific exceptions lead to large try/catch blocks* • try/catch blocks are part of our change surface area - Pryamid of Doom / fragile

Slide 218

Slide 218 text

@GanbaroDigital Option 1 Consequences • We don’t need $failure at all (not ROP!) • Generic exceptions are a huge time sink when investigating faults • Specific exceptions lead to large try/catch blocks* • try/catch blocks are part of our change surface area - Pryamid of Doom / fragile

Slide 219

Slide 219 text

@GanbaroDigital Option 1 Consequences • We don’t need $failure at all (not ROP!) • Generic exceptions are a huge time sink when investigating faults • Specific exceptions lead to large try/catch blocks* • try/catch blocks are part of our change surface area - Pryamid of Doom / fragile

Slide 220

Slide 220 text

@GanbaroDigital Option 1 Consequences • We don’t need $failure at all (not ROP!) • Generic exceptions are a huge time sink when investigating faults • Specific exceptions lead to large try/catch blocks* • try/catch blocks are part of our change surface area - Pryamid of Doom / fragile

Slide 221

Slide 221 text

@GanbaroDigital Option 2: return the error as $failure

Slide 222

Slide 222 text

@GanbaroDigital rop_get_vat_rate() string float error error

Slide 223

Slide 223 text

@GanbaroDigital

Slide 224

Slide 224 text

@GanbaroDigital

Slide 225

Slide 225 text

@GanbaroDigital

Slide 226

Slide 226 text

@GanbaroDigital Option 2 Consequences • Everything has to agree on what $failure is, and how to use it • Still need to return *something* as main data out • Someone needs to remember to check $failure

Slide 227

Slide 227 text

@GanbaroDigital Option 2 Consequences • Everything has to agree on what $failure is, and how to use it • Still need to return *something* as main data out • Someone needs to remember to check $failure

Slide 228

Slide 228 text

@GanbaroDigital Option 2 Consequences • Everything has to agree on what $failure is, and how to use it • Still need to return *something* as main data out • Someone needs to remember to check $failure

Slide 229

Slide 229 text

@GanbaroDigital Option 1 is effort and fragile. (And it’s the current way) Option 2 requires 100% accuracy from humans.

Slide 230

Slide 230 text

@GanbaroDigital ?? ?? What if the caller tells us how to handle failure?

Slide 231

Slide 231 text

@GanbaroDigital

Slide 232

Slide 232 text

@GanbaroDigital

Slide 233

Slide 233 text

@GanbaroDigital

Slide 234

Slide 234 text

@GanbaroDigital

Slide 235

Slide 235 text

@GanbaroDigital

Slide 236

Slide 236 text

@GanbaroDigital

Slide 237

Slide 237 text

@GanbaroDigital rop_get_vat_rate() string float error error

Slide 238

Slide 238 text

@GanbaroDigital rop_get_vat_rate() string float error handler error handler

Slide 239

Slide 239 text

@GanbaroDigital input output error handler error handler “The Tunnel of Transformation”

Slide 240

Slide 240 text

@GanbaroDigital Option 3 Consequences • Everything still has to agree on what $failure is, and how to use it • 2nd parameter is non-optional callback • Caller decides how to handle errors • Caller can throw exceptions if preferred

Slide 241

Slide 241 text

@GanbaroDigital Option 3 Consequences • Everything still has to agree on what $failure is, and how to use it • 2nd parameter is non-optional callback • Caller decides how to handle errors • Caller can throw exceptions if preferred

Slide 242

Slide 242 text

@GanbaroDigital Option 3 Consequences • Everything still has to agree on what $failure is, and how to use it • 2nd parameter is non-optional callback • Caller decides how to handle errors • Caller can throw exceptions if preferred

Slide 243

Slide 243 text

@GanbaroDigital Option 3 Consequences • Everything still has to agree on what $failure is, and how to use it • 2nd parameter is non-optional callback • Caller decides how to handle errors • Caller can throw exceptions if preferred

Slide 244

Slide 244 text

@GanbaroDigital Our partial function builder now knows what to expect for the second parameter.

Slide 245

Slide 245 text

@GanbaroDigital

Slide 246

Slide 246 text

@GanbaroDigital

Slide 247

Slide 247 text

@GanbaroDigital Reasonability: Reusability: Robustness: ✓ ¾ ✓

Slide 248

Slide 248 text

@GanbaroDigital ?? ?? What about our reusability score?

Slide 249

Slide 249 text

@GanbaroDigital The error handler is provided at the point of use not at the point of definition.

Slide 250

Slide 250 text

@GanbaroDigital

Slide 251

Slide 251 text

@GanbaroDigital Reasonability: Reusability: Robustness: ✓ ✓ ✓

Slide 252

Slide 252 text

@GanbaroDigital

Slide 253

Slide 253 text

Thank You Any Questions? A presentation by @stuherbert
 for @GanbaroDigital