We have all used code analysis tools on our projects and these are useful for identifying some code smells. The issue is that most of them treat metrics in isolation and isolated metrics can’t tell you if the design is good or bad. You need more context.
In this blog post we’ll see how to go beyond code smells. We’ll see how to identify design smells and inappropriate coupling in the technical architecture. We’ll define detection strategies for common design smells (like God Class and Feature Envy) and implement them using NDepend. Last but not least, we’ll see how we can define fitness functions that detect dependency violations in our application’s architecture.
Brief overview of the most common tools
First, let’s briefly have a look at the most common code quality analyzers in the .Net world and the problems they are supposed to solve.
StyleCop‘s purpose is to enforce a set of style and consistency rules. It looks at the C# source code. Here is a StyleCop rule: Using Directives Must Be Placed Correctly
Visual Studio Code Analysis (the old FxCop) helps you adhere to
the .NET Framework Design Guidelines. It looks at the .Net assemblies. Roslyn Analyzers are gradually replacing FxCop rules. For example, this is very useful a rule enforced by FxCop: Rethrow to preserve stack details.
ReSharper is a hell of a tool, but its main purpose is to help developers refactor and navigate code. Yes, it does highlight some code quality issues, but that is just the cherry on top of a great tool. Erik Dietrich has blogged about the NDepend vs. ReSharper comparison.
SonarQube is a platform designed for continuous inspection. So this is more in line with what I’m looking after. But, the problem is that SonarQube is more of a pluggable platform. It doesn’t come with many rules out of the box. You actually plugin other code analyzers (like StyleCop, FxCop, Roslyn Analyzers, NDepend) to gather the metrics.
Issues with most code quality analyzers
These are all good tools and they are good at what they do. But they can’t really help protect the design and architecture.
Isolated Metrics
All these tools treat metrics in isolation. This means that they might tell you that your class is too complex and it’s not cohesive. But it doesn’t put 2 and 2 together and tell you: hey, this is a God Class. This means that the developer looking at the data needs to make this decision. But it doesn’t have to be that way. You can automate this detection.
Another downside is that, since they treat metrics in isolation, it doesn’t give you a hint of where to start refactoring and how. It’s like the tool is telling you that you have a sore throat, a runny nose and a cough, instead of telling you you might have a cold. It can make you feel overwhelmed. I remember running Sonar on a legacy code base and looking at the dashboard, not knowing where to start. It felt like searching for a needle in a haystack.
Focused at the Code level
The second disadvantage (and it’s tightly related to the first one) is that these tools focus on the code. But, I would like more – I would like some hints that my design is wrong. If the symptoms are grouped into well known design smells you’ll have less information to look at and a strategy on how to deal with them.
Hard to extend
And last but not least, these tools are not that easy to extend. I’ve implemented (many years ago) some StyleCop and FxCop rules. It wasn’t rocket science, but it did need a fair amount of code. I also used the FxCop and StyleCop plugins for SonarQube. But, as an example, SonarQube didn’t integrate all of FxCop’s extensiblity points (like adding a Custom Dictionary) . This meant we needed to mark a lot of issues as false positives.
Enter NDepend
So what’s the alternative? We can use a tool that is specifically designed for the job. This is NDepend’s description on the NDepend site:
NDepend is a “Swiss Army Knife” for .NET and .NET Core project teams. With its wide range of features, it gives deep insight into code bases and empowers developers, architects and executives to make intelligent decisions on projects.
NDepend Web Site
So, I like what I’m seeing. I want insight, not just information. And I want to know what to do with that information – so I want to feel empowered.
I remember seeing another good description of NDepend in an online presentation (can’t remember the title or the presenter, sorry!). The presenter defined NDepend as The Rolls Royce of static code analyzers. And I think it’s that, and then some. It’s a Rolls Royce that can transform into a plane or a submarine, depending on your needs. It just gives you so much power. I’ve blogged before about how NDepend can help you Visualize Dependencies, Query your Code Base, and Visualize Code Metrics. But let’s see specifically how it can help you protect the design and architecture of your system.
A word on Architecture and Design
Grady Booch makes the following distinction between Architecture and Design in his “On Design” paper:
All architecture is design but not all design is architecture. Architecture represents the significant design decisions that shape a system, where significant is measured by cost of change.
Grady Booch
There isn’t a hard line between design and architecture and this line might be moving, as some things get easier to change over time. I consider the architectural style (Clean Architecture, Layered Monolith, SOA, etc.) of an application an architectural decision.
Protecting the Design
So how can we protect the design? First, we need to know what is good design and what is bad design. After that, we need to automate as much as possible. To do this, we need to formally define what’s good and what’s bad. How? In terms of correlated metrics, of course. Let’s see some examples.
Good Design
Good design adheres to design principles. SOLID, DRY, KISS and Uncle Bob’s component coupling principles are a few examples. But, to automatically detect code that does not adhere to these principles, we need to be able to express them through metrics. This can be hard to do for some of them. Let’s take the easy route and look at some principles that are already defined using metrics.
Component Coupling Principles
In Clean Architecture, Uncle Bob defines three principles related to the coupling of components in a system. These are the Acyclic Dependencies Principle, the Stable Dependencies Principle and the Stable Abstractions Principle. I’ve already documented how to implement these with NDepend in a previous blog post.
Bad Design
Design Smells
Bad design smells! There are many books written about how to identify design smells. I read three:
- Refactoring by Martin Fowler briefly describes common code smells and how to refactor to get rid of them.
- Refactoring for Software Design Smells: Managing Technical Debt by Girish Suryanarayana, Tushar Sharma, and Ganesh Samarthyam goes deeper into the subject. It contains a catalog of 25 structural design smells and refactoring options, with clear examples.
- Object-Oriented Metrics in Practice by Michele Lanza and Radu Marinescu.We’ll focus on this book in this blog post for a simple reason: while the other two describe how to identify code smells, they do it informally. This means that a person needs to analyze and interpret the data. In Object-Oriented Metrics in Practice, the authors present an automated way of identifying suspects of common design smells by using Detection Strategies.
A Detection Strategy is a composed logical condition, based on a set of metrics for filtering. Basically, a detection strategy uses metrics to identify design disarhomonies. Most of the design smells are defined only informally, without the use of metrics. This is why the authors propose this process for defining a detection strategy:
- Select informal design rules that describe the design flaw
- Identify Symptoms – break down the informal design rule into symptoms that can be captured by a single metric
- Select Metrics that can quantify each symptom
- Select Filters – for each metric, define a filter for capturing the symptom
- Compose the Detection Strategy
You can see detection strategies from common design smells implemented with NDepend here. Have a look if you ever wondered how you could automatically identify a God Class in your code.
But good design is, to some extent, subjective. Different teams might value different design principles. The beauty of tools like NDepend is that if you can measure it, you can implement it. So it’s up to you.
Antipatterns
Antipatterns are practices that were once a good idea, but are now considered harmful. An example is the Service Locator antipattern. Good use of Dependency Injection means that only one location in the application knows about DI – the composition root. So let’s see how we can detect wrong usages of an IoC container.
// <Name>Autofac references outside of Composition Root</Name>
warnif count > 0
let autofacModule = ThirdParty.Types.SingleOrDefault(t => t.FullName == "Autofac.Module")
let autofacAssemblies = ThirdParty.Assemblies.Where(a => a.NameLike("Autofac.*"))
from autofacAssembly in autofacAssemblies
from type in JustMyCode.Types
where
type.IsUsingAssembly(autofacAssembly) &&
(autofacModule == null || !type.DeriveFrom(autofacModule)) &&
type.Name != "Program"
select new { type, autofacAssembly }
In this case we’re using Autofac and the composition root is the Program class. We are also excluding classes that derive from AutoFac Module, since this is a common way of grouping dependencies. Of course, the composition root might be different, depending on the type of application. You can extend the rule above to match the composition root for the type of application you are implementing and the IoC container you are using.
Protecting the Architecture
Why would we need to do this? Here is a tweet that explains it quite nicely:
Building Evolutionary Architectures introduces the concept of a fitness function:
An architectural fitness function provides an objective integrity assessment of some architectural characteristic(s).
Building Evolutionary Architectures
So fitness functions are ideal candidates for protecting the architecture of a software system. They provide the right balance between top-down rules and bottom-up emergence. Almost all architectural styles and patterns have some strict dependencies rules. This makes NDepend a good candidate for implementing them. Let’s see some examples:
Clean Architecture
Uncle Bob describes this architectural style in this blog post and in his book. A clean architecture is defined by the Dependency Rule: software dependencies can only point inward. Let’s implement this rule for an architecture that emphasizes the Domain Model. Each layer (domain, application, infrastructure) is represented by a namespace. For example, in the MyShop.Finance application, the domain code will be in the MyShop.Finance.Domain namespace.
// <Name>Clean Architecture - Domain should not reference Application</Name>
warnif count > 0
from domainComponent in Namespaces
where domainComponent.Name.Contains(".Domain")
from domainType in domainComponent.ChildTypes
from applicationComponent in Namespaces
where applicationComponent.Name.Contains(".Application")
from applicatinType in applicationComponent.ChildTypes
where domainType.IsUsing(applicatinType)
select new {
domainType,
applicatinType,
Severity = Severity.Blocker
}
The rule above will identify all domain types that reference application types. You can easily create similar rules for matching domain types that reference infrastructure types and application types that reference infrastructure types.
Layered Architecture
In a layered architecture, a layer can only talk with layers below it. In strict layered architecture, a layer can only talk with the layer below it. The rules are very similar to the one above. The only difference is the naming convention.
Service-Oriented Architecture
In a service-oriented architecture, services should be decoupled and only communicate through messages. The messages assembly is the contract of a service. This is what Martin Fowler calls a Published Interface.
So let’s see how would we implement this rule. We’ll assume that all assemblies follow the [CompanyName].[ServiceName].[Component] naming convention. We’ll also allow reusable code in library projects that follow the [CompanyName].Library.[Component] naming convention.
// <Name>SOA - Service References</Name>
warnif count > 0
let serviceForAssembly = new Func<IAssembly, string>(a => a.Name.Split('.')[1])
let serviceForType = new Func<IType, string>(t => serviceForAssembly(t.ParentAssembly))
let isPartOfServiceContract = new Func<IType, bool>(t => t.ParentAssembly.Name.EndsWith("Messages"))
let isLibrary = new Func<IType, bool>(t => t.ParentAssembly.Name.Contains("MyShop.Library"))
from type in JustMyCode.Types
let service = serviceForType(type)
from usedType in type.TypesUsed.ExceptThirdParty()
let serviceForUsedType = serviceForType(usedType)
where service != serviceForUsedType
&& (!isPartOfServiceContract(usedType) && !isLibrary(usedType))
select new {
type, service, usedType, serviceForUsedType
}
Anti-corruption Layer
An anti-corruption layer (ACL) is a concept from Domain-Driven Design. Its purpose is to protect a bounded context from the influences of other contexts. Let’s say that you need to integrate with a shipping provider (FanCourier) in your application, but the model that you use doesn’t map to the model used by the shipping provider. To protect your domain, you put an ACL in front of it. All communication with FanCourier should go through the ACL. From this definition you can extract a dependency rule: only the ACL should depend on FanCourier.
// <Name>ACL - Fan Courier</Name>
warnif count > 0
let thirdPartyAssembly = Assemblies.WithName("FanCourier").Single()
from thirdPartyType in thirdPartyAssembly.ChildTypes
from typeUsingThirdPartyType in thirdPartyType.TypesUsingMe
where typeUsingThirdPartyType.ParentAssembly.Name != "MyShop.ItOps.FanCourier.Gateway"
select new {
typeUsingThirdPartyType,
thirdPartyType,
Severity = Severity.Blocker
}
At line 4 we identify the third party component and at line 8 we match the ACL – MyShop.ItOps.FanCourier.Gateway.
Versioning
Versioning is another important architectural decision. Many teams decide that changes that are not backwards compatible should cause a change in the major version number. NDepend already implements rules for detecting API breaking changes. We can easily customize them to only match changes when the major version was not increased. For example:
// <Name>Semver API Breaking Changes: Methods</Name>
warnif count > 0 from m in codeBase.OlderVersion().Application.Methods
where m.IsPubliclyVisible &&
(m.ParentAssembly.Version.Major == m.ParentAssembly.NewerVersion().Version.Major) &&
// The rest of the rule here
At line 5 we check that the major version has changed between two versions of the code base.
Things to consider
Although NDepend makes it easier to protect the design and architecture of your system, there are things you need to watch out for.
Exceptions, exceptions, exceptions
If you implement these rules, you have a good chance of needing to add exceptions. One example is the rule that matches Data Classes. If you use DTOs, you’ll have lots of false positives. So you’ll need to filter out these types, which will make the queries more complicated.
Updating the fitness functions
If you have fitness functions, you should have a process for updating them. You don’t want everyone updating the rules and adding exceptions without proper thought. But, you also don’t want these rules to become a bottleneck. The deployment pipeline should fail the build if it doesn’t comply to the rules. But, if this is a proper exception, it shouldn’t take days to update the rule. The process needs to balance the top-down enforcement with bottom-up emergence.
Conventions make your life easier
Following strict conventions makes implementing these rules much easier. You could use a naming convention, like in the examples in this blog post. Or, you could add marker attributes to code elements. That’s all well and good on greenfield projects, but it’s trickier on brownfield products. On this type of projects, you first need to add conventions and change your code to comply to them.
Managing edited rules
As we saw in the versioning example, we can edit the default NDepend rules, which is great. But, the rules are updated, so with new releases you’ll need to check if there were any updates and update your own rules. So you’d be basically branching the default ruleset (let’s call this main) and on every new NDepend release you’d need to merge the changes from main to your own branch.
Duplication
If you implement the rules in CQLinq, like I did in this post, there’s a fair chance you’ll duplicate some queries. One example is the definition for the new metrics used for detecting design smells. You could implement these with the NDepend API. You’ll lose some of the interactivness of CQLinq, but you’ll remove duplication and your queries will be faster, since you can compute the metrics only once. Also, there’s already a User Voice item for this feature, so it’s likely we’ll get this in a future version of NDepend.
Running the rules in your Deployment Pipeline
We’ve defined a set of rules that can run automatically. The next step is to integrate them into your deployment pipeline. You can do this in two ways. The first and most obvious is to run NDpend as part of your build step. There are plugins for some of the CI/CD tools out there, like Azure DevOps and Team City. The second option is to implement the checks using NDepend API as a set of unit tests.
Conclusion
In this post we’ve seen how you can leverage NDepend to protect the design and architecture of your system. So, what should you do now? First, discuss with your team and see what are your system’s most important structural characteristics. Then try to implement fitness functions for them. I’ve shown how to do this with NDepend. You can use JArchitect if you’re working with Java. There are other alternatives, although they might be less extensible. For example, I know two other options for enforcing simple dependency rules in .Net. NetArchTest allows writing unit tests that enforce architecture dependency rules. Visual Studio Enterprise also has a Dependency Validation check.
Good luck and may your system be protected against architectural decay!