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 ;)

--

--

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