While reading Hacker News I came across a very interesting blog post which contained a small discussion based on a Tech Talk by Google about how and why you should avoid
if statements and
switches through polymorphism.
The basic premise is that if you are repeatedly checking if an object is a type of something or if some flag is set then you’re probably violating one of the fundamental concepts of Object-Oriented Programming. Rather than introducing (complicated and/or cryptic) logic for a generic and incohesive class, it is advisable to implement a concrete subclass of an abstract superclass.
Ultimately, you should be trying to implement each fundamental OOP concept (SOLID) - pragmatically of course, as over-engineering a solution is just as poor practise.
Why Should We Do This?
The main benefits (taken from the Tech Talk slides) are threefold:
- Functions without ifs are easier to read
- Functions without ifs are easier to test
- Polymorphic systems are easier to maintain (and extend) - again, refer to the Open/Closed Principle
I’m going to adapt a non-production code (i.e. test code) refactor I recently worked on.
My main responsibility is to look after ITV’s (exposed) back-end systems, including Mercury, our video content playlist service. Given a production ID and the client platform (e.g. DotCom for ITVPlayer.com, Android for the ITV Android app etc.) then Mercury returns the requested piece of content. The platform part is important as Mercury handles many platform-specific things such as lower content bitrates for the mobile platforms and higher bitrates for YouView and the ITVPlayer site, for example.
So of course, it is necessary to test that platform-specific things work on the intended platforms only.
wrong way fast way to complexity and technical debt
Here’s a basic scenario that I’ve already given some background on above:
Pretty straightforward. Now we can implement these steps quite naively:
I think it’s implementations like this that give tools like Cucumber and its proponents a bad name. The step implementations are littered with conditional logic, unnecessary passing through of variables to various classes and a significant number of instance variables (compared to local variables).
Refactoring and removing the conditional logic
A much better approach is to properly model your domain (yes, even with testing).
Platforms are objects and should be treated as such. Mercury is also an object but it should be part of platform objects in a has-a relationship.
Let’s refactor our code from above starting with the Cucumber feature:
The plan is to have a data (platform object) instantiation pre-condition in the Given step, before having generic When and Then steps which will harness our new object-oriented design.
The new steps can be simply re-written (note the meta-programming in the Given step):
Now we need our platform base class. The idea here is to define generic platform behaviour which the individual subclasses can override if required.
An example platform might look something like this:
As a result of this new design, it is so easy to see the generic and specific behaviour for each platform. Not only that, but the test code itself is much easier to write, maintain and read. I’ve no doubt that any developer could come in and quickly get up and running.
I’ve deliberately left out the Mercury classes as they could contain some commercially sensitive information (especially the stuff around adverts). With that aside, the Mercury response class was a really important refactor as it encapsulates all the tricky xpaths and regular expression parsing of the SOAP response in one place. Again, for any platform-specific behaviour it was just a case of creating a concrete subclass of
Mercury::Response to implement the differences.
Staying Out of Trouble
There is always a fine line between meaningful concrete subclassing that aids understanding versus runaway subclassing and getting caught in an inheritance nightmare.
Base (abstract) classes are a Ruby anti-pattern, yet are embraced by the Java community.
Indeed, in statically typed languages there can be lots of boiler plate code which is ripe for inheritance. However, unless you’re a fairly experienced developer who is deft with an IDE then it’s so easy to become entangled amongst so many interfaces and implementations that you don’t know which way is up (I know because I’ve been in that situation before).
The concept of complete if-less programming has certainly left an impression on me. Not that I didn’t know that having good OO principles was desirable when designing software, I simply wasn’t aware that there was a movement around this concept.
I think that it’s easy - especially when writing non-production code - to subscribe to a ‘hack away’ mentality rather than properly think things through. Deadlines are often tight, testers are often lacking experience in writing clean, maintainable code and developers tasked with writing tests don’t always take it up with enthusiasm.
But the fact remains that there is probably no better way of describing your system than through a solid set of BDD OO tests.