This article is part of a wider series on ‘how to make your software team much more effective’, and focuses on programming style, design and architecture.
Our goal as programmers is to write small pieces of code that combine together to do something useful:
- Each piece should be simple, so we can understand what it’s doing.
- Each piece should minimise its dependencies on other pieces, so that you can make changes without everything else breaking.
- The pieces should combine together flexibly so that you can build big programs out of them.
In other words, we want our code to be modular, so it’s simple and reusable. Pretty much everyone agrees on this as a goal. We hope that this will enable teams of people to build large, complex systems quickly and correctly, and to modify them as requirements change.
Both object-oriented and functional programming styles offer this promise, but their approach to modularity is very different.
Side note: If you don’t know the difference between object-oriented programming and functional programming, then you should first learn about both techniques and get some practice with them before you can form a rich opinion about which to use and when. (See resources at the bottom of this article).
Prefer functional over object-oriented programming
- Make your functions as pure as you can. In other words, avoid hidden inputs and hidden outputs to your functions, e.g. globals, changing the input variables in-place, manipulating class member variables, extract out the non-determinism/randomness. If nothing else, this will make everything easier to test, which makes things easier to understand, quicker to debug, and often seems to lead to better design.
- Avoid object-oriented cleverness for control structures. Avoid inheritance hierarchies, polymorphism, casting, non-virtual methods, etc. They obscure the meat of your code with lots of complicatedness and boilerplate, and they cause the flow of execution to jump around confusingly by scattering functionality over multiple levels of the inheritance hierarchy.
- Avoid classes as data structures. I am firmly convinced that ”it is better to have 100 functions operate on one data structure than 10 functions on 10 data structures". If you build your data structures out of standard library components (like lists, dictionaries, sets etc), you can use all the language’s built-in features for manipulating them, and they’re maximally interoperable. For this to work well, define a hierarchy of types to enforce integrity and to benefit from compile-time error-checking from your IDE.
- Prefer immutability. This goes hand-in-hand with the advice to ‘make your functions as pure as you can’ (above). Even if your language doesn’t easily support immutable variables, you can pretend it does by avoiding in-place state changes and get most of the benefits. Even if you can’t stick to this at a system level, I prefer immutability in core, low-level functions that are used widely.
- Prefer languages that don’t force you to define classes, and that treat functions as first-class variables. You may not have much choice over the language you write in. But if you do, these are good criteria, and most popular modern languages pass both tests..
The goal is to write smaller, simpler, de-coupled functions that you can flexibly compose.
If you’re interested but not yet convinced, and would like to learn more about functional programming:
- Watch Rich Hickey’s talks, starting with Simple Made Easy at 0.5x speed.
- Read this compelling paper illustrating the benefits of functional programming (unfortunately in a weird language called Miranda).
- Take advantage of the tools for functional programming in your language, e.g.
- Python has functools and itertools in its standard library.
- C# has LINQ
- Work through the first couple of chapters of Structure and Interpretation of Computer Programs. And/or learn Clojure. After a few months of concerted effort struggling with both, I felt my mind had been meaningfully expanded, and the experience had a profound effect on my style of programming, even after I went back to Python.
To sum it up:
Our goal as programmers is to write small pieces of code that combine together to do something useful. Both object-oriented and functional programming are abstractions designed to help us. I’m pretty sure there are problem domains that are a better fit for one or the other, and certainly some languages pretty much make the decision for you.
But if you have the choice, write pure functions because they are easier to test, and testability is the single best indicator of whether something’s a good idea. And avoid classes, because they are complicated and harder to flexibly compose.
What is object-oriented programming?
What is functional programming?
- http://blog.jenkster.com/2015/12/what-is-functional-programming.html (opens with a really clear description of ‘side-effects’)
- https://maryrosecook.com/blog/post/a-practical-introduction-to-functional-programming (useful for its contrasting non-functional and functional implementations of the same problem)