The tenet is: reduce
the number of your .NET assemblies to the strict minimum. Having a single assembly is the ideal number. This is for example
the case for Reflector or NHibernate that both come as a
single assembly.
A lot have been said on
this topic. I dig in this article why using more namespaces and less
assemblies for componentization is a good thing. Jeremy Miller does it so in
this blog post. The point is that assemblies are physical while namespaces are logical. As a consequence, having N
assemblies multiplies by N the burden of dealing with physical things. This
burden consists of referencing N assemblies from a Visual Studio project, slowing
down significantly compilation of N Visual Studio projects, managing the
deployment for N files, need for the CLR to do N CAS security checks at startup
time, etc… Personally I have seen applications with up to 750 assemblies! Quote from Jeremy Miller: I took 56 projects one time and consolidated them down to 10-12 and cut
the compile time from 4 minutes to 20 seconds with the same LOC
I would like to discuss
here the motivations behind creating an assembly. I will then illustrate these
reasons through the choices we did for assemblies of the NDepend code base. In an effort to reduce
the number of assemblies in your shop, it is a good thing to find a valid reason for the existence of each assembly of your code base. If no solid reasons can be
found you’ll find room for merging assemblies.
Valid and Invalid reasons to create an assembly
Valid reasons to create
an assembly:
- Tier separation: Need to
run some different pieces of code in different AppDomain or process. The idea is
to avoid overwhelming the precious Window
process memory with large pieces of code not needed.
- Potential for loading large
pieces of code on-demand. This is an optimization made by the CLR: assemblies are loaded on-demand. In other
words, the CLR loads an assembly only when a type or a resource contained in it
is needed for the first time. Here also you don’t want to overwhelm your Window process memory with large pieces
of code not needed most of the time.
- Framework features
separation. In case of very large framework, users shouldn’t be forced to embed
every features into their deployment package. For example, most of the time an
ASP.NET process doesn’t do some Window Forms and vice-versa, hence the need for
2 assemblies System.Web.dll and System.Window.Forms.dll. This is valid only
for large framework with assemblies sized in MB. For example the NUnit http://www.NUnit.com framework certainly does
not need 25 assemblies for 15.000 Lines of Code! Quote from Jeremy Miller: Nothing is more irritating to me than using 3rd party toolkits that
force you to reference a dozen different assemblies just to do one
simple thing.
- AddIn/PlugIn model, need
for interface/factory/implementation physical separation.
- Test/application code separation. If you are not releasing source code but only assemblies, you likely don't want to release tests. Not releasing test assemblies make this easy.
- When several assemblies have
been created for the reasons above, they likely need to share some common code.
Such shared code must be placed in a dedicated shared assembly.
We could add assembly as a
unit of versioning but IMHO, the need for versioning is a subset of all these
reasons enumerated above.
Invalid reasons to create
an assembly:
- Assembly as unit of
development, or as a unit of test. Modern Source Control Systems make it easy
for several developers to work simultaneously on the same assembly (i.e the
same Visual Studio project). The unit should be here the source file.
- Automatic detection of
dependency cycles between assemblies by MSBuild and Visual Studio. There are tools
such as NDepend that
can detect dependency cycles between namespaces or types of an assembly.
- Usage of internal
visibility to hide implementations details. This public/internal visibility
level is useful when developing a framework where you want to hide
implementation details to the rest of the world. Your team is not the rest of
the world, so you don’t need to create some assemblies especially to hide some implementations
details.
- Usage of internal
visibility to prevent usage from the rest of the application. If you want to
prevent usage and thus control the structure/dependencies of your code base, you should
better use some dedicated tools such as NDepend .
If you are interested in
reducing the number of your Visual Studio projects/assemblies, I would suggest
reading this blog post Hints on how to componentized existing code.
It shows how to use some NDepend dependencies features to get some hints about
which sets of assemblies should be merged.
A case study
NDepend code base is
split across 11 assemblies and here is the dependency diagram of NDepend assemblies
(made by NDepend itself):

Something you might
notice is the XDepend term. We are currently building the XDepend product (aka NDepend for Java), that will be released in 2009. More information are
available on the official website http://www.XDepend.com and I will talk more about this in
the future. This XDepend/NDepend duality leads to 2 different deployment
packages for the 2 products and this is why there are NDepend.Console.exe, VisualNDepend.exe,
NDepend.Platform.DotNet.dll on one
hand and XDepend.Console.exe, VisualXDepend.exe, XDepend.Platform.Java.dll on the other hand.
The need for 4 exe
instead of 2 was needed mostly for terminology. XDepend.Console.exe certainly makes more sense for an XDepend user than NDepend.Console.exe. All
these 4 executables are almost empty in terms of code. The console ones
initialize the platform (Java or .NET) and start the analysis implemented in NDepend.Analysis.dll while the Visual
ones initialize the platform and start the UI, implemented in NDepend.UI.dll.
The need to initialize
the platform (.NET or Java) is a motivation for isolating the code specific to
each platform. Hence the need for the 2 assemblies NDepend.Platform.DotNet.dll and XDepend.Platform.Java.dll.
This separation will also make easy potential future platform support (C++,
Delphi…).
The tool comes with 2
primary usages, analyze code and digging into analysis results through the UI.
This is the motivation for having 2 different executable assemblies: NDepend.Console.exe and VisualNDepend.exe on one hand, and 2
different libraries, NDepend.Analysis.dll
and NDepend.UI.dll on the other hand.
The idea is to avoid having both these assemblies loaded inside the same
process.
NDepend.Analysis.dll and NDepend.UI.dll both rely
on a lot of common code, core domain objects + many helper/util code, hence the
need for creating the NDepend.Framework.dll
assembly.
NDepend.CQL.dll is a lightweight assembly (less than 10KB) that contains the plumbing for
declaring CQL rules directly inside the source code.
Not only this assembly NDepend.CQL.dll
is used by NDepend users who harness this possibility, but also we use it
extensively to declare our own constraints in our code. For users who which to
declare CQL constraints inside their source code, it is more convenient to link
with a single and lightweight assembly, hence the need for NDepend.CQL.dll.
Finally, the NDepend.AddIn.dll assembly contains the
plumbing needed to register the NDepend VisualStudio and Reflector addin. This assembly references big assemblies such as the Reflector.exe assembly. In order to prevent loading by mistake Reflector.exe in one of the NDepend process, it was a good thing to create a dedicated NDepend.AddIn.dll assembly. Also, addin assemblies are registered in VisualStudio and Reflector
through their names and the name NDepend.AddIn is well-suited.
Posted
12-08-2008 10:18 AM
by
Patrick Smacchia