01 July 2013

Program to an Interface, Fool

If you’ve read my post about how the object-oriented paradigm is being changed by languages such as Rust and Go, and if you’ve seen my project of Rust Design Patterns, then you’ve probably realized that I’ve taken quite a liking to Rust.

In addition to this, over this past weekend, I finished reading the classic Design Patterns: Elements of Reusable Object-Oriented Software. All of this has caused me to think quite a bit about one of the core principles in the book:

Program to an ‘interface’, not an ‘implementation’.

What does it mean?

First you need to understand what interface and implementation mean. To put it shortly, an interface is just what we call a set of methods that an object responds to.

An implementation is where the code and logic for the interface resides.

In essence, the principle is advocating that when we write a function, or method, that we choose to reference an interface instead of something more concrete like a class.

Programming to an Implementation

First let’s look at what happens when you don’t follow this principle.

Imagine you are Guy Montag from the book, Fahrenheit 451 (F451 from here on out). As everyone knows, books in F451 are forbidden. It’s the job of firefighters to set them on fire whenever they come across them. Therefore thinking in terms of OOP, a book has a method called burn().

Books aren’t the only things that can burn. If we have another object say a Log of wood, it also has a method called burn(). So let’s write this code in Rust to see how it turns out without “programming to an ‘interface’”:

struct Book {
    title: ~str,
    author: ~str,
}

struct Log {
    wood_type: ~str,
}

So this pretty straight forward. We just create two different structs to represent a Book and a Log. Now let’s implement the methods for our structs:

impl Log {
    fn burn(&self) {
        println(fmt!("The %s log is burning!", self.wood_type));
    }
}

impl Book {
    fn burn(&self) {
        println(fmt!("The book %s by %s is burning!", self.title, self.author));
    }
}

Now that a Log and a Book both have burn() methods, let’s set them on fire.

First let’s set the log on fire:

fn start_fire(lg: Log) {
    lg.burn();
}

fn main() {
    let lg = Log {
        wood_type: ~"Oak",
    };

    // Burn the oak log!
    start_fire(lg);
}

Everything works great and we get the output “The Oak log is burning!”.

Now since we’ve already written the start_fire function, can we pass in a book as well since they both have burn() methods? Let’s try:

fn main() {
    let book = Book {
        title: ~"The Brothers Karamazov",
        author: ~"Fyodor Dostoevsky",
    };

    // Let's try to burn the book...
    start_fire(book);
}

Does it work? Nope. Instead we get the error:

mismatched types: expected Log but found Book (expected struct Log but found struct Book)

Which completely makes sense, because we wrote our function to expect a Log struct but we passed it a Book struct. How can we fix this? Well we can write the function again but this time take in a Book struct as an argument. That isn’t a good solution at all, however. We now have the exact same function in two places and if one changes, we need to manually change the other.

Let’s take a look at how “programming to an ‘interface’” can fix this.

Programming to an Interface

Let’s take the structs that we had previously, but this time let’s add an interface. In Rust, interfaces are called traits:

struct Book {
    title: ~str,
    author: ~str,
}

struct Log {
    wood_type: ~str,
}

trait Burns {
    fn burn(&self);
}

Now in addition to the two structs, we also have an interface called Burns. This defines just a single method called burn(). Let’s implement the interface for each struct now:

impl Burns for Log {
    fn burn(&self) {
        println(fmt!("The %s log is burning!", self.wood_type));
    }
}

impl Burns for Book {
    fn burn(&self) {
        println(fmt!("The book \"%s\" by %s is burning!", self.title, self.author));
    }
}

So far it doesn’t look that much different. This is where the power of programming to an interface comes in:

fn start_fire<T: Burns>(item: T) {
    item.burn();
}

Rather than expecting a Book object or a Log object, we just take in any object with any type (we call the type T) that implements the Burns interface. This leaves us with the following main function:

fn main() {
    let lg = Log {
        wood_type: ~"Oak",
    };

    let book = Book {
        title: ~"The Brothers Karamazov",
        author: ~"Fyodor Dostoevsky",
    };

    // Burn the oak log!
    start_fire(lg);

    // Burn the book!
    start_fire(book);
}

Now as we’d expect, we get the following output:

The Oak log is burning!

The book “The Brothers Karamazov” by Fyodor Dostoevsky is burning!

Which is exactly as we wanted.

Conclusion

By following the principle behind “programming to an ‘interface’”, we were able to write a function once that is now completely reusable across any object that implements the Burns interface. Since a lot of programmers are paid by the hour, the more time we spend writing reusable code and the less time we spend maintaining old code, the better.

This is a very powerful principle for this reason.

It might not always be possible to program to an interface, but often it makes writing reusable and elegant code easier. It provides a very nice abstraction and makes working with your code a lot easier.

Edit

The response to this post was awesome. I was really pleased that it made it to the front page of /r/programming, the front page of /r/rust, and some cool people tweeted it. In particular I loved the feedback some Rust developers gave. I was able to fix my mistake of naming the trait as well as learn a bit about owned pointers. This is certainly why I try to contribute back to the community =]

You're awesome for reading this. Follow me on Twitter.