How to fix poor project structure using convention based installation

2920
How to fix poor project structure using convention based installation

Having looked at a number of projects in my lifetime, I always come across classes named something like "CustomerService" with similar variations (usually in the same project calling each other) ranging from "CustomerProvider / Helper /Manager / Store / etc...". 

There are two hard things in computer science: cache invalidation, naming things, and off-by-one errors - PhilKarlton

As a new developer working on a project it becomes really hard to figure out what the structure is, and in the case of adding code what to name classes. Naming and structure always seem to be a developers achilles heel(almost akin to superman and kryptonite).

So, I wanted to come up with a solution to the problem, something more structured that helps facilitate better naming and structuring and the way I decided to do this is through dependency injection. By now we are all familiar with Inversion of control, and comfortable using it to decouple dependencies within our applications. Most of us have dabbled with the usual suspects CastleWindsor, AutoFaq, Ninject to name a few. One thing people don’t realise is we can also utilise these frameworks to enforce good structure as well as unit test the structure itself to ensure new developers don’t stray from the named path. For the examples below I’m going to use Castle Windsor.

Step 1 – Define the structure and start naming the onion layers

The codebases of yester-year were usually architected using a n-tier structure typically following the pattern: 

Presentation Layer (Controller) > Business Layer (Service) > Data Layer (Repository)

As time progressed new patterns emerged the structure became more complex however we still try to adopt some form of onion layering within the application. Whether it’s one onion or many within a single solution we should always strive to define what the layers are in the application.

So, to start we should define:

1.      What the onion layer is named (forming groups of similar classes).

2.      What the responsibility of each layer is.

Step 2 – Create your conventions

Now that we have some understanding of the layers, we can start defining them in code. I use an empty interface to do this. Note Castle distinguishes these layers as ‘Conventions’.

/// A class that contains business logic, it also does not directly access any data source.
public interface IService
{
}

Note the interface has no bearing on logic, and does not alter how the app behaves. It’s simply used as a marker to distinguish the layers of the application. A small description is also provided to define what the responsibility of the layer is. These conventions are also a way to document the structure of the application.

Step 3 – Install all dependency’s using the convention.

Now that we have a convention we can blanket install all classes subscribing to that convention, if your using castle Windsor there is a slight difference in how this is done depending on whether you apply the convention directly on the class itself or if you apply it to another interface.

Applying it to an interface

/// A class that contains business logic, it also does not directly access any data source.
public interface IService
{
}

/// Blanket install all IServies
container.Register(Classes.FromAssembly(Assembly.Load("Assembly name goes here"))
    .BasedOn(IService)
    .WithService.AllInterfaces()
    .LifestyleSingleton());

/// Example usage
public interface ICustomerService : IService
public class CustomerService : ICustomerService

Applying it to a class

When applying it to class the installation has a slight difference.

/// A class that contains a business rule, it validates whether the rule has been met
public interface IRule
{
 string ApplyRule();
}

container.Register(Classes.FromAssembly(Assembly.Load("Assembly name goes here"))
 .BasedOn(IRule)
 .WithService.Base()
 .LifestyleSingleton());

public class CustomerRule : IRule

When creating a new class that fits within a pre-defined convention installation becomes a walk in the park, just apply the convention interface and you’re done.

Step 4 – Unit testing structure

Now that we have our convention setup and we are installing all classes with that convention we can apply a unit tests that will check against the structure. We are testing on two things here:

1.      Only Services should have a‘Service’ Suffix

2.      Only Services should exist in a ‘Service’ namespace

[TestFixture]
public class TestSolutionConventionTests
{
    [SetUp]
    public void Setup()
    {
        // Register all dependencys in the project using castle
        RegisterDependencies(); 
    }

    [Test]
    public void OnlyServices_HaveServiceSuffix()
    {
        // Get access to IWindsorContainer
        var container = DependencyResolver.Container; 
        // Get all classes in the application where the name ends with Service (using reflection).
        var allServices = GetPublicClassesFromApplicationAssembly(c => c.Name.EndsWith("Service"), "Assembly name where service exists goes here");
        // Get all services installed within castles container that use the interface IService
        var registeredServices = GetImplementationTypesFor(typeof(IService), container);

        // Assert the names all match and are equal
        allServices.ToList().Should().Equal(registeredManagers, (ac, rc) => ac.Name == rc.Name);
    }

    [Test]
    public void OnlyServices_LiveInServicesNamespace()
    {
        var container = DependencyResolver.Container; 
        // Get all classes in the application where the namespace contains Service
        var allServices = GetPublicClassesFromApplicationAssembly(c => c.Namespace.Contains("Service"), "Assembly name where service exists goes here");
        var registeredServices = GetImplementationTypesFor(typeof(IService), container);

        allServices.ToList().Should().Equal(registeredManager, (ac, rc) => ac.Name == rc.Name);
    }

    private Type[] GetPublicClassesFromApplicationAssembly(Predicate where, string assemblyName)
    {
        return Assembly.Load(assemblyName).GetExportedTypes()
            .Where(t => t.IsClass)
            .Where(t => t.IsAbstract == false)
            .Where(where.Invoke)
            .OrderBy(t => t.Name)
            .ToArray();
    }

    private Type[] GetImplementationTypesFor(Type type, IWindsorContainer container)
    {
        return container.Kernel.GetAssignableHandlers(type)
            .Select(h => h.ComponentModel.Implementation)
            .OrderBy(t => t.Name)
            .ToArray();
    }
}

Picture below describes what these unit tests protect against:

Step 5 – Introducing new conventions

As your solution evolves you’re going to come across certain scenarios where the responsibilities of a class don’t fit into the conventions defined (as we have a list of conventions with descriptions it’s easy to distinguish if a new convention is needed). These scenarios will mainly occur at the beginning phase of a new application (as its rapidly evolving) and as conventions get defined you will find that having to define a new one will become an increasingly rare activity.

This process should mitigate the scenario of having a customer/service/manager/provider…

Step 6 – Sharing conventions across projects unified code base

Once we’ve established some conventions for a project we can easily extract these out into a separate project and package it as a NuGet package. This allows us to apply the conventions to other solutions giving us a unified structure that looks the same from one solution to another.

New developers will surely appreciate this, and as a co-worker sitting next to them the wtf count will be below uncomfortable thresholds!