Avoiding Nulls with "Tell, Don't Ask" Style

When dealing with possibly non-existent information, a Maybe type forces client code to be aware of, and deal with, the possible non-existence of information. That complicates the API, violates information hiding and spreads awareness of the possible non-existence of the information throughout the code, complicating everything as it goes. (This is a cue for Haskell programmers to feel smug about monads.) For that reason, I consider a Maybe type to be a tool of last resort when eliminating the use of null references from my code. If my objects follow the Tell, Don't Ask style, I can avoid it altogether.

If an object has a query method that returns a value that might not exist, then it must have some way of returning the "does not exist" case.

On the other hand, if the object tells a collaborator to do something with the value, it can choose, when the value does not exist, to just not tell it's collaborator anything or to tell the collaborator to do something different. There's often no need to represent the "does not exist" case as a data structure because it never gets passed around.

For example, if I have a Customer that may or may not have an email address, and that is exposed as a public accessor, I have to have some way to represent the case when the email address doesn't exist. For example, by using the Maybe type I described earlier.

public class Customer {
    ...
    public Maybe<EmailAddress> emailAddress() { ... }
    ...
}

On the other hand, if I use a Tell, Don't Ask style, instead of exposing a Maybe<EmailAddress>, I can give the customer a method like:

public class Customer {
    ...
    void tell(CustomerServiceAnnouncement announcement) { ... }
    ...
}

and the CustomerServiceAnnouncement could have methods like:

public interface CustomerServiceAnnouncement {
    void sendByEmail(EmailAddress address);
    void sendByPost(PostalAddress address);
}

In this example, the CustomerServiceAnnouncement is a Curried Object that holds both the text of the announcement and references to the communication infrastructure by which the text can be delivered to a customer.

If the Customer has an email address, it calls back to sendByEmail. If not it calls sendByPost:

public class Customer {
    private PostalAddress address;
    private Maybe<EmailAddress> emailAddress;

    ...

    public void tell(CustomerServiceAnnouncement announcement) {
        if (emailAddress.exists()) {
            for (EmailAddress a : emailAddress) {
                announcement.sendByEmail(a);
            }
        }
        else {
            announcement.sendByPost(address);
        }
    }
}

Now that the existence of the emailAddress is entirely hidden within the customer class, I'd be happy to replace the Maybe with a nullable reference, because that null won't pass between different objects and potentially cause NullPointerExceptions in far away parts of the codebase.

Replacing the Conditional with Polymorphism

Taking it further, I can give the EmailAddress and PostalAddress a common interface:

public interface CommunicationMethod {
    void send(CustomerServiceAnnouncement announcement);
}

The Customer can have a field called preferredCommunication of type CommunicationMethod, which might refer to an EmailAddress, if the customer has one, or to the customer's PostalAddress. The Customer then implements tell(...) to call send(...) on its preferredCommunication, and even the code inside the Customer can be mostly agnostic of whether the Customer has an email address or not.

public class Customer {
    private PostalAddress address;
    private Maybe<EmailAddress> emailAddress;
    private CommunicationMethod preferredCommunication;
    
    ...

    public void tell(CustomerServiceAnnouncement announcement) {
        preferredCommunication.send(announcement);
    }
}

In this way I raise the abstraction level of programming of how to send updates to different customers from low-level procedural / algorithmic code containing conditional logic on the state of my objects up to high-level, compositional code that doesn't care about state. The composition of objects defines the system's behaviour. The code that does the composing acts as a declarative description of that behaviour.

Based on a message sent to the Growing Object-Oriented Software, Guided by Tests discussion group.

Copyright © 2010 Nat Pryce. Posted 2010-07-26. Share it.

Comments powered by Disqus