UPDATED on March 13, 2017: Add Builder pattern section
Most developers have strong opinions regarding whether a language should be strongly-typed or weakly-typed, whatever notions they put behind those terms.
Some also actively practice stringly-typed programming - mostly without even being aware of it.
It happens when most of attributes and parameters of a codebase are String
.
In this post, I will make use of the following simple snippet as an example:
public class Person {
private final String title;
private final String givenName;
private final String familyName;
private final String email;
public Person(String title, String givenName, String familyName, String email) {
this.title = title;
this.givenName = givenName;
this.familyName = familyName;
this.email = email;
}
...
}
The original sin
The problem with that code is that it’s hard to remember which parameter represents what and in which order they should be passed to the constructor.
Person person = new Person("john@doe.me", "John", "Doe", "Sir");
In the previous call, the email and the title parameter values were switched. Ooops.
This is even worse if more than one constructor is available, offering optional parameters:
public Person(String givenName, String familyName, String email) {
this(null, givenName, familyName, email);
}
Person another = new Person("Sir", "John", "Doe");
In that case, title
was the optional parameter, not email
.
My bad.
Solving the problem the OOP way
Object-Oriented Programming and its advocates have a strong aversion to stringly-typed code for good reasons. Since everything in the world has a specific type, so must it be in the system.
Let’s rewrite the previous code à la OOP:
public class Title {
private final String value;
public Title(String value) {
this.value = value;
}
}
public class GivenName {
private final String value;
public FirstName(String value) {
this.value = value;
}
}
public class FamilyName {
private final String value;
public LastName(String value) {
this.value = value;
}
}
public class Email {
private final String value;
public Email(String value) {
this.value = value;
}
}
public class Person {
private final Title title;
private final GivenName givenName;
private final FamilyName familyName;
private final Email email;
public Person(Title title, GivenName givenName, FamilyName familyName, Email email) {
this.title = title;
this.givenName = givenName;
this.familyName = familyName;
this.email = email;
}
...
}
Person person = new Person(new Title(null), new FirstName("John"), new LastName("Doe"), new Email("john@doe.me"));
That way drastically limits the possibility of mistakes. The drawback is a large increase in verbosity - which might lead to other bugs.
Pattern to the rescue
A common way to tackle this issue in Java is to use the Builder pattern. Let’s introduce a new builder class and rework the code:
public class Person {
private String title;
private String givenName;
private String familyName;
private String email;
private Person() {}
private void setTitle(String title) {
this.title = title;
}
private void setGivenName(String givenName) {
this.givenName = givenName;
}
private void setFamilyName(String familyName) {
this.familyName = familyName;
}
private void setEmail(String email) {
this.email = email;
}
public static class Builder {
private Person person;
public Builder() {
person = new Person();
}
public Builder title(String title) {
person.setTitle(title);
return this;
}
public Builder givenName(String givenName) {
person.setGivenName(givenName);
return this;
}
public Builder familyName(String familyName) {
person.setFamilyName(familyName);
return this;
}
public Builder email(String email) {
person.setEmail(email);
return this;
}
public Person build() {
return person;
}
}
}
Note that in addition to the new builder class, the constructor of the Person
class has been set to private
.
Using the Java language features, this allows only the Builder to create new Person
instances.
The same is used for the different setters.
Using this pattern is quite straightforward:
Person person = new Builder()
.title("Sir")
.givenName("John")
.familyName("Doe")
.email("john@doe.me")
.build();
The builder patterns shifts the verbosity from the calling part to the design part. Not a bad trade-off.
Languages to the rescue
Verbosity is unfortunately the mark of Java. Some other languages (Kotlin, Scala, etc.) would be much more friendly to this approach, not only for class declarations, but also for object creation.
Let’s port class declarations to Kotlin:
class Title(val value: String?)
class GivenName(val value: String)
class FamilyName(val value: String)
class Email(val value: String)
class Person(val title: Title, val givenName: GivenName, val familyName: FamilyName, val email: Email)
This is much better, thanks to Kotlin! And now object creation:
val person = Person(Title(null), GivenName("John"), FamilyName("Doe"), Email("john@doe.me"))
For this, verbosity is only marginally decreased compared to Java.
Named parameters to the rescue
OOP fanatics may stop reading there, for their way is not the only one to cope with stringly-typed.
One alternative is about named parameters, and is incidentally also found in Kotlin. Let’s get back to the original stringly-typed code, port it to Kotlin and use named parameters:
class Person(val title: String?, val givenName: String, val familyName: String, val email: String)
val person = Person(title = null, givenName = "John", familyName = "Doe", email = "john@doe.me")
val another = Person(email = "john@doe.me", title = "Sir", givenName = "John", familyName = "Doe")
A benefit of named parameters besides coping with stringly-typed code is that they are order-agnostic when invoking the constructor. Plus, they also play nice with default values:
class Person(val title: String? = null, val givenName: String, val familyName: String, val email: String? = null)
val person = Person(givenName = "John", familyName = "Doe")
val another = Person(title = "Sir", givenName = "John", familyName = "Doe")
Type aliases to the rescue
While looking at Kotlin, let’s describe a feature released with 1.1 that might help.
A type alias is as its name implies a name for an existing type; the type can be a simple type, a collection, a lambda - whatever exists within the type system.
Let’s create some type aliases in the stringly-typed world:
typealias Title = String
typelias GivenName = String
typealias FamilyName = String
typealias Email = String
class Person(val title: Title, val givenName: GivenName, val familyName: FamilyName, val email: Email)
val person = Person(null, "John", "Doe", "john@doe.me")
The declaration seems more typed. Unfortunately object creation doesn’t bring any betterment.
Note the main problem of type aliases is that they are just that - aliases: no new type is created so if 2 aliases point to the same type, all 3 are interchangeable with one another.
Libraries to the rescue
For the rest of this post, let’s go back to the Java language.
Twisting the logic a bit, parameters can be validated at runtime instead of compile-time with the help of specific libraries. In particular, the Bean validation library does the job:
public Person(@Title String title, @GivenName String givenName, @FamilyName String familyName, @Email String email) {
this.title = title;
this.givenName = givenName;
this.familyName = familyName;
this.email = email;
}
Admittedly, it’s not the best solution… but it works.
Tooling to the rescue
I have already written about tooling and that it’s as important (if not more) as the language itself.
Tools fill gaps in languages, while being non-intrusive. The downside is that everyone has to use it (or find a tool with the same feature).
For example, when I started my career, coding guidelines mandated developers to order methods by alphabetical order in the class file. Nowadays, that would be senseless, as every IDE worth its salt can display the methods of a class in order.
Likewise, named parameters can be a feature of the IDE, for languages that lack it.
In particular, latest versions of IntelliJ IDEA emulates named parameters for the Java language for types that are deemed to generic.
The following shows the Person
class inside the IDE:
Conclusion
While proper OOP design is the historical way to cope with stringly-typed code, it also makes it quite verbose and unwieldy in Java. This post describes alternatives, with their specific pros and cons. Each needs to be evaluated in the context of one’s own specific context to decide which one is the best fit.