6

Clean Architecture Refactoring: A Case Study

 3 years ago
source link: https://blog.ndepend.com/clean-architecture-refactoring-a-case-study/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Introduction to Clean Architecture

The recent post Clean Architecture for ASP.NET Core Solution: A Case Study explained that one of the most interesting property promoted by Clean Architecture is the abstraction of the infrastructure code. This way the application can consume the infrastructure code without being bound with its implementation. The infrastructure code represents basically all kinds of frameworks an application can use: UI, persistence, network…

This other post Implementing a Domain with POCO (Plain Old CLR Objects) explains that the domain layer should only rely on basics and primitives CLR types like string, int, bool… The post highlights that the domain can implement the business rules that don’t require an infrastructure access, typically data validation and transformation. The application layer, which is just above the domain layer, defines infrastructure abstractions. It can thus implement business rules that requires infrastructure access (persist data when …, send an email when …).

Finally the infrastructure implementations defined in a dedicated infrastructure layer are injected in the application layer from a layer that sits on top of infrastructure and application, like WebUI for example in the diagram above.

The Case Study

.NET is evolving quickly and it is now time for us, at NDepend, to migrate to multi-platforms .NET code. The first step to achieve, is to make the analysis and reporting available on Linux and MacOS. The UI will follow later.

To make this code runnable on multiple platforms, all the domain and analysis code needs to be implemented against .NET Standard. This way it will be invokable from .NET Core, from .NET 5 or 6 code, and from .NET Framework code: indeed we still need to run on .NET Framework since Visual Studio, that hosts our extension, is running on .NET Framework. Even Visual Studio 2022 recently announced will still run on .NET Framework.

What prevents a direct migration to .NET Standard is that the domain and analysis code is mixed with some UI code in the project NDepend.Core. This is the result of 15 years of work, targeting only the .NET Framework and the Windows UI stack (Winforms and WPF). Clearly this is a violation of the domain/infrastructure segregation Clean Architecture principle.

In this post I will explain:

  • How we refactor to segregate the domain code from the infrastructure code (UI code)
  • How the fact that our components are already layered makes this move much easier, a matter of a few days

To make things more concrete, here is the goal of the refactoring:

Matching classes of NDepend.Core that needs to be Refactored or Moved to NDepend.UI

The first thing to do is to identify classes in NDepend.Core that are using a UI framework. For each of these classes:

  • Either it is a pure UI classes (like a form or a control) that needs to be moved up to NDepend.UI. In such a case, we must make sure first that it is not used anymore from another classes in NDepend.Core.
  • Either the class slightly uses UI and is a good candidate to remain in NDepend.Core to anticipate when the UI will be multi-platform. Indeed, the goal is to end up with as much code as possible compiled to .NET Standard that can be run on any platform. Thus for such class we must either keep it in NDepend.Core by abstracting the UI usage or split it to move up its UI part to NDepend.UI.

To identify all the concerned classes, the raw ways would be to discard UI framework references from NDepend.Core, recompile and churn the compiler errors. This would be quite tedious considering that we are talking of more than 300 classes and thousands of methods!

Instead to plan the refactoring work, I wrote a code query that matches 307 types declared in NDepend.Core that somehow consume a UI framework:

An interesting point is that this code query is modular enough to be adapted to many refactoring situations. Here is the plain-text query so you can customize it to your needs:

// <Name>NDepend.Core Types using UI</Name>
let tUI = ThirdParty.Assemblies.WithNameWildcardMatchIn(
    "System.Windows.*",
    "System.Drawi*",
    "WindowBase*",
    "Presentation*",
    "Microsoft.Msagl.GraphViewerGdi*").ChildTypes().ToHashSetEx()
let tUsingUI = Assemblies.WithNameIn("NDepend.Core").ChildTypes()
.UsingAny(tUI)
from t in tUsingUI  
let usageFromCore = Assemblies.WithNameIn("NDepend.Core").ChildTypes()
   .UsingAny(t.ToEnumerable() )
//where usageFromCore.Count() == 0 (see below why it is commented)
select new {
   t.NbLinesOfCode,
   locJustMyCode = JustMyCode.Contains(t) ? t.NbLinesOfCode : null,
   mUIUsed = tUI.Intersect(t.TypesUsed),
   usageFromCore

Before explaining how we refactored these 307 classes, let’s explain how we are layering components. Indeed, this will be a key point to simplify the refactoring.

Layered Components

Per convention, in our code namespaces represent components. By following the rule Avoid namespaces dependency cycles for more than a decade we ended up with 400+ namespaces (and hence components) completely layered. All those 400+ components can be visualized in a slick triangular Dependency Structure Matrix.

In this matrix, rows with a lot of blue indicates a popular component consumed by many components above it. On the other hand, columns with a lot of blue indicates complex components that consume many lower-level components.

An heuristic is applied to square the matrix as much as possible. Such triangular pattern indicates an highly cohesive grape of components.

Simplified Refactoring thanks to Layered Components

Let’s go back to our code query that matches the 307 classes to refactor. By clicking the button Export to Graph in the query result, we obtain a dependency graph with the 307 classes nested in their parents namespaces. With no surprise, the parent namespaces are layered. This leads to an order to refactor these classes, from the top ones to the bottom ones. The benefits are:

  • A) There is no refactoring drama, like when the code is not compilable nor testable for days. The refactoring is progressing class by class. The amount of remaining work is measured by the number of classes still matched by the query.
  • B) Imagine whether the same large-scale refactoring should occur on an entangled architecture like the one below. Pretty much any class in this structure is dependent on all other classes. This is why hovering a class leads most other classes and edges to be highlighted in red. Where to begin? In such situation, prior to any significant refactoring, all components – or at least most of components -should be layered first. But doing so on hundreds of classes could take weeks or months. This is the infamous spaghetti code anti-pattern!

Getting details to guide Refactoring

The code query presented above provides 2 useful columns:

  • Column mUIUsed: for each class to refactor we can see the UI types it consumes:
  • Column usageFromCore: for each class to refactor we can see the classes from NDepend.Core that depend on it. This is useful because those incoming dependencies prevent a simple move up to NDepend.UI or alternatively, simple UI abstraction/injection. For that reason, those incoming dependencies generate the bulk of the refactoring work (as explained in the next section).

The remark above implies that the matched classes with no usage from NDepend.Core are good candidates to be refactored first! They can be listed by uncommenting the clause where usageFromCore.Count() == 0 in the original code query. Here 60 classes are matched. Some can be readily moved up to NDepend.UI, like the Forms ones. Some others will require some investigation to see if they can remain in NDepend.Core by abstracting the UI usage.

Using Dependency Matrix to Demystify Complex Situations

To demystify situations when a type is using the UI but is also used by other NDepend.Core types, we can generate a dependency matrix that will be an eye opener. Let’s take the interface IRowItem as an example. We can copy the NDepend.Core classes that depend on IRowItem to the dependency matrix columns:

And then copy IRowItem itself to the matrix rows:

We then obtain a dependency matrix that sheds light on the situation.

Here the IRowItem interface is using UI just because it has a System.Drawing.Image Image { get; } property. It is used by some rows algorithms (sorting, comparing…) implemented in 5 classes. Let’s remind the goal to end up with as much code as possible compiled to .NET Standard that can be run on any platform. It would be a bummer to move up IRowItem and these 5 classes to NDepend.UI just because of this property. When we will support more UI frameworks, we’ll want to re-use as-is all these rows logic.

To solve this situation, we defined an interface IImageHandle in NDepend.Core and our property becomes IImageHandle Image { get; }. IImageHandle is implemented in NDepend.UI, injected by NDepend.UI and consumed only in NDepend.UI. Taking account that the notion of IRowItem is already an UI abstraction, introducing IImageHandle makes sense.

Last step: ensuring .NET Standard Compliance for NDepend.Core

Removing the NDepend.Core adherence to any UI Framework represents the bulk of the work to make it compilable on .NET Standard and consequently, to make it runnable on multiple platforms. But UI is not the only infrastructure to care for. Indeed .NET Standard doesn’t support 100% of .NET Framework System‘s libraries.

This 2018 post – Quickly assess your .NET code compliance with .NET Standard – I explained how an astute code query can be written to spot the part of the code that is not .NET standard compliant. The idea is to analyze all the code and also the DLL netstandard.dll at once. This DLL can be found for example in C:\Program Files\dotnet\sdk\NuGetFallbackFolder\netstandard.library\2.0.3\build\netstandard2.0\ref (adjust the version). This DLL contains all the .NET Standard API but no implementation.

Here is the astute: By analyzing your code with netstandard.dll at once, a code query can be written to spot .NET Fx types used by the application that are not defined in netstandard.dll. Below is the resulting code query executed against NDepend.Core. It lists both our code that cannot be compiled against .NET Standard and the non compliant code elements consumed.

Hopefully it is not much! We can see that NDepend.Core depends a bit on .NET Remoting, that is not .NET Standard compliant. Thus we just have to replace this with another Inter process Communication (IPC) framework.

Summary

In this post we saw that it is important to refactor code to abide by one of the most important Clean Architecture principle: domain code shouldn’t consume infrastructure code, which is mostly UI Fx in our case.

Then we showed that a large-scale refactoring can be planned thanks to some code queries. Clearly the fact that the code was already layered helped a lot. It took only 8 days to refactor these 300+ classes. Around a third of them naturally bubbled up to NDepend.UI (controls, forms…) and the remaining were kept in the .NET Standard project NDepend.Core with a bit of work.

Something not mentioned yet, is the fact that the code was almost entirely covered by tests (92% for NDepend.Core). This clearly helped a lot. It took only 8 days because we were confident that we didn’t break anything: the test suite was executed several times a day and it caught a dozen of regressions introduced by mistake during this refactoring session.

You can refer to this article 2 Simple Principles to achieve High Code Maintainability that explains more why Layered code and High Test Coverage Ratio are two essentials properties that lead to maintainable code. Code that is easy to refactor and evolve, no matter the future requirements.

Let’s conclude on the satisfying code coverage view of NDepend.Core. Each rectangle represents method grouped in types, then grouped in namespaces. The color indicates the code coverage ratio in the range [0% to 100%].

My dad being an early programmer in the 70's, I have been fortunate to switch from playing with Lego, to program my own micro-games, when I was still a kid. Since then I never stop programming.

I graduated in Mathematics and Software engineering. After a decade of C++ programming and consultancy, I got interested in the brand new .NET platform in 2002. I had the chance to write the best-seller book (in French) on .NET and C#, published by O'Reilly (> 15.000 copies) and also did manage some academic and professional courses on the platform and C#.

Over the years, I gained a passion for understanding structure and evolution of large complex real-world applications, and for talking with talented developers behind it. As a consequence, I got interested in static code analysis and started the project NDepend.

Today, with more than 8.000 client companies, including many of the Fortune 500 ones, NDepend offers deeper insight and understanding about their code bases to a wide range of professional users around the world.

I live with my wife and our twin babies Léna and Paul, in the beautiful island of Mauritius in the Indian Ocean.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK