๐๏ธ Friend Assemblies compromise for Software Engineering
A practical way in .NET to encapsulate modules' internals, helping you drive a good-enough software architecture
If you're a software engineer ๐ท, you most definitely use some kind of architectural design pattern like Domain-Driven-Design principles (๐ซ not DDD) or IDesign principles or something else; doesn't matter that much for the sake of this article.
Now, with any kind of architectural pattern you need some kind of modularization ๐ฆ and therefore some means of encapsulating the internal workings of these modules.
For instance, in DDD, you don't want all the internals of <<say>> the accounting domain to be exposed to <<say>> the manufacturing domain.
Or in IDesign, you don't want to expose engines and resource accessors to consuming clients.
Or in general, you don't want to expose the internal business logic and models to the consuming client.
In an ideal world ๐, with unlimited time, effort and money, you'd achieve this by completely separating the contracts (aka interfaces and exposed models) from their concrete implementations AND truly decouple them! Meaning no compile time reference to the concrete csproj! Such binding would need to happen only at runtime based on some conventions or dependency config files and factories. And this would need to be done for each and every module, with no exception. And make no mistake, I totally ๐ฏ recommend this approach if you have the luxury to do so!
Unfortunately, in our real world, more often than not, โ๏ธthe software engineersโ๏ธ go full JavaScript style ๐คข and reference everything from everywhere, most of the time without even realizing it, through ignored reference chains.
And just like ๐ฎ Seรฑor JavaScript/FE ๐คข developers ๐ฎ, when corners are cut and ๐ฉ is produced, they leave it there, call it technical debt, and comfort themselves that they'll compensate it with self discipline. And that discipline lasts for a day or two until they need to switch focus on something else.
In many cases, they even do put interfaces on the concrete types but blindly, just for the fun of it, without any practical value.
"Just because that's how good software looks like ๐ง ... and it has something to do with the I in SOLID..." ๐คฆ ๐.
The fully decoupled approach, is clean, sexy, beautiful, sublime, extensible, solid, maintainable but hard to sell to most of nowadays โ๏ธsoftware engineersโ๏ธ and time consuming and effort intensive compared to the latter ๐ฉ, which is very handy and fast to write but it's painful if not impossible to upkeep for the long run. Trade-offs, as usual.
๐คHello, Friend Assemblies!
So, a good-enough compromise to mitigate the effort of encapsulating the concrete internals of modules, in .NET, is to use friend assemblies.
With this approach you tell the compiler that all internal
artifacts of a given .NET assembly act like public
artifacts only for other specific friend assemblies. And that's how you achieve module encapsulation!
This is done via the InternalsVisibleTo attribute.
As a usage example, say our DataAggregatorA module exposes itself only to BusinessLogicA and BusinessLogicB modules. Therefore, everything in DataAggregatorA needs to be marked internal, and then it has to explicitly declare the two friend assemblies. Like below.
In DataAggregatorA
csproj, we add a new root file called FriendAssemblies.cs
with the following content.
And that's it. Module encapsulated. Any assembly other than BusinessLogicA or BusinessLogicB that references DataAggregatorA, directly or indirectly (via a reference chain that contains BusinessLogicA or BusinessLogicB), will not have any visibility inside DataAggregatorA.
The effort for doing this is minimal, and the benefit is huge, as you mimic a great deal of that utopian encapsulation scenario. So there's no real excuse for not doing it!
๐Encapsulation Example
Let's analyze the depicted example.
- We have a consumer module that needs to run a given business operation for a specific input model and get back a specific result model.
- As it should, the consumer only knows about the business operation and the exchange models. Nothing more, nothing less.
- The business operation module needs to orchestrate various internal business logic modules to achieve the end result. Therefore it needs to know about:
- The business logic modules
- The internal data models to interact with the business logic modules
- The public data model to project the end result
- Each business logic module needs to know about some internal resource modules to be able to do their thing.
- The resource modules don't need to know about any other module.
Each piece can be a separate module, or grouped together per layer, like ResourcesLayerModule, BusinessLogicsLayerModule, BusinessOperationsLayerModule, PublicModels, PrivateModels, etc.
This architecture would translate into the following friend assemblies:
- Internal DataModel and Operations are friends with and referenced by:
- Resources
- Business Logic
- Business Operations
- Resources are friends with and referenced by:
- Business Logic
- Business Logic are friends with and referenced by:
- Business Operations
- Business Operations are not friends with anyone, they expose public contracts and models for consumers.
In IDesign.
- Internal Model is friend with and referenced by:
- Resource Accessor
- Engine
- Manager
- Resource Accessor is friend with and referenced by:
- Engine
- Manager
- Engine is friend with and referenced by:
- Manager
- Manager has no friend assemblies, but references both internal model and public model and handles the mapping between the two.
- Client references manager and public model but has no access to internal engine, resource or model, even though they're part of the reference chain.
A further reading that adds to this topic and presents a neat way to hide the concrete implementations between friend assemblies themselves:
https://hintea.com/internal-modules-concrete-encapsulation/