Alexito's World

A world of coding šŸ’», by Alejandro Martinez

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.

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: "alexito4@gmail.com")
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: "alexito4@gmail.com")
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.

If you liked this article please consider supporting me