In the dynamic realm of software development, maintaining a coherent and consistent architecture is crucial for the long-term success of any project. As projects evolve and teams grow or change, ensuring that everyone adheres to the agreed-upon architectural principles becomes increasingly challenging. Meet ArchUnitNET, a powerful tool designed to be the guardian of your codebase, ensuring that it remains resilient to architectural drift.
Defining architectural rules 🔗
ArchUnitNET allows you to define and enforce architectural rules within your .NET projects. These rules serve as the blueprint for your code's structure, specifying how different components should interact and ensuring that the architecture remains aligned with your team's vision. By establishing a set of rules, you can safeguard against unintended deviations from your architectural design. Whether it's enforcing layer boundaries, dependency rules, or naming conventions, ArchUnitNET empowers you to articulate and uphold the architectural guidelines that matter most to your project.
But how do we achieve this? 🔗
It's relatively simple. We'll execute unit tests that holds a reference to each specific layer. We establish our rules and use reflection to verify whether they are violated.
Let's have a look. Pretend that we have this structure (with no logic):
- Web
-- WebClass.cs
- Domain
-- DomainClass.cs
- Application
-- ApplicationClass.cs
- Infrastructure
-- InfraClass.cs
-- IExternalClient.cs
-- ExternalPartySender.cs (implements IExternalClient)
-- AnotherExternalClient.cs
Let's come up with a few simple examples. You can build on this as much as you want.
- The web should never use anything from the domain layer.
- Let's avoid exposing our domain to the external world.
- The domain layer shouldn't depend on any other layers.
- Let's maintain the purity of our inner logic.
- The Application layer can incorporate everything.
- Meh, let's have this one do all the mess.
- While the infrastructure layer can have it all, we've agreed to use a client class for interactions with external parties. This stipulates that the domain should not be used here.
- We aim to prevent sending our domain to external parties, adhering to our team's agreement to use the client as a naming convention.
To safeguard our current architecture from these pitfalls, we can construct a corresponding test and apply the defined rules:
using Application;
using ArchUnitNET.Domain;
using ArchUnitNET.Fluent;
using ArchUnitNET.Loader;
using ArchUnitNET.xUnit;
using Domain;
using Infrastructure;
using Web;
using Xunit;
using static ArchUnitNET.Fluent.ArchRuleDefinition;
namespace ArchUnitTest
{
public class ArchUnitTest
{
private static readonly Architecture Architecture = new ArchLoader()
.LoadAssemblies(System.Reflection.Assembly.Load(typeof(WebClass).Assembly.GetName()))
.LoadAssemblies(System.Reflection.Assembly.Load(typeof(DomainClass).Assembly.GetName()))
.LoadAssemblies(System.Reflection.Assembly.Load(typeof(ApplicationClass).Assembly.GetName()))
.LoadAssemblies(System.Reflection.Assembly.Load(typeof(InfraClass).Assembly.GetName()))
.Build();
private readonly IObjectProvider<IType> WebLayer =
Types().That()
.ResideInAssembly(System.Reflection.Assembly.Load(typeof(WebClass).Assembly.GetName()))
.As("Web Layer");
private readonly IObjectProvider<IType> DomainLayer =
Types().That()
.ResideInAssembly(System.Reflection.Assembly.Load(typeof(DomainClass).Assembly.GetName()))
.As("Domain Layer");
private readonly IObjectProvider<IType> ApplicationLayer =
Types().That()
.ResideInAssembly(System.Reflection.Assembly.Load(typeof(ApplicationClass).Assembly.GetName()))
.As("Application Layer");
private readonly IObjectProvider<IType> InfraLayer =
Types().That()
.ResideInAssembly(System.Reflection.Assembly.Load(typeof(InfraClass).Assembly.GetName()))
.As("Infrastructure Layer");
[Fact]
public void Web_layer_should_never_use_the_domain_layer()
{
IArchRule rule = Types().That().Are(WebLayer).Should().NotDependOnAny(DomainLayer)
.Because("Web should not have any reference to the domain layer.");
rule.Check(Architecture);
}
[Fact]
public void Domain_layer_should_never_use_any_other_layers()
{
IArchRule rule = Types()
.That()
.Are(DomainLayer)
.Should().NotDependOnAny(WebLayer)
.AndShould().NotDependOnAny(ApplicationLayer)
.AndShould().NotDependOnAny(InfraLayer)
.Because("Domain should not have any references at all.");
rule.Check(Architecture);
}
[Fact]
public void Infrastructure_layer_clients_should_not_use_domain_layer_types()
{
const string caseInsensitiveRegex = @"(?i)\\*Client";
IArchRule rule =
Classes().That().Are(InfraLayer).And()
.ImplementInterface(caseInsensitiveRegex, true)
.Should().NotDependOnAny(DomainLayer)
.And().Classes()
.That().HaveName(caseInsensitiveRegex, true)
.Should().NotDependOnAny(DomainLayer)
.Because("The domain objects should not be used inside client classes.");
rule.Check(Architecture);
}
}
}
This test implementation abides the given rules and will fail when a violation happens. And you know what's even better? We should enforce this by integrating it with a continuous integration (CI) pipeline. By incorporating ArchUnitNET tests into your CI process, you can automatically validate the codebase against architectural rules with every code change. This immediate feedback loop will not only catch potential violations early, but also fosters a culture of architectural awareness within the development team.
Now, it might be more interesting to have a look at this example project yourself. You can find it here, at github. You'll be able to play around with the defined rules and make tests fail with the given examples. Please note that you can still create references between the projects. At the moment you implement a violation, the test will fail.
Why should I be so defensive? 🔗
In the ever-evolving landscape of software development, teams frequently encounter challenges in maintaining a consistent architecture. As new features are added and codebases expand, architectural drift can become a real concern, leading to a tangled and less maintainable codebase.
ArchUnitNET acts as a proactive defense mechanism, preventing architectural drift by automatically checking your code against the defined rules. This ensures that every addition or modification aligns with the established architectural principles, minimizing the risk of unintended consequences and preserving the integrity of your codebase.
A team dance with ArchUnitNET 🔗
ArchUnitNET goes beyond being a mere tool for rule enforcement; it becomes a catalyst for collaboration within your development team. By providing a shared understanding of the architectural principles, ArchUnitNET encourages developers to contribute code that aligns with the established norms. This collaborative approach ensures that the entire team is working towards a common goal, fostering a sense of consistency and predictability in the codebase.
In conclusion, ArchUnitNET empowers development teams to build robust and maintainable software architectures. By defining, enforcing, and continuously validating architectural rules, ArchUnitNET acts as a vigilant guardian.
But wait, there could be a catch! 🔗
Let's consider the potential scalability challenges as projects grow over time. With ArchUnitNET enforcing strict architectural rules, there's a risk during refactoring that the existing setup could be inadvertently disrupted, potentially introducing new issues. While ArchUnitNET is a valuable tool for enhancing and maintaining architectural integrity, it's essential to approach architectural changes with caution. Be mindful of the evolving needs of the project and ensure that adjustments to rules align with future goals and don't hinder necessary adaptations. Always maintain a forward-looking perspective to mitigate potential obstacles that may arise during the project's lifecycle.
Additionally, note that certain rules may activate later. For instance, a forbidden reference might be added, and nothing happens until a class (or rule) is applied, at which point a test will fail. Keep in mind that this solution isn't foolproof and stay aware of future developments 😉!