Lessons in Software Design
I originally wrote Gravtastic, a library for using the Gravatar API, to learn a bit about Ruby and how to publish gems. It was a fun project which I did during my exams in 2007. The project has a few people watching it on Github so it is the best public representation of my abilities (other than my proposed enhancement to Rails). I looked back on it the other day and it struck me just how much I had learnt in the last few years. None of my code was bad, but I've been living and breathing binary since late 2008 and my opinion about everything has changed. Hang on, who am I trying to protect? The code was bad and here is what I learnt from re-writing it.
Lesson 1: Results
Firstly, the spec suite was massive. It was disgusting to work with. Sitting down to some refactoring I made one little change and three tests broke (all for unrelated features). The library still functioned correctly, but the specs were too closely coupled to its internals. To fix this, I went through and made specs that only tested the result, not the implementation. To help choose what features to cover with the specs, I singled out methods that were more than 4 lines long. Anything less than that, really, can probably be tested by inspection. In an ideal world, your software should be so simple that you know it works just by looking at it.
Despite the current Cucumber craze, this lesson shouldn't be confused with writing integration tests: it specifically applies to how you write your unit tests. Each method I was testing was essentially mocking out the execution of the rest of the library. Here are some particularly bad examples:
it "changes .gravatar_source" do
lambda {
@klass.is_gravtastic :with => :other_method
}.should change(@klass, :gravatar_source)
end
it "returns the value of @gravatar_defaults" do
@klass.instance_variable_set('@gravatar_defaults', :foo)
@klass.gravatar_source.should == :foo
end
Theoretically doing this meant that one method could be tested in isolation without anything else working. I mean, that's how tests should be, right? No. In reality, this just resulted in a big mess of interweaving dependencies. There were originally 34 specs and I ended reducing them down to 10, the core functionality.
it "source is :email" do
@g.gravatar_source.should == :email
end
it "options are { ... }" do
@g.gravatar_defaults.should == { ... }
end
It was actually better just to ignore the perfectness of the tests and go with testing the outcomes that I cared about. This process had the added bonus of also making the specs a readable source of documentation.
Lesson 2: Power
The code had prided itself on being strict: every option passed to the #gravatar_url method was checked and any invalid keys thrown out. This, however is the wrong way to approach software. If you are designing a system to be used by others, it needs to be as open as possible.
Let's say, for example, that Gravatar brings out some new feature which is activated by the parameter xtra=extreme, so a developer would call it using gravatar_url(:xtra => 'extreme'). Before the Great Refactoring, the library would just filter the xtra parameter because it wasn't defined in the list of valid parameters. That would mean that every time Gravatar released an update I'd be there, playing catchup. Fixing this was easy: I just don't give a shit anymore. Users can pass whatever parameters they want. If the user passes an argument which I know a shortcut for (like r for rating) then great, it'll use that.
I know there will be the people who think "what about developers who don't test"? Fuck them. If they are not testing the code they write and don't pick up on a poorly passed parameter, then their app should blow up. I'm not going to sacrifice power and flexibility to fix the mistakes of a minority. I guess this is why I prefer dynamic languages.
Lesson 3: Marketing
As I mentioned before, the last thing I want to be doing is maintaining software like Gravtastic and supporting its users. Yeah, I'm lazy. I spend all day surfing StackOverflow. Until, that is, when I saw a question from somebody who couldn't use Gravtastic because the README was confusing. I love the ego trip of somebody discussing my library, however they were discussing how they couldn't use it. Part of me says "screw them, if they can't figure out the README, then they probably shouldn't be using the software". At the same time, I'd like to make software that will actually be used (I don't want to be absolutist). That's the reason I released it in the first place and the whole point of open source software. So I decided to help.
When I looked at it, the README was atrocious. I'm the only person who could have understood it. The main problem was that it wasn't targeted at the correct audience. In the end, I spent more time rewriting the damned README than coding. Version 2.1 of Gravtastic is more an update in it's marketing than anything else.
Not having practised writing in years I found it very fucking hard. Yes, I'm an "arts" student who has to write essays on things like Tocqué's interpretation of Maria Leszczyńska or the Japanese avante-garde in the 1920's and 30's, but that is hardly writing to entertain someone. If you find shitty painters and pretentious sculptures interesting then you probably won't understand what I mean by "entertainment". Most users don't know what they want, let alone know that they want your product. Getting someone to think that what you offer is fun is the easiest way to get them to want it. And that's why working very hard on writing good READMEs is the easiest way to get people to like your software.
Of course, the best way of getting people to appreciate whatever you do is by doing a good job. It's a shame I'm not a good programmer, because then that would be easy. I constantly have to force myself to reflect on what I've done wrong. It's never pretty, but neither is your Mum.