The problem
Lets say we have a contacts app and one of the screens shows the contact’s phone number.
The problem with that code is that we can easily end up with instances that contain invalid state:
A phone number screen with an empty phone number!
Approach #1:
One way to prevent it is to add some logic in the presentation layer:
Open the phone number screen only if the phone number is not empty
Unfortunately this approach does not provide an actual solution but a patch. Our main goal is to have a PhoneNumberScreen
that handles ONLY valid phone numbers.
Approach #2:
What we need is to move the necessary checks inside the PhoneNumberScreen
class.
We could check the number’s validity on render and show some kind of message when there is no phone number.
It is quite clear that this solution provides a bad UX. Why open a screen when the user cannot use it? Also, in any additional usage of phoneNumber
inside the PhoneNumberScreen
we need to make the same check as in render()
and handle both of its states.
Approach #3:
What we really need is to make sure that if the screen is created, then it is certain that it has a valid phone number. There are two ways to achieve that. The first one is by checking upon creation that the phone number is valid and throw an exception if it is not.
It works but we need to document it and add a try-catch wherever we create a screen instance.
The second one is by having a helper function that creates the screen only if the phone number is valid.
This also works as expected but once again we need to document it and on top of that handle any null values returned by the helper function.
Nevertheless this solution seems fine and for a very small code base is quite acceptable. The problem is that it does not scale alongside the code base. Imagine how many helper functions we need to implement every time we have to use a phone number instance in our components if we want to keep them “clean”.
The actual problem
The actual problem lies in the PhoneNumber
itself. It represents more than one states and each time we use an instance of it we must “dive” in its value and translate it to that state.
What we really need is a better representation of a valid and invalid phone number.
Final approach
This is where we use sealed classes and separate the two states:
This way we accomplish our main goal: we change the PhoneNumberScreen
to accept only instances of ValidPhoneNumber
and can now be certain that the screen will be used only with valid data. The code is self documented and any further development in the screen class will not have to consider other states for the phone number:
One big drawback of this change is that every usage of the PhoneNumber
class has just broke (see: creation of Contact instances).
Fortunately there is a quick solution for that! Factory function:
A function that has the same name with the previously used class (PhoneNumber
) and takes a single string parameter. If the parameter is not empty it returns a ValidPhoneNumber
. In any other case it returns an InvalidPhoneNumber
:
The end result is almost the same as the starting point but this time we have clear states and components that can guarantee that they will not crash because of erroneous data:
Top comments (0)