When we structure our code into assemblies (generally named binaries, libraries or packages in other platforms than .NET) we are reasoning about three main things:
- Deployment: different assemblies are deployed on different containers. Some assemblies end up on the UI client, some on the application server and some on third party servers we may use;
- Separate Concerns: we put code that addresses similar concerns in one assembly and we separate it from the code that addresses different concerns. This may translate into encapsulate the implementation of one functional area into a module and offer it through an abstract interface to others. It may also translate into separate the data access concern from business logic;
- Assure Consistency: we want that certain things to always be done in the same way through the entire application, so we define an assembly that will be reused by other assemblies around our application
Another important aspect of referencing assemblies (or linking binaries) is that we cannot have circular references. This may play an important role in managing dependencies. It may help us to avoid circular dependencies among modules or components if we carefully map them to assemblies.
Each time I design the main structure of a new application I do it with all the above in mind. These, used well can bring huge advantages in managing the growth of the application. Even if it is about a rich desktop UI client, which has in same process the business logic and a local database, so the entire application deploys in one container, I will still have more assemblies because I want to use the other advantages. I want to use assembly references to enforce separation of concerns and to enforce consistency. These are the most important tools to manage the complexity of the application, which is critical in large applications developed by more people or even more teams.
When the initial assembly structure is defined we have: the assemblies (or assembly types) and the rules of how they reference each other. This should satisfy the deployment requirements and it should reflect the concerns that must be separated and the things that must be done consistently. I usually put it in a simple diagram with boxes for assemblies and arrows for allowed references. Where there are no arrows, there cannot be references. This diagram not only that helps to explain and verify the design, but it can also be used when reviewing the implementation. If in code we see references that are not in the diagram it may be a fault in the implementation (encapsulation or abstraction leaking, code at wrong level of abstraction, etc.) or it may be a case which was not handled by the design, so the diagram needs to be adjusted.
Let’s dive into details, by looking at some examples.
Logging, is a cross-cutting concern, which in most of the cases we implement by using a third party library. We look for a library (Log4Net for example) which has a high degree of configurability and it can log in files, in databases or send the traces to a web service. In all the cases where we write a log trace, we want to specify in the same way the type, the criticality, the priority and the message. We want to use Log4Net in the same way everywhere in our app. Consistency is important. When something needs to be changed, we want to be able to do the change following the same recipe in all the places where we log.
We can easily enforce this by wrapping the external library in one of our assemblies. Our assembly defines the Log interface which we’ll use in the application. This interface shapes the logging library to our application specific needs. All the configuration and tweaking is done now in one single place: our Logging assembly which implements the Log interface. It is the only one that may reference Log4Net. The rest of the code of the application doesn’t even know that Log4Net is used.
In general any external library gives a very generic API and it is very extendable to many kind of applications. The most applications the library fits, the most successful the library is. When we plug such library in our application we need to tweak it to our specifics and we need to use it in the same way in all the cases.
Even if wrapping it is a very simple solution, it is very powerful. It isolates the change. If something needs to be changed in how the external library is configured, now we don’t need to go through the entire application where it was used. It is directly used in only one place: our wrapper assembly. Even more when we need to replace the external library or to upgrade it to a new version the changes are again isolated in our wrapper. We can isolate in our wrapper all the concerns of communicating with the external library which may include concerns about communication with external systems, security concerns, error handling and so on.
In this example the only assembly that can make a connection to the database is the DataAccess assembly. It implements all the data access concerns and offers an abstract interface to above layers. Even more, it does not contain the data model classes, so the business logic (validations or business flows) are kept outside. For more details on how this could be implemented you can refer to my previous post: Separating Data Access Concern.
Here we can see that we do not have references between the assemblies that implement the business logic, the Functional Modules. They communicate only through abstract interfaces placed into the Contracts assembly. The Contracts assembly contains only interfaces and DTOs. No logic. With this we make sure that we will not create dependencies between implementation details of the functional modules. The functional modules can access data through the DataAccess assembly, but they cannot go directly to the database. They don’t have any UI logic since they do not have references to UI frameworks assemblies (like System.Web or System.Windows). The UI assembly gets the functionality and data only through the abstract interfaces from Contracts assembly. They can’t do data access otherwise. All are linked through Dependency Injection, which is abstracted by the AppBoot assembly.
To conclude, even if you didn’t start with all these in mind when you’ve created the assemblies of your app, I think it worth the effort to draw such a diagram at any moment, because it will show opportunities to bring more order, clarity and better ways to manage the size of your project.
this design approach is discussed in detail in my Code Design Training