Avoiding Nulls with 'Tell, Don't Ask' Style (2010)
natpryce.com
natpryce.com
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.
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.
The customer object is in a good position to decide how the customer should be reached.
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.
[1] https://zedshaw.com/archive/indirection-is-not-abstraction/
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.
> 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.
Though it may add say 20% time to typing the code in, but it can also serve as a kind of commenting.
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.
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.
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).var listofNullandNotNullObjects = getSomeThings();
foreach(var oneThing in listofNullandNotNullObjects) {
oneThing.doSomething(); // null objects would just return
}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 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.