Hacker Timesnew | past | comments | ask | show | jobs | submitlogin
Avoiding Nulls with 'Tell, Don't Ask' Style (2010) (natpryce.com)
34 points by fagnerbrack on Sept 18, 2018 | hide | past | favorite | 27 comments


This essay happens to be specific case about "null" but the more generalized concept is the optimal design of any public-vs-private state and how it affects the balance of code at the client site vs embedded inside the class. Unfortunately, the author's email example isn't a good case study of the principle he's trying to demonstrate.

If you expose unnecessary state, your class is acting like a "dumb data struct" and you make client code more complicated and it proliferates needless and error-prone code duplication. (I made a previous comment about this.[1])

On the hand, if you hide information that makes sense for client code to know about, you increasingly make fatter and heavier class code with extraneous class methods that's idiosyncratic to an isolated piece of client code rather than being of general utility for many clients. Arguably in this case of "missing email", it may make more sense for the client code to handle an "if (customer.email == null) SendPostalMail()".

You have to determine whether you can reasonably hide knowledge in a class such that client code (including all future client code) never needs it. E.g. if some future code needs to behave differently such as creating a different type of web login screen depending on the existence of email address, it probably doesn't make sense to fatten up Customer class with another method such as "CreateWebLogin()" to generate HTML -- all to avoid exposing the null status of email address. This would be a code smell.

[1] https://qht.co/item?id=11481194


I totally agree, it basically is one of the few instances in programming where you really can't get around needing experience. At the time of writing the majority of top level comments were arguing against the pattern, but it's useful to at least know about alternatives instead of just declaring them as bad.


Agreed, in my experience this is the real difference between mediocre/OK/excellent programmers; the ability to choose the "right" level of encapsulation and abstraction at design time. It's definitely not always correlated with experience, although finding someone relatively junior on the good+ side of the scale is a real find because as you said the school of hard knocks tends to be the best teacher in this realm. IMHO Most anyone can learn to program at a reasonable level, but not everyone can do design well; see bootcamp graduates(and also "ten years of the same 1year of experience)…


Good thoughts! Do you have a blog? :)


Ok, so a ”Maybe” avoided, but instead we added a coupling, plus moved the decision logic to the inside of the Customer object?

I would consider this a very bad design. It’s one of those solutions that you think of and tell yourself ”look at how elegant it looks”. Then one month (or more) when you revisit the code to do some additions / code reading, you realize how bad an idea it was.


I completely agree. This is a bad design and not something that I would allow on my team. That logic does not belong in the Customer object.


Where's the added coupling? Before you'd have two accessors returning their own type, one of them nullable. Now you have one method that doesn't return. That interface is also agnostic of how customers can be reached: If we add sendByPigeon() the Customer interface won't see change.

The customer object is in a good position to decide how the customer should be reached.


It's coupled the Customer to the service announcements without there being any need to. At least in the first refactoring.

The second refactoring is somewhat different. That would make more sense if the communication decision wasn't based solely on the existence of an email address. But all he's really done now is add a kind of bespoke Visitor pattern to work around a lack of double dispatch.

Personally, I'd be inclined to keep the naive version unless there were clear requirements for double dispatch (and probably more than one example). If there were multiple requirements, I'd build in an abstract Visitor framework so you're back to limited coupling at the expense of some complexity.

TBH I'd probably be using a language that had multiple dispatch but that's not always an option.


You're forgetting the calling code in your accounting: While the Customer objects learns about Announcements, the calling code (we don't see) forgets about EmailAddress and PostalAddress. That means less coupling overall.


I think this must be seen as a toy example. The Option type used this example, so it's natural that the "Tell, Don't Ask" would use the same, even though the option type is perhaps the better fit.


That is a tremendous amount of indirection[1] to avoid a silly Maybe/Optional/null check...

[1] https://zedshaw.com/archive/indirection-is-not-abstraction/


2010... There must be an engineer somewhere right now, who's trying to fix some bug in this guy's legacy code, muttering "why" each time one of these "smart" patterns comes up in the codebase.

I know from experience, that the temptation is great to be smart in your code, but after all these years my rule of thumb is: whenever you feel that you're doing something particularly clever, it's probably a bad idea.


There's a good quote that I look at once in a while to remind myself to not be too smart.

> Debugging is twice as hard as writing the code in the first place. Therefore, if you write the code as cleverly as possible, you are, by definition, not smart enough to debug it. — Brian Kernighan and P. J. Plauger in The Elements of Programming Style.


Debugging isn't too bad if you can put in good trace code that can be switched in or switched out and left in with minimal overhead/risk.

Though it may add say 20% time to typing the code in, but it can also serve as a kind of commenting.


I think my life as a programmer would be on the whole much easier if people didn't feel the urge to complexify simple things like a struct with an optional field.


I 100% agree. I don't understand how is returning 'Maybe X' is more complex that returning 'X'. In a safe programming language, a well designed 'Maybe' class should be both comfortable and safe enough to use by the callee. This post solves a non-existent problem.


I belive that the last example is overcomplicated. The CommunicationMethod instance still has to check if the email address is present with a really weird accesss to the Customer object. Anyway, the purpose of Maybe in FP is to force the user to handle both cases, not only replace "!= null" with ".exists()".


This is the original concept of object-oriented programming, i.e. don't ask for information, pass a message instead and let the destination object do its job.

Ultimately, this doesn't really work, because it increases coupling. In order to decrease coupling, the functionality that is hidden behind a message should be factored out into another class, and thus we end up with classes that are actually simple conditional functions with embedded arguments.

So, a better strategy would be to simply have functions that accept a number of parameters and do the job we want.

For example, a function that sends a package, like the one in the article, should accept a number of parameters, check upon those parameters and select the appropriate action to perform.

In this case, we wouldn't care about nulls, because they would be handled gracefully by our function, plus we get decoupling and increased reusability for free.


It worked in the early days of Smalltalk, because a Smalltalk environment is basically a blob of tightly coupled classes that all call and delegate to one another, up and down the abstraction chain, and when you want to load the application you just snarf all those classes into memory, all at once.

These days, if you're a good little do-bee, you will properly use the SOLID principles and introduce interfaces everywhere there's an identifiable joint between classes. Instead of having a class call into another class, you change the reference of the class's object to an interface reference and call through that. So now you've introduced more code, more concepts, and more complexity to do something like send a fucking email which is conceptually simple on paper (and you have a library to handle actually sending the email).

Congratulations. Welcome to software-architecture NASA.


> a Smalltalk environment is basically a blob of tightly coupled classes that all call and delegate to one another, up and down the abstraction chain, and when you want to load the application you just snarf all those classes into memory, all at once.

I really like the idea of Smalltalk images (which can also be used by some Lisps, and Factor IIRC) since we can tweak and fix values directly, rather than trying to orchestrate a way for the fixed value to be used the next time we restart it.

On the other hand, I really like functional programming ideas like immutability (which naively would stop us fixing/tweaking things) and referential transparency (where an expression and its result are interchangable; if we switch out the result, it's no longer interchangable with the expression we wrote). I also like version control, reproducible builds, etc. which seem to be trickier with images.

An interesting research area, I suppose :)

> So now you've introduced more code, more concepts, and more complexity to do something like send a fucking email which is conceptually simple on paper (and you have a library to handle actually sending the email).

I completely agree w.r.t. the insanity of people decoupling all of their classes from each other (my old boss got infected by that idea, it was painful). Still, the particular example of sending an email is a legitimate case of external interaction: whilst it's fine to, for example, create as many Customer/Request/whatever objects as we like during unit tests (since they're internal implementation details) we don't want those tests to be sending actual emails.

So if I were writing this, I would have the email-sending-mechanism as a parameter, where we can pass a real email sender when running on production and a dummy argument-swallowing black hole during tests. Note that the interface for an email sender is simply a function (from EmailDetails or whatever, to void).

> Congratulations. Welcome to software-architecture NASA.

One thing I've found about architecture astronauts is that they obsess over "reusability", yet insist on creating all their own stuff from scratch, rather than reusing what's already provided by the language. For example, from this article:

    public interface CommunicationMethod {
        void send(CustomerServiceAnnouncement announcement);
    }
The author has just re-invented the idea of a function! Yet (as Rich Hickey famously said), by inventing a new class they've made themselves incompatible/unusable by all the existing code, infrastructure and tooling that works for functions(/closures/whatever).


Instead of using the Maybe monad, just hardcode the Maybe monad into every class /s. Their `tell` method is basically just the `>>=` method from the Maybe monad but with the second parameter already supplied making it less flexible.


Or embrace them by using the null object pattern.

var listofNullandNotNullObjects = getSomeThings();

foreach(var oneThing in listofNullandNotNullObjects) {

    oneThing.doSomething();  // null objects would just return
}


I wish your comment was closer to the top. I totally agree that if you're avoiding nulls this is the right way to do it. I wouldn't say it's right for everything, but when done correctly it makes for some really clean code.


This seems like a terrible way to architect an application. No longer do we have a simple Customer data class, but a hybrid monstrosity that grows every time a new use case involving a nullable field is introduced.

I would much rather prefer to have a use-case specific class that takes care of notifying a Customer of something, and a @Nullable annotation on the Customer fields that can be null. Then:

- Every use case can decide how it wants to handle those situations, and express it in a clear way

- Every use case can be modified or deleted independently without having to do surgery in some huge object

- The code is clearly readable without having to jump through levels of indirection and polymorphism to get to the actual logic


I find the Null Object Pattern and hiding nulls to be dangerous. Why pretend or give the illusion that something exists when it doesn't?

I prefer explicit checks. I don't mind `Maybe` because that's still explicit and enables you to chain operations, avoiding repetitive null checking; the consumer must still handle the null case.


With the null object pattern, your logic still has to determine if it needs to create a null object or not. The difference is that code is down where it needs to be not further up where it can be duplicated or have more abstraction built around it that just complicates what should be really simple code at that point.


Interesting. I'd have to see some concrete examples to understand better. Where I've seen it used caused issues because it was masking data integrity issues.




Consider applying for YC's Summer 2026 batch! Applications are open till May 4

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: