Skip to content

Commit 942bf7b

Browse files
committed
Publish "Refactoring Beyond the Commit"
1 parent 6ad4ea7 commit 942bf7b

11 files changed

+179
-114
lines changed

.idea/java-to-kotlin-website.iml

Lines changed: 0 additions & 9 deletions
This file was deleted.

.idea/misc.xml

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/modules.xml

Lines changed: 0 additions & 8 deletions
This file was deleted.

_drafts/2024-05-25-refactoring-conditional-to-calculation.md renamed to _drafts/conditional-to-calculation.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
---
2-
layout: post
32
title: Replace Conditional With Calculation
4-
tags:
3+
published: false
4+
layout: default
55
---
6+
# {{ page.title }}
67

78
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".
89

_drafts/creating-objects.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
title: Builders to Named Parameters
3+
layout: default
4+
published: false
5+
---
6+
# {{page.title}}
7+
8+
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
15+
4. Dependency Injection Frameworks
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
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.
2+
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
---
2+
title: Extensions to Polymorphic Parameters
3+
layout: default
4+
published: false
5+
---
6+
# {{page.title}}
7+
8+
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+
return null
35+
} else {
36+
val convertedValue = PostCode.parse(strValue!!)
37+
return convertedValue
38+
?: throw SqlConversionError("could not parse column $name as a PostCode")
39+
}
40+
}
41+
42+
fun ResultSet.getPostCode(name: String) =
43+
getPostCodeOrNull(name)
44+
?: throw SqlConversionError("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 class ResultColumn<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.

_drafts/factories-to-closures.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
----
2+
title: Factories to Closures
3+
layout: default
4+
published: false
5+
----
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
---
2+
title: Test Data Builders to Constants
3+
layout: default
4+
published: false
5+
---
6+
# {{page.title}}
7+
8+
## Test Data Builders
9+
10+
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:
14+
15+
```java
16+
public class BookingExamples {
17+
public static FerryBookingRequestBuilder aFerryBooking() {
18+
return new FerryBookingRequestBuilder()
19+
.withProviderServiceId("example-service")
20+
.withNumberOfBerths(2)
21+
.withCabinClass("standard");
22+
}
23+
24+
public static VehicleDetailsBuilder aVehicle() {
25+
return new VehicleDetailsBuilder()
26+
.withType(CAR)
27+
.withRegistration("CPL593H");
28+
}
29+
}
30+
```
31+
32+
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.
35+
36+
```java
37+
public class FerryBookingTest {
38+
public void booking_with_one_campervan() {
39+
var request = aFerryBooking()
40+
.withVehicle(aVehicle()
41+
.withType(CAMPERVAN)
42+
.build())
43+
.build();
44+
45+
// ... use the request in the test
46+
}
47+
}
48+
```
49+

_journeys/builders-to-constants.md

Lines changed: 0 additions & 6 deletions
This file was deleted.

_journeys/builders-to-named-parameters.md

Lines changed: 44 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,41 @@ published: false
55
---
66
# {{page.title}}
77

8-
A recent newcomer to Java might be struck by the way libraries provide "builders" to help programmers construct objects.
8+
A recent newcomer to Java might be struck by how many libraries provide "builders" to help programmers construct objects.
99
What do Java programmers use builders for? And what does Kotlin do differently for those use cases?
1010

1111
## Builders in Java
1212

13+
Let’s take a look at a typical use of builders in Java code…
14+
For example, here is a method that builds a FerryBookingRequest from a posted web form:
15+
16+
```java
17+
public FerryBookingRequest formToBookingRequest(Form form, Trip trip) {
18+
FerryBookingRequestBuilder builder = new FerryBookingRequestBuilder() // <1>
19+
.withProviderServiceId(form.get("service_id")) // <2>
20+
.withNumberOfBerths(
21+
form.get("berths", asInt, trip::getTravellerCount))
22+
.withCabinClass(form.get("cabin_class"));
23+
24+
for (var i = 1; form.contains("vehicle_" + i); i++) {
25+
builder.withVehicle(new VehicleDetailsBuilder() // <3>
26+
.withType(
27+
form.get("vehicle_" + i + "_type",
28+
asEnum(VehicleType.class)))
29+
.withRegistration(
30+
form.get("vehicle_" + i + "_registration"))
31+
.build()); // <4>
32+
}
33+
return builder.build(); // <4>
34+
}
35+
```
36+
37+
<1> We create a builder for a FerryBookingRequest
38+
<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+
1343
The authors of <<GHJV_DPEOROOS_1994,_Design Patterns: Elements of Reusable Object-Oriented Software_>> describe the intent of the Builder pattern as:
1444

1545
> 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
2959
Firstly, Java methods do not have named parameters.
3060
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.
3161

32-
For example, here is a method that builds a FerryBookingRequest from a posted web form:
3362

3463
<!-- begin insert: -->
3564
```java
@@ -109,17 +138,18 @@ public FerryBookingRequest formToBookingRequest(Form form, Trip trip) {
109138
```
110139

111140
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.
114143
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?
115144

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,
118148
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.
121151

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.
123153
We cannot use this style of code to create immutable objects.
124154
We have to declare local variables to hold partially constructed objects and write statements to connect our objects together.
125155
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
128158
That's what builders solve for Java programmers.
129159
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).
130160

131-
Here's what that object graph would look like if we construct it with builders:
132161

133-
```java
134-
public FerryBookingRequest formToBookingRequest(Form form, Trip trip) {
135-
FerryBookingRequestBuilder builder = new FerryBookingRequestBuilder() // <1>
136-
.withProviderServiceId(form.get("service_id")) // <2>
137-
.withNumberOfBerths(
138-
form.get("berths", asInt, trip::getTravellerCount))
139-
.withCabinClass(form.get("cabin_class"));
162+
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.
140164

141-
for (var i = 1; form.contains("vehicle_" + i); i++) {
142-
builder.withVehicle(new VehicleDetailsBuilder() // <3>
143-
.withType(
144-
form.get("vehicle_" + i + "_type",
145-
asEnum(VehicleType.class)))
146-
.withRegistration(
147-
form.get("vehicle_" + i + "_registration"))
148-
.build()); // <4>
149-
}
150-
return builder.build(); // <4>
151-
}
152-
```
153-
154-
<1> We create a builder for a FerryBookingRequest
155-
<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.
160165

161166
What does it take to write the builder itself?
162167

@@ -206,64 +211,15 @@ class FerryBookingRequestBuilder implements travelator.Builder<FerryBookingReque
206211
That's quite a lot of boilerplate code!
207212

208213
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.
212215

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:
217-
218-
```java
219-
public class BookingExamples {
220-
public static FerryBookingRequestBuilder aFerryBooking() {
221-
return new FerryBookingRequestBuilder()
222-
.withProviderServiceId("example-service")
223-
.withNumberOfBerths(2)
224-
.withCabinClass("standard");
225-
}
226-
227-
public static VehicleDetailsBuilder aVehicle() {
228-
return new VehicleDetailsBuilder()
229-
.withType(CAR)
230-
.withRegistration("CPL593H");
231-
}
232-
}
233-
```
234-
235-
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-
public class FerryBookingTest {
241-
public void booking_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-
```
252216

253217
## Kotlin's alternatives to builders
254218

255219
What alternatives does Kotlin offer?
256220

257221
Named parameters: code that constructs object graphs can be readable.
258222

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.
269224

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

Comments
 (0)