Fri 27 Apr 2018 21:36:13 BST
Tue 01 May 2018 23:05:44 BST Edited For Readability
We need to talk about abstractions
Abstraction is a word that is used a lot in software development discourse, but I sometimes think that we might not all be talking about the same thing when we use it. I first noticed this when I saw a slide from a talk that said something along the lines of
Rules of abstraction:
- Don't use abstraction
- Don't use abstraction
- Don't use abstraction
I still kind of wonder what that person's code looks like. Recently I've also seen quite a lot of warnings about premature abstraction. These are usually made by super smart people who I am sure are better developers than me, but something about this viewpoint slightly vexes me. This blog post is basically me trying to explain why.
Let's start with what I think abstraction means, because this is kind of important. Dictionary definitions vary a bit but the common theme is of developing general ideas from specific examples. I like to think about abstractions as the distillation of an idea. Boiling off the extraneous details and condensing out the purer, more general essence. All models are abstractions by necessity - a non abstract model of a thing would just be the thing itelf - and given that software development is, to a large degree, the practice of creating models then it's easy to see how central the concept of abstraction is to programming. Which makes "don't use abstraction" a fairly baffling rule of thumb for a software developer.
It's probably worth looking at some specific examples of abstraction in a software development context. Let's imagine we are writing a todo list application. Its going to have a RESTful web API, we're going to use a ports and adapters approach to separate our core domain logic from how we interact with it, and the first route we are going to implement is going to be a POST to /todos which creates a new Todo with a title and a description, and saves it by writing it to a file.
Clearly we're using a bunch of abstractions in this example. We're got the concept of a todo, which has a title and description and has a method to save it. This is an example of an abstraction in the model of our domain and these are probably the most important kind of abstractions that we create as they are core to how our application will work. We're using a file to store the todo, which is an operating system abstraction that keeps us from having to worry about some of the messier details of physically writing data to a disk. This is an example of how software development relies on layers of abstraction, that make life easier for the people trying to solve problems in the layer above. Finally, we will likely be implementing an adapter abstraction to map a http request (probably in some representation dictated by a HTTP framework) to a representation defined in our core domain model. We might call this an "architectural abstraction" to distinguish it from the domain abstraction relating to todo concepts themselves. You can probably identify a ton of other abstractions at this point but those will do for now.
We can see even in that small example that there are different types of abstractions and they help us in a number of ways. From an implementation point of view, they enable us to reuse code, and avoid rewriting lots of boilerplate for very similar situations. I feel like warnings about abstraction focus on this aspect, because DRYing something up can sometimes lead to unwanted coupling in our code for example. Perhaps the more important use of abstraction though is it gives us a way to think about the design of our system. To me, abstractions feel like the compositional units of design in the same way that classes or modules etc. are the compositional units of the implementation. To me it seems pretty hard to separate the act of creating abstractions from the act of design, but more on that later.
So, back to the question of premature abstraction then. Would it be better to have started implementing that initial feature of the app without attempting to identify the likely abstractions that will be used? One school of thought says yes, because I might not need those abstractions, maybe I need different ones, or maybe I don't need any at all, and it's best to let the tests drive these things out.
I feel that thinking about the likely abstractions ahead of time is actually a big benefit. It allows me to get feedback on the design straight away, before any code has even been written. I can discuss the abstractions with people and play out scenarios. I can ensure the team are the same page with regards the design ideas and principles. There is also the question as to whether I would be able to TDD my way to better abstractions. A lot of the value in abstractions is in their generality, so thinking about them before being exposed to specific problems might help prevent getting too bogged down in the specifics and being unable to see the bigger patterns. Also, I'm not sure I believe that the TDD process on its own can lead someone to a good design. It's a way of getting feedback on a design, but the processing of that feedback and being able to use it to steer towards a good design is down to the abilities of the developer(s). If the developers are more junior, it seems even more likely that they would benefit from some discussion of the general ideas that might be useful, rather than getting stuck straight into red - green - refactor.
OK, so there were possibly some straw men in there. Of course you would hope that a developer without enough understanding of design would not be abandoned to TDD on their own without assistance. And TDD does not say you can't also do some design thinking up front, I imagine most TDDers would actually encourage this, so long as its not the dreaded "big design up front". Likewise, there is nothing that says that people who think about abstractions beforehand are not allowed to change their mind if the tests are telling them its a good idea. So perhaps the difference in viewpoints that I detect is more a difference in emphasis. I think this is worth bearing in mind in order to avoid throwing the design / thinking baby out with the BDUF bathwater. And we should try not to incubate a "fear of abstraction" because I think that will lead to more programs made up of a bunch of ugly, imperative scripts that are hard to read and harder to think about.