When people think of software engineers, they typically think of people who sit in front of a computer and type all day. This is only a mostly accurate view of what software engineering is. Some of the most important work a software engineer does- something that in my opinion separates good software engineers from great ones- is in the step before they ever touch the keyboard, the design phase. Planning the correct solution to a problem is a tricky process. You have to synthesize your knowledge of the codebase you are working in with the parameters of problem while considering speed, space complexity, best practices; the list goes on. I recently fumbled one such decision in the planning phase, and paid the cost of having to refactor the code later for it. What I tripped up on was choosing the right balance between getting things done and designing a system that could hold up to the future- I overengineered my solution, and ended up with a solution that was too complex for the problem at hand and took longer to implement than the alternative.
My task was to create an Android widget, which in Google’s words is an “‘at-a-glance’ view of an app’s most important data and functionality that is accessible right from the user’s home screen”. The particular widget I made was a launcher for the users most used apps based on app open frequency. At most, the widget would be 4 rows by 4 columns and each row had 4 shortcuts in it, meaning there could be a total of 16 app icons to show. To me, that seemed like a large number. Large enough to be ‘n’. So I designed a solution that would accommodate any arbitrary number of rows. This solution involved using an Android service, which is basically a long running process that runs in background threads, to handle transforming raw information into a launcher grid item. Ordinarily, that wouldn’t be so bad. However, widgets are created and updated by WidgetProviders, which are also pieces of code that run in the background. Additionally, coding widgets involves an extremely limited subset of regular android code (for example, you can only use views from this list of 16: http://developer.android.com/guide/topics/appwidgets/index.html#CreatingLayout) that interacts with your apps code like a different application.
The sum total of all this was 3 different moving parts in different threads: The WidgetProvider, which handled updating the UI of the widget and delegated the grid item formatting part of that task to the service, and also asked the application’s main thread to update the list of apps to format. Even to me as the designer and implementer of this code, it was hard to follow. Function A might call function B in another thread, which would in turn trigger function A to run again, but with a flag not to call function B, and so on and so forth. Though the widget worked and the code could handle, for our intents and purposes, an infinitely large number of most used apps to show, there was no need for it to do so. In the design phase, I failed to see that there would likely never be a case where we needed to show more than 16 apps and that the problem at hand was just to provide a way to show up to 16 apps. I architected a solution to a problem that we didn’t have and provided for a future that would likely never come to be. As a result, though my code worked, it was much more complex than it needed to be.
My engineering lead, Tunde, pointed this out after working with the code himself, and suggested I simplify it. Though it involved copy and pasting some things and some violation of DRY principles, refactoring the code ultimately made it much stronger. The new version only needed the WidgetProvider and the main thread, cutting out the need for the service entirely by just hard coding the 16 positions that we might ever draw. Though a little verbose, the code flow was now much easier to understand and work with both for myself and my team members.
In essence, this illustrated to me the trap of overengineering. Trying to find the balance between a solution that works and a solution that can stand the test of time is one of the hardest challenges an engineer faces. My mistake was forgetting Voltaire’s words of advice to not let perfect be the enemy of good, or Jana’s own core value of ‘Getting stuff done’. Not only did my original solution take longer than the subsequent one to code, but it also added too much complexity to what should have been a simple problem. I relearned a lesson I am sure I will learn again in the future- that something good and simple is many times better than a future proof but complex solution, especially at an agile startup where we want to iterate quickly. Nowadays I make sure to check that my solution solves the problem at hand, and doesn’t solve a future problem at the cost of today’s resources.