Changing basic assumptions in apps
One of the hardest things for me as an app developer is changing basic assumptions in a safe, gradual way. Here’s an example.
Critter4Us is an app that reserves animals for demonstrations and student practice in a veterinary college. There are business rules like “an animal can be used to practice injections no more than twice a week”.
In its original form, Critter4Us only made reservations for one point in time (the morning, afternoon, or evening of a particular day). I’m now making it take reservations for “timeslices”, where a timeslice has a start date, an end date, and any subset of {morning, afternoon, evening}. Doesn’t seem like a huge change, but it turns out to be pretty fundamental:
-
A lot of questions vaguely like “is this point within this range?” are now “do these two ranges overlap?” There’s an existing
Timeslice
object (which, despite the name, was about points in time). It has some time-related behavior, but responsibility for some other behavior leaked out of it because the data was so simple. -
Some of the database operations were annoyingly slow after being hard to get right, quite likely because my SQL-fu is weak. Since the questions are becoming more complex, I want to do less calculating when questions are asked and more stashing partial answers in the database. So adding this feature requires more database work than just a simple schema migration.
(This feels like premature optimization of a feature I don’t know wouldn’t be fast enough, but my main motivation is that I’m more confident of getting the right answers if I stash partial answers. This is the most important business logic in the app.)
-
The UI needs to change. Should that be done by upgrading the existing reserve-a-point-in-time page (adding clutter for a case that’s used seldom) or adding a new page?
I want to make all these changes in such a way that (1) the tests are all passing most of the time and (2) the app is deployable most of the time. I devised the following strategy after writing a partial spike and throwing it away:
-
DONE Change the current Cappuccino front end. It used to deliver
?date=2010-09-02&time=morning
to the Sinatra backend. Now it delivers
timeslice={'firstDate':'2010-09-02', 'lastDate':'2010-09-02', 'times':['morning']}
This isn’t hard because the front end doesn’t do any calculations on the date.
-
DONE Have the backend controller that receives the data quickly convert the new format into the old.
-
DONE Move conversion of new-format-to-old into the
Timeslice
object. (Only two classes makeTimeslice
objects, so that’s easy.) -
DONE Change the Reservations table to add new columns. Change the
Reservation
object to allow it to be constructed using either the old or new format. Change all the non-test code to use the new constructor. (It’s convenient to keep the terser form around for the tests that use it.) -
IN PROGRESS
Reservation
andTimeslice
aren’t completely dumb objects — they probably get told more than they get asked — but they do have accessors fordate
andtime
. Those accessors are still meaningful because (at this point) thefirst_date
andlast_date
are always the same and the set oftimes
can only ever contain one element.However, change their names to
faked_date_TODO_replace_me
andfaked_time_TODO_replace_me
. Run the tests. For each no-such-method failure,-
If the purpose of the call can easily be expressed in terms of the new interface, rewrite the call to use it.
-
If not, use the convoluted, soon-to-be-replaced name.
-
-
Replace the old code that answers the question “for which procedures may this animal be used at this moment?” with code that assumes cached partial answers. Mock out the cached partial answers and thereby design the partial answer table.
-
Add the cached table to the database. Change the code that creates reservations to cache partial answers.
-
Working from the controllers down, examine each method that uses
faked_date_TODO_replace_me
andfaked_time_TODO_replace_me
. What does the method do? What makes sense in a world where reservations and uses of animals are not for a single point in time? Where does the method really belong? Fix them. -
Now that all uses are fixed, delete the methods with silly names.
-
Generalize the question-answering code to answer questions about timeslices more complicated than single points. Much careful testing here.
-
Change the UI to collect more complicated timeslices.
It’ll be interesting to see what the final structure of the code looks like. A lot of this code was written early in the project, so I’m sure it’ll improve a lot.
If you want to see the code, it’s on GitHub.