February 16, 2019
"Oh, really?" I reply. Let's enumerate the 8 principles for Clean Code I teach on Codemanship training courses and see if that's true.
1. It has to work. Can we at least agree that code should do what it's supposed to, regardless of the programming paradigm? Thanks. Tick.
2. It must be easy to understand. Again, that's paradigm-agnostic, is it not? Tick.
3. It should be low in duplication - unless removing the duplicate code makes it harder to understand. No objects here. Tick.
4. It should be made from simple parts. That could mean simple classes and simple methods, or it could mean simple functions. Tick.
5. Single Responsibility. The accepted definition of the SRP is "Classes should only have one reason to change", but it doesn't take a genius to extrapolate the need to build our software out of parts that do one job. That could mean classes, but it could mean functions or modules. The design benefits of composing our software out of single-purpose functions (which all pure functions should be) are the same as composing it out of objects that only do one job. A function abc() can ony be used one way. but a(), b() and c() could be used 15 ways -> a(), b(), c(), a()b(), a()c(), b()a(), etc etc. The possibilities are greatly increased.
Say we had a function that fetches an IMDB rating from a web API and then calculated a rental price for that title based on the rating.
What if we want to use the IMDB rating in other functions? With this non-SRP-compliant code, we can't. If we refactor fetchng the rating into its own function, we get an increase in composabiity.
6. Swappable Dependencies (Open-Closed + Dependency Inversion + Liskov Substitution). Again, I think what confuses folk here is the explicit use of the word "class" in the definitions of these principles. Reworded, they make much more sense. If we said that modules and functions should be closed for modification, for example, then we have a principle that makes sense. If we said that high-level functions should not depend directly on low-level functions, again, that makes sense. If we said that we should be able to substitute a function with another implementation of that function (e.g., a function that calls a web service with a stub implementation), then that also makes sense. More generally, can we add or replace functions without modifying existing functions?
This leads us on to the mechanism by which we make dependencies easily swappable: dependency injection. And this might be the root of the misunderstanding. OO terminology has dominated discussions about dependencies for so long that I think maybe some programmers only recognise "dependencies" in OO terms. That is to say, a function using another function isn't a dependency. But, of course, it is. 100%. If a() calls b() and I rename b() to c(), then a() breaks.
Back to our movie rental pricer: what if we want to unit test the pricing logic without making a web API request?
A refactored design injects that dependency as a function parameter, making it composable from the outside (e.g, from the test code).
Now we can stub the IMDB rating and turn our integration test into a unit test that executes much faster.
(NB: Of course, we could make the price() function pure by removing any dependency on the IMDB API and just passing in a rating. But to illustrate making functional dependencies swappable, I haven't done that.)
So, swappable dependencies: Tick.
7. Client-Specific Interfaces. If we're working in a functional style, each function is equivalent to an interface with just one method. So this doesn't really appy in FP. But the intent of the Interface Segregation Principle is that modules shouldn't depend on things they're not using. When I review JS code, one of my bugbears is unused imports. In the cut and thrust of coding, dependencies change, and not all developers are fastidious about cleaning up their imports as they change.
Let's say after I introduced dependency injection for the fetch_rating() function I forgot to remove the import for that module from pricer.js:
If the name of the imported module changes (or the file moves), then pricer.js is broken. So, the functional, dynamic equivalent of the ISP might be "Modules shouldn't reference things they don't use."
8. Tell, Don't Ask. This is often oversimplified to "don't use getters and setters", which is why it's typically interpreted as an object oriented design principle. But the spirit of encapsulation lives in all modular programming paradigms, including functional. Our aim is to have our modules and our functions know as little about each other as possible. They should present the smallest surface area through which clients interact with them. Function parameters can be thought of as setters. Every extra parameter creates an extra reason why the client might break.
Imagine we have a function for fetching scalar values from database tables, for example. It requires information to connect to the right database, like it's URL, a user name and password, the name of the table, the name of the column, and the unique key of the row that contains the data we want. That's a lot of surface area!
If this were a class, we could provide most of that information in a constructor, leaving pricer.js with little it needs to know. In functional programming, we do have an equivalent: closures. I can create an outer function that accepts most of those parameters, and an inner function that just needs the unique key for the required row.
Now, with the introduction of dependency injection, I can construct this closure outside of pricer.js - e.g., in my test code:
And pricer.js is presented with a function signature that requires it to know a lot less about how fetching scalar values from database tables works. In fact, it knows nothing about that. It just knows how to get IMDB movie ratings.
And, yes, that would be swappable with the function that fetches ratings from the IMDB web API. So it's a Win-Win.
So, to recap, with a tiny amount of reframing, the eight design principles I teach through Codemanship's code craft courses most definitely can be applied to functional code. FP isn't magic. Even pure FP doesn't, purely by itself, solve all your correctness, readability, duplication and complexity problems. And it most certainly doesn't eliminate the need to manage dependencies.
The irony is I can remember - once upon a time - programmers telling me about Dijkstra and Parnas etc: "These design principles don't apply to us, because we do object oriented programming".
Posted 1 year, 10 months ago on February 16, 2019