Don’t fork, factor
There comes a time in the lifecycle of any significant software project when developers will be tempted to fork the system into two different products. You might, for instance, find yourself with a customer whose requirements seem at odds with the direction of your product. You might be facing some seemingly unresolvable incompatibility of system components.
At Emma, we’re in the process of rolling out a new API that will allow our customers to manage their data in cool new ways. As part of this process, we’re migrating users to a new database schema and adapting our software to talk to the new API. In deciding how quickly we can move our systems to support both API users and those who still use our legacy database, we’ve faced this forking question a number of times.
Forking can look alluring. A system that only has to support one kind of user sure looks simpler than one that has to figure out what kind of user it’s working for and then do the right thing. Failure to fork without good factoring can result in code with ‘if’ statements strewn all over the place, each of them adding complexity and confusion for maintainers.
On the other hand, forking produces two systems, both of which need to be maintained. You’ll probably find yourself making the same change in more than one place. It also complicates deployment, and you may have to work out schemes to direct users to the right system.
Adding features to our internal user admin tool to support API users gave us an opportunity to try the approach of factoring in code that would work with legacy accounts and new accounts at the same time. We were pleased to find that we could achieve that by making a small number of low risk changes to the admin tool, adding a layer of indirection. The code actually looks better than it did before the change.
Our support team uses the admin tool to, among other things, restore deleted members to mailing lists. The old implementation of this connects directly to the database and issues some SQL queries to move some records around. The new way is to issue an HTTP request to the REST API.
The code in the admin tool isn’t super nice-looking. Still, it’s served us well for many years, and we wanted to support the new API without changing the older code extensively.
So, we have some old code that looks about like this:
class Controller(…): def undelete_members(account_id, user, member_ids): # 50 or so lines of code to execute some SQL # queries inside of the controller method. # Here be dragons. …
We use a simple factory method so that the behavior can be changed on a per-account basis and then move the operations into a pair of classes that abstract the operations on the members.
class Controller(…): def undelete_members(account_id, user, member_ids): impl = get_backend(account_id, user) impl.undelete_members(member_ids) def get_backend(account_id, user): # Just the one 'if' statement backend_class = APIBackend \ if is_an_api_account(account_id, user) \ else LegacyBackend return backend_class(account_id, user) class APIBackend(object): def undelete_members(self, member_ids): res = self.call_api( self.account_id, '/members/undelete', method='POST', data=member_ids) class LegacyBackend(object): def undelete_members(self, member_ids): # The same 50 or so lines of code to execute # some SQL queries, moved out of the # controller. Let sleeping dragons lie. …
One of the interesting things about this is that, in addition to wedging some nice new API code into the admin tool, the legacy code gets better too. While it’s mostly untouched, so we don’t have to worry too much about introducing subtle bugs, we now have a structure with greater separation of concerns. The part of the code that deals with the incoming HTTP request is separated from the part of the code that manages the member data. Also, in LegacyBackend, you see something that looks kind of like a primitive object model. Better still, the APIBackend class looks like the beginnings of something that could be used as a client that simplifies access to the REST API in our other systems, or for end users who want to manage their own accounts in Python scripts. Everybody wins.
The moral of the story is that, with a little bit of basic old fashioned object ingenuity, you might be able to stretch your system’s behavior a lot further than you think, saving yourself from supporting and managing two different products, while making your code more flexible and more maintainable.