Here's an example of how you might approach this from a functional perspective, and how it helps avoid the potential pitfalls. I'm working in Haskell, which I'll assume you don't know, so I'll explain it in detail as I go along.
data Application = Applied ApplicationDetails |
InReview ApplicationDetails |
Approved ApplicationDetails |
Declined ApplicationDetails
This defines a data type that can be in one of four states that correspond to your application states. ApplicationDetails
is assumed to be an existing type that contains the detailed information.
newtype UpdatableApplication = UpdatableApplication Application
A type alias that needs explicit conversion to and from Application
. This means that if we define the following function which accepts and unwraps an UpdatableApplication
and does something useful with it,
updateApplication :: UpdatableApplication -> ApplicationDetails -> Application
updateApplication (UpdatableApplication app) details = ...
then we have to explicitly convert the Application to an UpdatableApplication before we can use it. This is done using this function:
findUpdatableApplication :: Application -> Maybe UpdatableApplication
findUpdatableApplication app@(Applied _) = Just (UpdatableApplication app)
findUpdatableApplication _ = Nothing
Here we do three interesting things:
- We check the state of the application (using pattern matching, which is really handy for this kind of code), and
- if it can be updated, we wrap it in an
UpdatableApplication
(which only involves a compile type note of the change of type being added, as Haskell has a specific feature to do this kind of type-level-trickery, it costs nothing at runtime), and
- we return the result in a "Maybe" (similar to
Option
in C# or Optional
in Java -- it's an object that wraps a result that could be missing).
Now, to actually put this together, we need to call this function and, if the result is successful, pass it on to the update function...
case findUpdatableApplication application of
Just updatableApplication -> do
storeApplicationInDatabase (updateApplication updatableApplication)
showConfirmationPage
Nothing -> do
showErrorPage
Because the updateApplication
function needs the wrapped object, we can't forget to check the preconditions. And because the precondition check function returns the wrapped object inside a Maybe
object we can't forget to check the result and respond accordingly if it failed, either.
Now... you could do this in an object-oriented language. But it's less convenient:
- None of the OO languages I've tried have a simple syntax for making a type-safe wrapper type, so that's boilerplate.
- It'll also be less efficient, because at least for most languages they won't be able to eliminate the wrapper type, as it will be required to exist and be detectable at runtime (Haskell has no runtime type checking, all type checks are performed at compile time).
- While some OO languages have types equivalent to
Maybe
they don't usually have as convenient a way of extracting the data and choosing the path to take at the same time. Pattern matching is really useful here, too.
this kind of code does not make it obvious such a rule exists
- This is why code has documentation. Writers of good code will take Euphoric's advice and provide a method to let the outside test the rule before trying it the hardware.