Short answer
Option
fields have use cases; they're not intrinsically bad. However, even though several well established libraries (e.g. ScalaTest) define classes with Option
fields, the latter, IMO, tend to be a code smell, as they often try to do too much for their own good.
In many cases, a type containing optional fields can easily and advantageously be replaced by an algebraic data type.
An example
The domain
Consider a business domain dealing with accounts. An account starts its life one day as an open account, but may eventually be closed. Accounts, among other data, contains the dates on which they were open and closed, where applicable.
Using an Option
field
Here is an implementation of an account, using an Option
field:
final case class Account(openedOn: LocalDate, closedOn: Option[LocalDate], ...)
We also have an account service, which defines, among other things, a close
method:
trait AccountService {
// ...
def close(account: Account): Account
}
This approach is problematic, for a number of reasons. One problem is that Account
isn't particularly performant: because closedOn
is a "boxed" type, you have one level of indirection too many, so to speak. Moreover, Account
's memory footprint is less than ideal: a "closed account" contains a pretty uninteresting value (None
), which is a waste of space.
Another, more serious, problem is that the close
method cannot enforce, at the type level, that the parameter be an "open account" and the result be a "closed account". You would have to write tests to check that this business rule is enforced by your implementation.
Using a small ADT (and eschewing Option
fields)
Consider the following alternative design:
sealed trait Account { ... }
final case class OpenAccount(openedOn: LocalDate, ...) extends Account
final case class ClosedAccount(openedOn: LocalDate, closedOn: LocalDate, ...) extends Account
This small ADT remedies the performance problem, but there is more... You can now encode the business rule at the type level! This is an example of making illegal states unrepresentable (a phrase attributed to Yaron Minsky). As a result, your service's API becomes more expressive and harder to misuse:
trait AccountService {
// ...
def close(account: OpenAccount): ClosedAccount
}
This example may be sufficient to convince you that the second approach is preferable, and that Option
fields are best avoided (or, at least, used sparingly).
Resources
For more more about eliminating optional fields towards making illegal states unrepresentable, see