
Avoiding Nulls with 'Tell, Don't Ask' Style (2010) - fagnerbrack
http://natpryce.com/articles/000777.html
======
jasode
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://news.ycombinator.com/item?id=11481194](https://news.ycombinator.com/item?id=11481194)

~~~
emsy
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.

~~~
Serow225
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)…

------
lerno
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.

~~~
lolc
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.

~~~
lucozade
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.

~~~
lolc
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.

------
deanCommie
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/](https://zedshaw.com/archive/indirection-is-not-abstraction/)

------
aaaaaaaaaab
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.

~~~
funkymike
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.

~~~
smileypete
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.

------
cousin_it
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.

~~~
gnulinux
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.

------
alaiven
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()".

------
axilmar
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.

~~~
bitwize
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.

~~~
chriswarbo
> 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).

------
TheCoelacanth
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.

------
chooseaname
Or embrace them by using the null object pattern.

var listofNullandNotNullObjects = getSomeThings();

foreach(var oneThing in listofNullandNotNullObjects) {

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

~~~
bradenb
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.

------
atypeoferror
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

------
ahallock
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.

~~~
chooseaname
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.

~~~
ahallock
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.

