Your question.
But, but - from my naivety, let me scream - sometimes a value CAN be meaningfully absent!
Fully on board with you there. However, there are some caveats I'll get into in a second. But first:
Nulls are evil and should be avoided whenever possible.
I think this is a well-intentioned but overgeneralized statement.
Nulls are not evil. They are not an antipattern. They are not something that is to be avoided at all costs. However, nulls are prone to error (from failing to check for null).
But I don't understand how failing to check for null is somehow proof that null is inherently wrong to use.
If null is evil, then so are array indexes and C++ pointers. They're not. They're easy to shoot yourself in the foot with, but they are not supposed to be avoided at all costs.
For the rest of this answer, I am going to adapt the intention of "nulls are evil" to a more nuanced "nulls should be handled responsibly".
Your solution.
While I agree with your opinion on the use of null as a global rule; I don't agree with your use of null in this particular case.
To summarize the below feedback: you're misunderstanding the purpose of inheritance and polymorphism. While your argument to use null has validity, your further "proof" of why the other way is bad is built on misuse of inheritance, rendering your points somewhat irrelevant to the null problem.
Most updates naturally have a Player associated with them - after all, it is necessary to know which player made which move this turn. However, some updates can't have a Player meaningfully associated with them.
If these updates are meaningfully different (which they are), then you need two separate update types.
Consider messages, for example. You have error messages and IM chat messages. But while they may contain very similar data (a string message and a sender), they do not behave the same way. They should be used separately, as different classes.
What you're doing now is effectively differentiating between two types of update based on the null-ness of the Player property. That is equivalent to deciding between an IM message and an error message based on the existence of an error code. It works, but it's not the best approach.
- The JS code will now simply receive an object without Player as a defined property, which is the same as if it was null since both cases require checking if the value is present or absent;
Your argument relies on misues of polymorphism. If you have a method which returns a GameUpdate
object, it generally shouldn't ever care to differentiate between GameUpdate
, PlayerGameUpdate
or NewlyDevelopedGameUpdate
.
A base class needs to have a fully working contract, i.e. its properties are defined on the base class and not on the derived class (that being said, the value of these base properties can of course be overridden in the derived classes).
This renders your argument moot. In good practice, you should never care that your GameUpdate
object does or doesn't have a player. If you're trying to access the Player
property of a GameUpdate
, then you're doing something illogical.
Typed languages such as C# wouldn't even allow you to try and access the Player
property of a GameUpdate
because GameUpdate
simply doesn't have the property. Javascript is considerably more lax in its approach (and it doesn't require precompilation) so it doesn't bother to correct you and has it blow up at runtime instead.
- Should another nullable field be added to the GameUpdate class, I would have to be able to use multiple inheritance to continue with this desing - but MI is evil in its own right (according to more experienced programmers) and more importantly, C# doesn't have it so I can't use it.
You shouldn't be creating classes based on the addition of nullable properties. You should be creating classes based on functionally unique types of game updates.
How to avoid the issues with null in general?
I do think it's relevant to elaborate on how you can meaningfully express the absence of a value. This is a collection of solutions (or bad approaches) in no particular order.
1. Use null anyway.
This is the easiest approach. However, you are signing yourself up for a ton of null checks and (when failing to do so) troubleshooting null reference exceptions.
2. Use a non-null meaningless value
A simple example here is indexOf()
:
"abcde".indexOf("b"); // 1
"abcde".indexOf("f"); // -1
Instead of null
, you get -1
. On a technical level, this is a valid integer value. However, a negative value is a nonsensical answer as indexes are expected to be positive integers.
Logically, this can only be used for cases where the return type allows for more possible values than can be meaningfully returned (indexOf = positive integers; int = negative and positive integers)
For reference types, you can still apply this principle. For example, if you have an entity with an integer ID, you could return a dummy object with its ID set to -1, as opposed to null.
You simply have to define a "dummy state" by which you can measure if an object is actually usable or not.
Note that you shouldn't rely on predetermined string values if you do not control the string value. You might consider setting a user's name to "null" when you want to return an empty object, but you'd encounter issues when Christopher Null signs up for your service.
3. Throw an exception instead.
No. Just no. Exceptions should not be handled for logical flow.
That being said, there are some cases where you don't want to hide the (nullreference) exception, but rather show an appropriate exception. For example:
var existingPerson = personRepository.GetById(123);
This can throw a PersonNotFoundException
(or similar) when there is no person with ID 123.
Somewhat obviously, this is only acceptable for cases where not finding an item makes it impossible to do any further processing. If not finding an item is possible and the application can continue, then you should NEVER throw an exception. I cannot stress this enough.