You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: _drafts/conditional-to-calculation.md
+3-2Lines changed: 3 additions & 2 deletions
Original file line number
Diff line number
Diff line change
@@ -1,8 +1,9 @@
1
1
---
2
-
layout: post
3
2
title: Replace Conditional With Calculation
4
-
tags:
3
+
published: false
4
+
layout: default
5
5
---
6
+
# {{ page.title }}
6
7
7
8
Among the many refactorings described in Martin Fowler’s Refactoring, "Replace Conditional with Polymorphism" eliminates branching control flow by replacing it with polymorphic calls. An alternative is to replace branching code with straight-line calculations. This is frequently used in graphics and games programming. Unsurprisingly, I call this refactoring "Replace Conditional with Calculation".
Java programmers apparently find it very difficult to create objects. Huge amount of engineering effort has been spent writing (and using) frameworks for creating objects: dependency injection frameworks, builders, annotation-driven code generators, etc., etc.
9
+
10
+
When using Kotlin on the JVM, you can still use all those tools, but there is less need for them (and, I’d argue, less need in modern Java also). In this series, we look at the various tools Java programmers use to create objects, the Kotlin alternatives, and how to refactor from one to the other.
11
+
12
+
1. Builders
13
+
2. Test-Data Builders
14
+
3. Distinguishing between Values & Stateful Objects
For a _Multiple Commit_ refactoring, the time it takes to merge the change to trunk is a limiting factor. The longer it takes to get the change merged to trunk, the longer it will take for improvements to uplift the rest of the team. The most effective strategy for sharing refactorings is trunk-based development, but if using pull requests then keep the team needs to ensure that reviews are very fast, in minutes, and report on correctness, compliance and legal issues, not style preferences.
We refactor to extract duplicate logic into extension methods. But refactoring to extension methods _itself_ leads to duplication.
9
+
10
+
Here we have lots of methods that extract a value from a row of a database query result set.
11
+
12
+
```kotlin
13
+
fun ResultSet.getOffsetDateTime(name:String): OffsetDateTime=...
14
+
fun ResultSet.getPostCode(name:String): PostCode=...
15
+
fun ResultSet.getTelephoneNumber(name:String): TelephoneNumber=...
16
+
fun ResultSet.getEmailAddress(name:String): EmailAddress=...
17
+
```
18
+
19
+
And because columns can be nullable, we also have:
20
+
21
+
```kotlin
22
+
fun ResultSet.getOffsetDateTimeOrNull(name:String): OffsetDateTime?=...
23
+
fun ResultSet.getPostCodeOrNull(name:String): PostCode?=...
24
+
fun ResultSet.getTelephoneNumberOrNull(name:String): TelephoneNumber?=...
25
+
fun ResultSet.getEmailAddressOrNull(name:String): EmailAddress?=...
26
+
```
27
+
28
+
Each extension has very similar logic:
29
+
30
+
```kotlin
31
+
fun ResultSet.getPostCodeOrNull(name:String): PostCode? {
32
+
val strValue:String?= getString(name);
33
+
if (wasNull()) {
34
+
returnnull
35
+
} else {
36
+
val convertedValue =PostCode.parse(strValue!!)
37
+
return convertedValue
38
+
?:throwSqlConversionError("could not parse column $name as a PostCode")
39
+
}
40
+
}
41
+
42
+
fun ResultSet.getPostCode(name:String) =
43
+
getPostCodeOrNull(name)
44
+
?:throwSqlConversionError("column $name was null, expected non-null")
45
+
```
46
+
47
+
48
+
(Note: we are using exceptions for error handling here, for clarity. In practice we would probably use a Result or Either type to represent either a successful conversion or a failure.)
49
+
50
+
And you address that by introducing a polymorphic parameter that embodies the variation obvious in the the extension function names (String, Date, Money, …)
51
+
52
+
Then you identify groups of parameters that you always pass around together, and can factor those out into a higher level abstraction that is easier to pass around and compose into yet higher-level abstractions.
53
+
In this case, name and converter are tightly related for any result set. So you might want to combine them into a ResultColumn abstraction.
54
+
55
+
```kotlin
56
+
data classResultColumn<T>(name:String, toKotlin: (ResultSet,String)->Result<T,ConversionError>)
57
+
```
58
+
59
+
And then you could compose ResultColumns to define converters for structured types, or the structure of your result sets.
In test code, Java programmers use builders for a different reason.
11
+
Here the goal of the builder is to create objects pre-initialised with safe defaults for testing, and allow tests to specify the values only of those properties relevant to the test scenario.
12
+
13
+
In our Travelator test modules we write static factory methods that create test data builders:
Our tests import these factory methods statically, so the code reads as a clear explanation of the test scenario.
33
+
For example, a test that needs a ferry booking request with a campervan need only specify that the booking has a vehicle with type `CAMPERVAN`.
34
+
The rest of the booking properties can be safely left as the default, so it is clear to the reader which properties affect the outcome of this test scenario and which are irrelevant.
<2> We collect the values we will use to create the FerryBookingRequest. The methods of the builder return a builder to the caller so that building the object can be done in a single expression of chained method calls. Although it _looks_ like a calculation, implementations usually rely on side effects -- the methods of the builder mutate its state and return `this`.
39
+
<3> We can use other builders to create sub-objects.
40
+
<4> We call `build()` to construct the FerryBookingRequest.
41
+
42
+
13
43
The authors of <<GHJV_DPEOROOS_1994,_Design Patterns: Elements of Reusable Object-Oriented Software_>> describe the intent of the Builder pattern as:
14
44
15
45
> Separate the construction of a complex object from its representation so that the same construction process can create different representations.
@@ -29,7 +59,6 @@ Java programmers do not use builders to separate construction from representatio
29
59
Firstly, Java methods do not have named parameters.
30
60
This means the code of a constructor call does not make explicit how it is initialising the properties of the object with the values passed to the constructor.
31
61
32
-
For example, here is a method that builds a FerryBookingRequest from a posted web form:
We can now clearly see which properties the code initialises.
112
-
However, this style constructs an object in an invalid state -- the no-argument constructor initialises object references stored by the object to null -- and the type system cannot guarantee that our code puts the object into a valid state before we use it.
113
-
If we forget to set all the necessary properties, our code will still compile but methods of the object will fail at runtime with a NullPointerException.
141
+
However, this style constructs an object in an invalid state: the no-argument constructor initialises object references stored by the object to null. The type system cannot guarantee that our code puts the object into a valid state before we use it.
142
+
If we forget to set all the necessary properties, our code will still compile, but methods of the object will fail at runtime with a NullPointerException.
114
143
For example, how easily did you notice that the call to `request.getVehicles().add(vehicle)` above will fail, because the no-arg constructor leaves the vehicles property set to a null reference, instead of initialising it to an empty list?
115
144
116
-
Even if we construct the object correctly, Bean-style initialisation
117
-
If someone -- maybe even ourselves -- later adds a property to the class and doesn't change our code to match,
145
+
Even if we construct the object correctly with bean-style initialisation,
146
+
if someone -- maybe even ourselves -- adds a property to the class at a later date
147
+
and doesn't change our code to match,
118
148
the code will continue to compile but now leave the object partially initialised.
119
-
Calls to the object will fail, often far -- in space and time -- from the construction code that is the source of the bug.
120
-
You've got to have very thorough test coverage of integrated code to have a good chance of catching these problems.
149
+
Calls to the object will fail, often far – in space and time – from the construction code that is the source of the bug.
150
+
You need thorough test coverage of integrated code to have a good chance of catching these problems.
121
151
122
-
Another annoyance of Bean-style initialisation is that we need to write imperative code to construct an object.
152
+
Another annoyance of Bean-style initialisation is that we must write imperative code to construct an object.
123
153
We cannot use this style of code to create immutable objects.
124
154
We have to declare local variables to hold partially constructed objects and write statements to connect our objects together.
125
155
Because the construction code is linear, it does not portray the structure of objects it is creating.
@@ -128,35 +158,10 @@ The larger the object graph, the more we want our code to portray the structure
128
158
That's what builders solve for Java programmers.
129
159
You can write expressions with builders that mirror the shape of the object graphs being built (unlike beans) _and_ the expressions show how they initialise the objects' properties (unlike constructors).
130
160
131
-
Here's what that object graph would look like if we construct it with builders:
Builders combine some benefits of constructor calls with some benefits of Java Bean conventions.
163
+
The build method can fail if any properties are missing, so that invalid objects are reported in the code that creates the object, rather than distant code that uses it. Not as good as a type-safe constructor call, but better than Bean conventions.
140
164
141
-
for (var i =1; form.contains("vehicle_"+ i); i++) {
<2> We collect the values we will use to create the FerryBookingRequest. The methods of the builder make it clear which properties are being set, so we are unlikely to initialise a property from the wrong form field (as long as the form fields are sensibly named). The methods start with the prefix "with", to distinguish them from bean setters, and return the builder so that the building of the object can be done in a single expression of chained method calls.
156
-
<3> We can use other builders to create sub-objects.
157
-
<4> We call `build()` to construct the FerryBookingRequest. Can fail in this method if any properties are missing, so that invalid objects are reported in the buggy code that creates the object, rather than distant code that uses it. Not as good as a type-safe constructor call, but better than Bean conventions.
158
-
159
-
So, code using builders combines benefits of constructor calls and of JavaBean conventions.
160
165
161
166
What does it take to write the builder itself?
162
167
@@ -206,64 +211,15 @@ class FerryBookingRequestBuilder implements travelator.Builder<FerryBookingReque
206
211
That's quite a lot of boilerplate code!
207
212
208
213
Yet Java programmers clearly find builders to be worth the effort.
209
-
Lots of open source libraries and even the standard library now provide builders for their classes.
210
-
211
-
## Test Data Builders
214
+
Lots of open source libraries and even the standard library now provide builders for their classes, and the Lombok compiler plugin can generate builders for classes that have been annotated with Lombok's `@Builder` annotation.
212
215
213
-
In test code, Java programmers use builders for a different reason.
214
-
Here the goal of the builder is to create objects pre-initialised with safe defaults for testing, and allow tests to specify the values only of those properties relevant to the test scenario.
215
-
216
-
In our Travelator test modules we write static factory methods that create test data builders:
Our tests import these factory methods statically, so the code reads as a clear explanation of the test scenario.
236
-
For example, a test that needs a ferry booking request with a campervan need only specify that the booking has a vehicle with type `CAMPERVAN`.
237
-
The rest of the booking properties can be safely left as the default, so it is clear to the reader which properties affect the outcome of this test scenario and which are irrelevant.
238
-
239
-
```java
240
-
publicclassFerryBookingTest {
241
-
publicvoidbooking_with_one_campervan() {
242
-
var request = aFerryBooking()
243
-
.withVehicle(aVehicle()
244
-
.withType(CAMPERVAN)
245
-
.build())
246
-
.build();
247
-
248
-
// ... use the request in the test
249
-
}
250
-
}
251
-
```
252
216
253
217
## Kotlin's alternatives to builders
254
218
255
219
What alternatives does Kotlin offer?
256
220
257
221
Named parameters: code that constructs object graphs can be readable.
258
222
259
-
Apply function: Imperative code in a block, caller can treat it as an expression.
260
-
261
-
Data classes: copy instead of mutate, constants instead of builders.
262
-
263
-
264
-
## Refactor from Builders to immutable data
265
-
266
-
267
-
268
-
## Moving On
223
+
The `apply` and `also` scope functions: you can insert a block of imperative initialisation code into an expression that constructs an object.
269
224
225
+
Data classes: you can use a constant values of a data class that you modify by calling their `copy` method instead of writing (or generating) a separate builder class.
0 commit comments