Monday, October 29, 2012

The Measure of a Design

We all want to write good software rather than bad software. What makes software good? I am not going to try and answer that question today. Instead I am going to talk about something that I've observed over and over in my software career - How can you judge whether software is designed well?

how easy is it to change the software?Note, I am not talking about whether the software is good or not. The ultimate metric of that is, does it solve the problem it is supposed to. My question is, is the architecture and design good? The empirical measure of that is, when new requirements come up, how easy is it to change the software?

Ease of changing software consists of various factors: how much code has to be written/changed, how easy is it to find the appropriate place(s) to change the code, and how confident can you be that the changes didn't break other functionality.

How Much Code
This measure isn't about absolute lines of code. It's about lines of code relative to expectations. If you are adding complex functionality, you'd expect it to be a decent amount of code. On the other hand, if you are just fixing a typo in an output, you'd expect that to be a really small amount of work.

There was one project I worked on that included a report of employees and their salaries. A request came in that the report have the employees sorted by salary. My expectation for this change is it should just be a matter of putting a sort call in the appropriate place, and possibly having to write a custom comparator. I.e. a really quick change. In reality, this change required some major reworking, and had to be delayed because it was going to take too long to accomplish.

When simple changes take a lot of work, this is evidence that your design is less than ideal.

Appropriate Place
When writing software, most tasks can be accomplished in a myriad of ways. A good architecture will lead you to choose one way over another. When the architecture guides you to a specific place for putting in new features, this is a good thing. Not only does it help with consistency, it means that when your new feature has to be changed, the next developer will know exactly where to go to find what you did, and know where to make the change.

This is something I really like about Ruby on Rails. It is an opinionated framework which sometimes feels very limiting, but means that most times I know exactly where to go find the code that I am looking for.

Unexpected Changes
One fear that is common when changing existing software is that you'll break something unintentionally. A hallmark of a good design is that this rarely happens. Well structured code with clean decoupled interfaces can go a long way to giving you confidence that a new change won't have unintended consequences. Yes, having a large suite of automated tests is really helpful (whether or not your design is good), but no test suite is completely thorough. I find that when I am making changes to code, I usually have a pretty good feel of how risky the change is. When most changes feel risky, this is a sign that the design can be improved.

Example
A coworker recently came to me with a change request from our client. We have an electronic form system. Form instances are allowed to be edited by the form submitter. Well, this one form type includes a list of people (as a field in the form) that the client wanted to also be allowed to edit the form. I.e. we had to make a change in the security system, but just for part of the application.

To fix this, what we had to do was to override the can_edit? method that is in the base form class, with the logic required in the derived class. Something like:
class SpecificForm < BaseForm
  def can_edit?
    super || form_editors.include?(current_user)
  end
end
The current design was a good one for this change by the above metrics. The new code required was very simple, it was obvious that "can_edit?" was the appropriate place to put the new logic, and since the change exists as a new method only in the one specific class (i.e. the base class's logic didn't change), it is unlikely to affect any other portion of the application.

Refactoring
Code is rarely written in ideal situations. Requirements aren't all known up front. You don't always have an adequate amount of time to do a good job. As a result, existing code bases don't have the best design. When you find that more and more of your changes take more time than expected, or it isn't obvious how they should be done, or each change introduces new bugs, it is time to think about refactoring.

It can be hard to find the time to do this, because by definition refactoring doesn't add any new functionality or fix any existing bugs. Yet it is important to do at times. A month ago, the "can_edit?" change mentioned above wouldn't have been nearly so simple. The permissions functionality had been spread across multiple classes and methods and instance variables in a very inconsistant manner. As such, it was easy to get confused as to the right way to do things, easy to miss a change that needed to be made, and easy to introduce bugs. However, a couple of weeks ago, I finally got fed up with all these (and many other) problems, and refactored the code. While that meant a couple of days where nothing new got fixed or added, that time will be more than made up over the next couple months as new features and bug fixes get made much more quickly.

One last thought. A, often overlooked, key to successful refactoring is pride - don't have too much pride in your code. Many times I've seen developers who are happy to refactor other people's code, but don't want to admit that there could be anything less than perfect about their own design and so resist any changes. Almost everything can be improved. Have enough security in yourself to allow your own designs to be improved and you will become a better developer.