Making mistakes: Part 4
Saturday, 7. Aug 2021
There can only ever be one common assembly, and it isn’t yours.
This is my favourite, and it can be found in almost every bigger project. I’ve seen it with every single client without exception, and I’ve done it myself until a few years ago. Most of us know it as that dreadful assembly called Common, Core, Base, Internal or similar (I’ll use Common for the rest of this article). It’s also known as the dark spot in a project, that everybody is required to use and that nobody wants to touch. A place for coding horrors that just keeps on growing into a swamp of nightmares. Too graphical and exaggerated? It’s the place where developers like to put all the code that multiple components of a project depend on.
It usually starts with a friendly piece of code that is used everywhere across the code base and that has some fundamental value to the entire project. This can be something like a logging mechanism, advanced exception handling or a base implementation of business objects. Since all of this is used in every single assembly of the project, a new assembly is born that resides right at the bottom of the dependency tree. This new assembly usually comes with a few hard rules that are set up in the beginning.
- It may only contain functionality that is required by every other component in the project.
- It must not implement functionality that is expected to change at all.
- It must not depend on anything but the BCL.
- It must be documented and tested properly.
Enter the four horsemen of the coding apocalypse
With all that in mind, developers are starting to sweep through their code to find refactoring candidates that should go into Common. The code base is being cleaned up a bit and all is well. Common is accepted as a new member to the family and no-one can live without it any more. This is the point where everybody should stop touching Common and leave it alone. But that’s not happening, and developers start to request the inclusion of code that isn’t used by one or two components in the project. In a large code base with a three-digit number of components, this doesn’t sound too bad at first. But in reality it’s the beginning of mayhem, and it eventually gets worse. The more this happens, the more likely it is that the requested functionality is actually included into Common and rule #1 is torn down. From now on, Common will grow faster and faster and will eventually include more code that is required by an even smaller portion of components.
Common is supposed to be an assembly whose content can only grow, but never mutate. Whatever is implemented should be done so as a cold, hard fact that represents nothing but the truth from this day until the end of days. But the fast growth will lead to a split in responsibilities, where the assembly is maintained by more than one team. This is also where not entirely static functionality sneaks in, that will change over time, forcing every other component to upgrade. This isn’t a biggie in the beginning since everything is tested and documented properly. Changes are propagated across the entire project through obsoleting, deprecating and semantic versioning. The frequent changes can be handled and development is smooth in general. There are some hiccups due to updating, but it’s nothing serious. With this much work being done by multiple teams on the same code and the faster release cycle, rule #2 dies a silent death.
Somewhere along the line, there will be an addition to Common that requires a more complex construct from the BCL. A feature like that could be a helper to handle file formats like XML and JSON. It could also be something else entirely, like vector maths or a generic handler for enum operations. All these are examples where there is a decent implementation already available in the System namespace, but more complete or easier to use implementations float around outside the BCL. Including external packages may not seem like a big deal, as long as they do not introduce any more third-party dependencies. But those packages dramatically add to the amount of code that is shipped with Common, and who’s to say that a future version of said packages will never depend on anything else? This ends up becoming a big mess where dependencies of that little common assembly can be directly used in every component of the project. In the name of ease-of-use, rule #3 is stomped.
With everything that has happened, Common has become a busy place with many people coming and leaving. There is no single managing entity that takes care and protects the component. As the saying goes, too many cooks spoil the broth, that’s exactly what is happening. Many parties get a say in what gets included and how it’s done. And if adaptions from a single party break the workflows of others, workarounds are added. Unfortunately, workarounds tend to circuit problems instead of solving them, since often the developer is under the gun or a deadline looms on the horizon. Traditionally, documentation is the first to go when time or money is thin on the ground, with testing as a runner-up. Rule #4 finally kicks the bucket as code quality and stability degrade further. This continues until everyone steers clear of Common and no more changes are volunteered. It has started in good intention, but now it’s dead in the water, fully immobilized and surrounded by a thick smell.
The underlying misconception and a trivial solution
Not all of these four scenarios are bound to happen in every organization, but one or two are sufficient to get the ball rolling. Very few teams manage to survive the horsemen by ruling with an iron fist, but it’s tough at best. The real issue is to believe that there can be a common assembly, that is depended on by everything in a project. There is no guarantee that every component in a project is going to have to log anything, or that there will be exceptions that need handling. No matter how basic a logic is and how important it may seem, there will always be a component that doesn’t require it. The only real Common that can ever exist is in fact mscorlib (Microsoft common object runtime library), also known as BCL (Base class library). It’s the only dotnet library in the world, that every single project must depend on, if it is meant to be useful at all. Obviously, there are multiple flavours of it, depending on the targeted framework. But it’s the place where all the common types like strings and booleans and integers reside, all the stuff that enables software to actually do something.
Since Common is already covered by Microsoft and there is no point in having another one, the problem of sharing code is still pressing. A solution can be found by taking a look at the NuGet community and how its participants create and share content. There is a trend to create more compact packages that ‘do just one thing but do it well’, which is reflected by NuGet’s top 100 most downloaded packages. And it makes a lot of sense, too. Smaller assemblies are far easier to handle and introduce fewer dependencies than large ones.
But that doesn’t mean that there can’t be a <YourProject>.Common at all. It just can’t exist as an assembly, but it can very much exist as a namespace that contains a large variety of assemblies. There is obviously no point in having the same utility code in multiple places, but something like <YourProject>.Common.<YourUtility> is a pretty good place for it. So instead of refactoring common code into an obscenity such as Common, having an army of utility assemblies is a much better choice. It enables each developer to cherry-pick whatever functionality they need. And it’s not restricted to dotnet or NuGet either, as packages and reusable code are the essence of modern programming and almost every underlying technology.
All this took me far longer to understand, than I’d like to admit. Much of my free time since early 2019 went into extracting small and specialized packages from my very own Common. I have managed to deploy a fraction of it into fifteen NuGet packages as of today, and I’m guessing that there will be at least five times as many once I’m done. It’s not a problem since this is all private code that is only used by myself, but imagine the impact of refactoring a behemoth like that in a project with a hundred developers or more. Even with a much smaller Common, it’s safe to assume that it would get out of hand quickly.