Type Systems and Domain Driven Development
If you like this post I recommend you to read a more recent article about the same topic: Solving the String problem with Swift 5.
Some days ago I read this tweet from @sendoaportuondo saying that creating a class for the mail field, the phone field, etc was crazy. I quickly asked who said that and he pointed me to Domain Driven Development.
Lo de crear una clase para el campo email, otra para el teléfono, etc. me hace bastante catacrocker #ddd
— Sendoa Portuondo (@sendoaportuondo) enero 21, 2015
After reading a little about that methodology I just agreed with it. It was a little crazy, I’m sure that there are reasons behind it but for me it seems just another crazy discipline in our field. It’s not the first time that I argued against all this dogmas that we follow without questioning, just because it’s well designed, even if at the end in the majority of cases it just results on having too many layers of complexity.
For this reason this post may seem a little hypocrite but here I go.
After working again with functional programing, after many years, and the powerful type system that the Swift compiler provides us I’ve connected the dots.
If you create types for everything, then you are using the compiler and it’s type system to its fullest potential, and you can ensure that your program is correct just by compiling it. It’s like having unit test automatically written by the compiler!
Let me give you an easy example. Imagine that you have this class Person
and the sendMailTo
function:
struct Person {
let name: String
let surname: String
let mail: String
init?(name: String, surname: String, mail: String) {
self.name = name
self.surname = surname
// Not production ready
if (mail.rangeOfString("@") == nil) || (mail.rangeOfString(".") == nil) {
return nil
}
self.mail = mail
}
}
I’m using here a struct
, so we are seeing already some Swift coolness, but bare with me, this is just a type. In the initialiser we validate that the mail
is valid and if not we fail the initialisation.
In some other place in our code we have a function that sends an email:
func sendMailTo(mail: String, text: String) {
println("Sending a mail to \(mail): \(text)")
}
We can use this like this:
let alex = Person(name: "Alex", surname: "Martinez", mail: "[email protected]")
if let me = alex {
sendMailTo(me.mail, text: "A super cool mail!")
}
If the object is created successfully, we can send the mail because we know that it’s valid. But then in another place of your program you have a mail that came from who knows where:
let mail = // this came from the hell...
sendMailTo(mail, text: "Nobody will receive this mail”)
Now, you cannot be sure that this will work because you don’t know if the email is valid or not. Of course, you can validate it in the sendMailTo
function, or create a Validator
that you can reuse (because you want to reuse everything right?). Anyway you will have to write a bunch of Unit Tests to make sure that your App doesn’t crash on the the face of your boss.
But let’s try to do what DDD says. Take a look at this other approach for creating the same class Person
:
struct Mail {
let mail: String
init?(mail: String) {
if (mail.rangeOfString("@") == nil) || (mail.rangeOfString(".") == nil) {
return nil
}
self.mail = mail
}
}
struct Person {
let name: String
let surname: String
let mail: Mail // <-- use Mail type instead of String
init?(name: String, surname: String, mail: String) {
self.name = name
self.surname = surname
if let mail = Mail(mail: mail) {
self.mail = mail
} else {
return nil
}
}
}
As you can see I’ve created a whole type
just for the mail
. I’ve also moved the validation in there, but believe me, that’s not the point that I want to make.
Now we can define the sendMailTo
function like this:
func sendMailTo(mail: Mail, # text: String) {
println("Sending a mail to \(mail.mail): \(text)")
}
The usage is not different than before:
let alex = Person(name: "Alex", surname: "Martinez", mail: "[email protected]")
if let me = alex {
sendMailTo(me.mail, text: "A super cool mail!")
}
But now look at what happens with the mail that came from somewhere else::
let mail = "this is not an email"
sendMailTo(mail, text: "hi!") // 'String' is not convertible to 'Mail'
Here we go! String
is not convertible to Mail
. Compile time error. Even in Swift this is a little verbose, but if you look at pure functional languages basically your code is just a bunch of types interacting, so it just a simple as that.