How difficult can it ever be to build a list of objects in Java and Kotlin using the builder pattern?

Hello everyone! Some days ago I had to build a list of objects and I would like to use the Builder pattern.

You don’t bother about Java? Skip to Kotlin!

In this example, I will image that I will build an automobile manufacturers list, so I will have a classic Manufacturer builder that I can use like:

Manufacturer manufacturer = new ManufacturerBuilder()
.withName("Fiat Automobiles S.p.A")
.withNationality("Italian")
.withContinent("Europe")
.withFoundationYear(1899)
.build();

I have a builder and obviously my Manufacturer object is immutable!

I need a list of manufacturers and I wouldn’t write verbose code like:

List<Manufacturer> euroManufacturers = new LinkedList<>();manufacturers.add(new ManufacturerBuilder()
.withName("Fiat Automobiles S.p.A")
.withNationality("Italian")
.withContinent("Europe")
.withFoundationYear(1899)
.build());
manufacturers.add(new ManufacturerBuilder()
.withName("Peugeout")
.withNationality("French")
.withContinent("Europe")
.withFoundationYear(1899)
.build());

After some refactoring and tests, I finally decide to have a ManufacturerListBuilder with an add method that accepts in input a Function<UserFilterBuilder, UserFilterBuilder>, so I can provide a builder every time I need to add an element using my builder, let’s look to the usage:

List<Manufacturer> euroManufacturers = new ManufacturerListBuilder()
.withContinent("Europe")
.add(builder -> builder
.withName("Fiat Automobiles S.p.A")
.withNationality("Italian")
.withFoundationYear(1899))
.add(builder -> builder
.withName("Peugeout")
.withNationality("French")
.withFoundationYear(1810))
.getManufacturers();

As you can see the code looks more compact and the list build responsibility is delegated to a specific class that I can easily reuse everywhere.

Moreover, I can extract a method to set the common values for the builders, so I don’t have to set the continent for each manufacturer.

How I implement the ManufacturerListBuilder? It was very simple, the core of the solution is the add method:

class ManufacturerListBuilder {

private String continent;
private final List<Manufacturer> manufacturers = new LinkedList<>();

public ManufacturerListBuilder withContinent(String continent) {
this.continent = continent;
return this;
}

ManufacturerListBuilder add(Function<ManufacturerBuilder, ManufacturerBuilder> builderFunction) {

ManufacturerBuilder builder = ManufacturerBuilder
.apply(new ManufacturerBuilder()
.withContinent(continent))
.build();

this.manufacturers.add(builder);
return this;
}

public Collection<UserFilter> getManufacturers() {
return this.manufacturers;
}
}

But there is a problem, the withContinent method works only if invoked before adding elements to the list, or wait, this is a bug or a feature? This permits me to use multiple continents in the same builder:

List<Manufacturer> manufacturers = new ManufacturerListBuilder()
.withinContinent("Europe")
.add(builder -> builder
.withName("Fiat Automobiles S.p.A")
.withNationality("Italian")
.withFoundationYear(1899))
.add(builder -> builder
.withName("Peugeout")
.withNationality("French")
.withFoundationYear(1810))
.withinContinent("America")
.add(builder -> builder
.withName("Chevrolet")
.withNationality("USA")
.withFoundationYear(1911))
.getManufacturers();

So, to better dress the bug to a beautiful feature I renamed the method to withinContinent.

But how this code will looks in Kotlin? Without any refactoring we can use the builder as below:

val manufacturers = ManufacturerListBuilder()
.withinContinent("Europe")
.add {
it.withName("Fiat Automobiles S.p.A")
.withNationality("Italian")
.withFoundationYear(1899)
}
.add {
it.withName("Peugeout")
.withNationality("French")
.withFoundationYear(1810)
}
.getManufacturers();

But in Kotlin we don’t need builders, because we can rely on named parameters.

So we can delete everything and write:

data class Manufacturer(val name: String,
val nationality: String,
val continent: String,
val foundationYear: Int)


fun example() {
val manufacterers = listOf(
Manufacturer(name = "Fiat", nationality = "Italian",
continent = "Europe", foundationYear = 1891),
Manufacturer(name = "Peugeout", nationality = "France",
continent = "Europe", foundationYear = 1810),
Manufacturer(name = "Chevrolet", nationality = "USA",
continent = "America", foundationYear = 1911)
)
}

But we miss a feature, we cannot build Manufacturer instances without repeating the continent. We can solve this using Sealed classes, that are used for representing restricted class hierarchies, when a value can have one of the types from a limited set, but cannot have any other type.

So we transform the Manufacturer from a data to a sealed class and we define the class hierarchies, one for each continent:

sealed class Manufacturer(open val name: String,
open val nationality: String,
val continent: String,
open val foundationYear: Int)

data class EuropeanManufacturer(override val name: String,
override val nationality: String,
override val foundationYear: Int)
: Manufacturer(name, nationality, "America", foundationYear)

data class AmericanManufacturer(override val name: String,
override val nationality: String,
override val foundationYear: Int)
: Manufacturer(name, nationality, "Europe", foundationYear)


fun example() {
listOf(
EuropeanManufacturer(
name = "Fiat",
nationality = "Italian",
foundationYear = 1891),
EuropeanManufacturer(
name = "Peugeout",
nationality = "France", foundationYear = 1810),
AmericanManufacturer(
name = "Chevrolet",
nationality = "USA",
foundationYear = 1911)
)
}

I’m still not happy, I don’t like to repeat the parameters in the constructor, but I can avoid them defining all the constructor parameters as abstract fields except the field that I must set as default:

sealed class Manufacturer(val continent: String){
abstract val name: String
abstract val nationality: String
abstract val foundationYear: Int
}

data class EuropeanManufacturer(val name: String,
val nationality: String,
val foundationYear: Int)
: Manufacterer("America")

data class AmericanManufacturer(val name: String,
val nationality: String,
val foundationYear: Int)
: Manufacterer("Europe")

Now the code looks pretty good!

What we are doing? We are doing Object-Oriented Programming, we are using polymorphism.

Defining a class that refers to a continent enable us of using pattern matching, so we can do some kind of magic like filtering all manufacturer by type:

   val europeansManufacturer = 
manufacturers.filterIsInstance<EuropeanManufacturer>()
val americansManufacturer =
manufacturers.filterIsInstance<AmericanManufacturer>()

Describing it using pattern matching:

fun example() {
manufacturers.forEach {
println(describe(it))
}
}

fun describe(m: Manufacturer) = when (m) {
is EuropeanManufacturer -> "${m.name} is an European."
is AmericanManufacturer -> "${m.name} is an American."
}

And many others magics ;)

Further reading

In the past, I wrote two articles like that, unfortunately, they are in Italian, but there is a lot of code, so you can understand them even just by reading the code ;)

--

--

--

Open-space invader, bug hunter and software writer

Love podcasts or audiobooks? Learn on the go with our new app.

Recommended from Medium

The Virtual Bookshelf

Microservices, bounded context, cohesion. What do they have in common?

Using AWS CodeBuild to set up Github Continuous Integration

10 Continuous Integration Systems your Team should know about in 2019

Apollo Pricing: Cost and various Pricing plans

Proxy Design Pattern in JAVA

Where to start with Rsync command : 8 Rsync Examples

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Davide Cerbo

Davide Cerbo

Open-space invader, bug hunter and software writer

More from Medium

RXJava Filtering Operator

JWT Authentication with Error Handling in Ktor

Sonarqube server run locally

Dependency Injection: Constructor Injection