Over the last few months I worked, on my spare time, on a new web development project: the site for the Web.NET Conference. It was a refreshing experience, going back working on custom development on ASP.NET MVC 4 and all the latest bits of technology.
That gave me quite a few ideas for posts, so over the next weeks I'm going to blog about some of the bits and pieces of code that I think are worth sharing with the community, like ActionResults, HtmlHelpers together with my first real-coding experiences with RavenDb.
As first post I’m not focusing on something about a technology, but I want to tell why you should never model a data field as Boolean, and instead start directly with an Enum.
How the problem started
I'm going to tell you what happened during the development of the registration and payment parts of the Web.NET Conference.
I needed to keep track of whether someone has paid or not, so I started modeling that field as a simple boolean value, HasPaid. I also wanted to show them a different message depending on whether they paid or not, to remind them they had to pay in order to get the meal they ordered. To achieve that I checked the value of the HasPaid field: after a while I realized that also people that didn't order a meal had the HasPaid to false, as indeed they didn't pay because they didn't have to.
So in this confirmation screen I also checked whether the total of what they had to pay was different from 0. And it was still manageable.
After a while someone asked if they could pay at the registration desk as in their country PayPal was not operating: so I moved them manually to HasPaid=true and added in the note field that they will pay at the desk, so that their lunch and were taken into account.
Then again something else happened: someone that has already paid unregistered and left what he paid as donation for the conference. Now I needed to filter out their meals, their participation at the conference, but still keep their payment in the count of the donations received, unlike the ones that asked for a refund for which I need to filter out also the payment: this was becoming a nightmare to handle with the original HasPaid field.
On with the refactoring
One evening I sat down, and refactored the payment procedure and all reports to go from a simple boolean to an Enum with the following options:
- Undefined
- DidntPay (the original HasPaid=false)
- NoNeedToPay (for those how didn't reserve a meal or gave no donation)
- Paid (the original HasPaid=true)
- PaysAtRegistration (for the guys who couldn't pay online)
- Refunded (if someone cancels, and wants his money back)
And after the refactoring and making sure all reports and state transitions were using the new Enum, I also had to build a small script to convert all the data inside the RavenDb repository (and this is something interesting I’d probably write a blog post about).
Another example of the same problem
The same problem occurred with the registration status: it started with 2 booleans, IsRegistered and IsWaitlist, then expanded with HasCancelled and WasApprovedFromWaitList and all crazy combinations of boolean expressions to understand his real status, and then, in that very same refactoring session, it all became a more clear and easier with that Enum:
public enum RegistrationStatus { Undefined, Confirmed, WaitingForPayment, Waitlist, Cancelled, Approved, ApprovalNotConfirmed }
State Machine/Workflow
The benefit of this approach is that both registration and payment became two state machines, and it was much more easy to implement a very simple workflow with the possibility to set allowed and forbidden transition (for example, someone in waitlist couldn’t become “confirmed” without passing from the “approved” status), and action that needs to be done when a given state transition happens. Here is an example of what the transition rules look like: previously with the combination of booleans it was crazy, and very easy to make logical mistakes.
public static class RegistrationFlow { public static void RegistrationStatusTransition(RegistrationStatus newStatus, Registration registration) { switch (newStatus) { ... case RegistrationStatus.WaitingForPayement: if(registration.RegistrationStatus!=RegistrationStatus.Undefined && registration.RegistrationStatus!=RegistrationStatus.Approved) throw new ArgumentOutOfRangeException("newStatus", String.Format("{0} -> WaitingForPayement not allowed", registration.RegistrationStatus)); if(registration.PaymentStatus==PaymentStatus.NoNeedToPay) registration.RegistrationStatus = RegistrationStatus.Confirmed; else registration.RegistrationStatus = RegistrationStatus.WaitingForPayement; break; case RegistrationStatus.Confirmed: if(registration.RegistrationStatus!=RegistrationStatus.Undefined && registration.RegistrationStatus!=RegistrationStatus.WaitingForPayement && registration.RegistrationStatus!=RegistrationStatus.Approved) throw new ArgumentOutOfRangeException("newStatus", String.Format("{0} -> Confirmed not allowed", registration.RegistrationStatus)); registration.RegistrationStatus = RegistrationStatus.Confirmed; break; ... } } }
Lesson Learned
When I tweeted about this while I was refactoring, someone told me: “Using a boolean is an antipattern”… well… now I experienced it myself.
From this experience, in the future, I’ll never use a Boolean field again, and always start with an Enum, especially with a Document database where migrating data is a bit more complex than with relational databases.
UPDATE: Given the comments received I’d like to clarify a bit my position. I’m not saying booleans are not to be used at all: booleans have value, just not when the the thing you are trying to model is a state and not just a real/physically boolean value.