The other day we were talking about problems we (as a room of developers) had been having with NuGet, inter-package dependencies, and were quick to relate them back to DLL Hell. But I had this slight epiphany from another comment, where it was observed that our X.Common package had 'helper code' for database access, base class helpers, mathematical calculations, and domain logic common functions. Not to mention the dependencies it pulls in to allow some of this code to work (libraries, adapters, ORMs, frameworks etc).
The second part of this problem, was that other packages pulled in this large common package for one small part, and ended up with all of it's dependencies as well. The applications, once put together had packages that it had no necessity for, and dependency trees that where deep, intertwined, and prone to break when versions of common third-party libraries had breaking changes.
"We should be building our packages with a single responsibility, or split them up until they do" was my statement. At hearing this back out of my own mouth, my brain instantly started thinking about the SOLID Principles and the concept of Composition over Inheritance.
My mind was racing ahead of itself thinking through how applicable these OO concepts were to assembly and package architecture. And It looks like these principles apply at whatever scale you are looking at, where you have units of functionality, and composability.
So here are my thoughts put into writing. Agree? Disagree? Have a read and please leave your thoughts in the comments.
- Single responsibility
- Liskov substitution
- Interface segregation
- Dependency inversion
- BONUS ROUND - Composition over Inheritance
Each NuGet package should have one, and only one reason to change. It should do one thing, and one thing well. Just like having a class called
'Manager' is a code smell, a package called
'Common' is also a smell. Maybe you have a
Core.Formatting and a
Core.NHibernateExtensions package that can be pulled out of your
'Common' and be much clearer what they are for, and which one you would add new code to when the need arises.
I struggle a little with how this directly applies, but I can see one example that kind of makes sense. If you have a requirement for your package component to log out information, you might think about including NLog so that you have defined that functionality. But now you are closed to extension, someone can't come in and use entlib logging, or you would have to open it up and modify it to now support both of these, as required. But this doesn't scale to a third, or fourth logging platform.
Instead, have the Logging capabilities defined as a pluggable part of your package. Even supply a NO-OP implementation (or Null Object) so that this is now an opt-in function. You can then leave your component closed for modification, and open to extension by you, or anyone else building an adapter package, a
'Feature.Logging.EntLibLogging' series of other small single-purpose adapter packages that their application can pull in when it wants to use your library with their particular logging framework. This doesn't even need to be a package, since they could implement their adaptation directly in their application code if they want to.
Extend this out from logging, to all the other cross-cutting concerns a large scale application might want your component to have the ability to instrument in, and there are many opportunities for this principle to be applied.
This is another hard one, but maybe we should look at this in terms of semantic versioning? Semantic Versioning defines the major version number as breaking changes, the minor as new features that are backwards compatible, and patch as backwards compatible bug fixes. So we say base class is a Major release (2.0.0), and it's subclass(derived class) is the next Minor (2.1.0) or Patch (2.0.1) release. Does this mean that this holds true?
"NuGet Dependencies in a program should be replaceable with instances of new Minor or Patch versions of those packages without altering the correctness of that program"
and extending from this:
"NuGet Dependencies in a NuGet Package should be replaceable with instances of new Minor or Patch versions of those packages without altering the correctness of that Package"
I think this is a fair substitution right? Exactly what Semantic Versioning has been defined to allow. If some third party library has updated, my app should be able to consume a newer minor or patch version, without needing to modify the original application (even compilation of sed program). And updating a third party library in the application, should not affect any of it's other NuGet packages, even ones which were compiled with the compatible Major version that we updated.
“many client-specific interfaces are better than one general-purpose interface.”
Well, should we say lets have lots of smaller packages, because that's going to be better than one giant common package? Maybe we want to take that even further? We may have a framework NuGet Package that solves 5 different problems. But we may want to only use one of these 5. If we create 5 interface projects, and depend on only the one we want, either someone else could implement a faster, or more efficient version of this one interface, and we can swap between them. If that new faster thing also had to implement all the other parts, we couldn't do this.
Yeah, this just sounds like the original pattern, but explicitly separating the interface of a packages functionality, from the implementation packages does sound valuable, and this even allows version independence that is much stronger to detect changes for. (This is sounding more like Liskov again, hmmm...)
That's the problem with trying to define precise examples with solid I think, I feel like Interface segregation is less of a concern when you are already following Single responsibility and Liskov substitution.
Ok, this one sounds the most obvious one right? In fact, I kind of covered this in my Open-Closed example. And kind of almost touched on it with Interface segregation as well.
one should “Depend upon Abstractions. Do not depend upon concretions.”
Composition over Inheritance
We all get told we should compose our objects from other components, instead of deriving and deriving from base classes for behaviour inheritance. Packages are the same. Lets take dependencies on 50 small packages and compose these into our application, and tie them together as required. These small packages are small because they follow all the above patterns, and their dependency tree is flat. at most they take only one or two (non-framework) packages as dependencies, which have no dependencies of their own. Definitely nothing too much more deeply nested then this if you can help it.
This is much better that the alternative, of 100 packages, intertwined like the most spaghetti of code that you might have ever seen, with no way in sight of decoupling and de-tangling the mess. The benefits of a flat structure and allowing the application to compose, is that you can get the maximum of reuse from any of these projects, they are truly independent components, and you can make the decision at the app level, if done well, about which logging framework your use, which ORM, or data access you choose, which database you use, how you host your code, and even what front-end technology you want to use, without any of these concerns having to be consistent across all your packages. These concerns are just more packages and tie-in code that you compose in.
So where does that put us? Well here are some general rules and observations I would try to follow, if the packages are only just starting to take shape, or you are looking at tidying up your packaging scenarios.
- Have a flat dependency structure, like you would a class hierarchy.
- Keep your packages small and single purpose.
- Use Semantic versioning properly, to help anyone who consumes your package.
- Don't take dependencies on anything large, or that you don't need to from inside a package.
- provide extension hooks from inside your package so that others can extend the functionality or swap out implementations of common concerns like data access and logging.
- Don't force a hard dependency on your functionality, if an interface package would do, especially for version compatibility.
Did I get it wrong? Do you Agree? Anything I missed? Leave a comment and let me know.