Clean Architecture by Uncle Bob

charlieharper

Expert Member
Joined
Jun 1, 2007
Messages
3,864
Reaction score
1,980
Location
South Coast, KZN
Been down the rabbit hole of Clean Architecture / DDD fundamentals past few months and it's really interesting.
Gradually refactoring parts of our 10 year old codebase to take more of these fundamentals into account as it will hopefully make shipping new features faster.

Anyone else been down this and / or using this in practice?
 
Last edited:
Yep I am using DDD and it is a game-changer. In the past I used anemic models and a more functional approach. with the latter your business logic sits in the functions rather than the model. So not really OOP in the true sense. Although ok, it tends to spread logic over a wider area. It was harder to write automated tests because the functions where the logic sits tended to be more closely coupled to the DB. At least that was in my case.

With DDD, you concentrate the logic in the models in true OOP, and it is a breeze to write unit tests for your models and business logic. When models become big, you simply refactor and break them apart along with your unit tests and use composition.

Note that you will not get DDD right (easily) without EF. Got the Tshirt here. EF makes it easy to have a pure model and hydrate it, despite the fact that your property setters should mostly be private (encapsulation).
 
I like these (code organisation) architectures from a theoretical/academic point of view.

In practise, I find that they solve problems that don't exist in the real world, and make code that is harder to understand and change.

They add unnecessary abstraction/indirection, where it is not needed.

"Oh, but you might want to change your ORM" - BS, this happens so rarely as to say it never happens.
"But having interfaces for all this stuff makes it easier to test" - You're doing your testing wrong. There are definitely valid cases for interfaces - `IProductService` is NOT one of them (Ignoring that fact that even having a concrete `ProductService` is a bad idea.....)


DDD, however, I don't think of as a (code organisation) architecture, and I don't think they even prescribe anything.

I don't know DDD well at all, but I think that something like `order.ship()` is crap IMO.
An order cannot ship itself, and to me, it makes tons more sense to conceptually do `ship(order)'
 
I like these (code organisation) architectures from a theoretical/academic point of view.

In practise, I find that they solve problems that don't exist in the real world, and make code that is harder to understand and change.

They add unnecessary abstraction/indirection, where it is not needed.

"Oh, but you might want to change your ORM" - BS, this happens so rarely as to say it never happens.
"But having interfaces for all this stuff makes it easier to test" - You're doing your testing wrong. There are definitely valid cases for interfaces - `IProductService` is NOT one of them (Ignoring that fact that even having a concrete `ProductService` is a bad idea.....)


DDD, however, I don't think of as a (code organisation) architecture, and I don't think they even prescribe anything.

I don't know DDD well at all, but I think that something like `order.ship()` is crap IMO.
An order cannot ship itself, and to me, it makes tons more sense to conceptually do `ship(order)'
Yes agreed DDD and code/source organisation are separate things.


An example of how I apply DDD in order to ensure SRP, encapsulation and being able to easily unit test:

Lets say I have to read a line from a CSV file. The line has several columns, where some of the columns are used to categorise the line and then save each line to the DB. You get a similar file from multiple data provider organisations and the categorisation rules are different for each data provider.


We also know that certain fields are mandatory while others are not.


Applying encapsulation, I create a class (Domain object):


C#:
public class FileLine
{
  public FileLine(string someMandatoryString, decimal someMandatoryValue)
  {
    // Check for null string and bounds for other args
    SomeMandatoryString = someMandatoryString;
    SomeMandatoryValue = someMandatoryValue;
  }

  public string SomeMandatoryString { get; init; }
  public decimal SomeMandatoryValue { get; init; }
  public string SomeOptionalString { get; set; }
  public Category Category { get; private set; }
}


Note the property setters.
The mandatory props are forced to be set in the ctor by using init. This also means that they cannot change after ctor. May be a requirement or not
The SomeOptionalString can be set at any time and does not uphold any business rules, so we mark the setter as public.
We want the FileLine to be categorised, but we dont want an external party to do it because it is the class' responsibility to do that based on its values and provider-specific rules. So we make the setter private. More on that later.

Let's use the std .NET TryParse pattern and add a method:

C#:
public static bool TryParse(string csvLine, out FileLine parsedLine)
{
  parsedLine = null;

  if (string.IsNullOrWhitespace(csvLine))
    return false;

  string[] fields = csvLine.Split(',');
  if (fields.Length != ExpectedNoOfFields)
    return false;

  // some other validations to ensure SomeMandatoryString and other is valid, etc

  if (decimal.TryParse(fields[1],out decimal someMandatoryValue) == false)
   return false;

  parsedLine = new FileLine(fields[0], someMandatoryValue) { set other optional props};

  return true;
}


The above allows FileLine to construct itself from the CSV line and ensuring the values are valid. There are of course other ways of doing that by moving the parsing out of the class.
Important to note that Domain objects can only exist in a valid state or not at all (Encapsulation). The method above ensures this.
The class is also easy to unit test as it has no dependencies.

So for the categorisation, we know that there are different rules based on values in the line as well as the data provider. Instead of having a huge switch, we create an interface because we are going to have different implementations, one for each data provider:

C#:
public interface ICategoriser
{
  Category Categorise(FileLine line);
}


and add some implementations:

C#:
public class SouthAfricaCategoriser: ICategoriser
{
  public Category Categorise(FileLine line)
  {
    if (line.SomeMandatoryString.Contains("ABC")
      return Category.Category1;

    if ((line.SomeMandatoryString.Contains("DEF") && (SomeMandatoryValue > 10))
      return Category.Category2;

    return Category.Undefined;
  }
}


C#:
public class UsaCategoriser: ICategoriser
{
  public Category Categorise(FileLine line)
  {
    if (line.SomeMandatoryString.Contains("USA")
      return Category.CategoryUsa;

    if ((line.SomeMandatoryString.Contains("USA") && (SomeMandatoryValue < 100))
      return Category.CategorySucks;

    return Category.Undefined;
  }
}


So each of these categorisers follow SRP and they are easy to unit test in isolation.

So how do we make the FileLine categorise? We add a Categorise method:

C#:
public void Categorise(ICategoriser categoriser)
{
  Category newCategory = categoriser.Categorise(this);

  if (newCategory == Category.Undefined)
    throw SomeExceptionOrJustReturn();

  Category = newCategory;
}

This method encapsulates the Category and only sets it when the category follows the business rule of not being undefined. The method does not know what the rules are and it does not care.
The method is also easy to unit test by having a mock categoriser if you would wish to unit test the method. Bear in mind that you already hasve tests that cover the various ICategoriser implementations.

To put it all together:

C#:
ICategoriser categoriser = CreateCategoriserBasedOnTheDataProvider();

string line = ReadLineFromFile();
if (FileLine.TryParse(line, out FileLine fileLine) == false)
  throw new ThereIsAnError();

fileLine.Categorise(categoriser);

The point of DDD is that the Domain objects implement the behaviour and rules you require where they belong: in the Domain. There are of course other places for other types of rules.
Also, the advantage of have a pure domain is that the domain objects have no dependencies other than the domain itself, centralising the logic.

Obviously this isn't for everyone and every project, but since I started doing TDD, I focus on breaking code up into much smaller testable chunks and using DDD helps with that. Also helps with keeping things loosely coupled


The complete FileLine then looks like this:

C#:
public class FileLine
{
  public static bool TryParse(string csvLine, out FileLine parsedLine)
  {
    parsedLine = null;

    if (string.IsNullOrWhitespace(csvLine))
      return false;

    string[] fields = csvLine.Split(',');
    if (fields.Length != ExpectedNoOfFields)
      return false;

    // some other validations to ensure SomeMandatoryString and other is valid, etc

    if (decimal.TryParse(fields[1],out decimal someMandatoryValue) == false)
     return false;

    parsedLine = new FileLine(fields[0], someMandatoryValue) { set other optional props};

    return true;
  }

  public FileLine(string someMandatoryString, decimal someMandatoryValue)
  {
    // Check for null string and bounds for other args
    SomeMandatoryString = someMandatoryString;
    SomeMandatoryValue = someMandatoryValue;
  }

  public string SomeMandatoryString { get; init; }
  public decimal SomeMandatoryValue { get; init; }
  public string SomeOptionalString { get; set; }
  public Category Category { get; private set; }

  public void Categorise(ICategoriser categoriser)
  {
    Category newCategory = categoriser.Categorise(this);

    if (newCategory == Category.Undefined)
      throw SomeExceptionOrJustReturn();

    Category = newCategory;
  }

}
 
Thanks for taking the time to post that all :thumbsup:

I guess it depends how things are classified.

Aggregate roots vs “Business process” modeling
FileLine vs “Parse a file”

Kind of ends up being the “same” code, organized differently.

Perfect use case for generics and interfaces though :D
Especially when you might want to parse sources in different data formats e.g. CSV, Tab Delimited, JSON, etc
 
Thanks for taking the time to post that all :thumbsup:

I guess it depends how things are classified.

Aggregate roots vs “Business process” modeling
FileLine vs “Parse a file”

Agreed, and how you prefer to view the problem and its implementation

I used to code in a more business process way (and still do), like you stated above. I _think_ it is a more FP mindset; not that I use true FP. I quite like business process modelling and coding because it lends itself perfectly to extension by using DI & decorator pattern.
E.g.
C#:
class OrderShipper: IOrderShipper
{
  public void Ship(order){...}
}

and now I need to add sending an email after shipping. Using DI, decorator and SRP:

C#:
class EmailSendingOrderShipper: IOrderShipper
{
  public EmailSendingOrderShipper(IOrderShipper orderShipper, EmailServerConfig emailServerConfig)
  {
    // set fields
  }

  public void Ship(order)
  {
    orderShipper.Ship(order);
    SendEmailAboutOrder(order);
  }

  private SendEmailAboutOrder(order) {...}

  private readonly IOrderShipper orderShipper;
  private readonly EmailServerConfig emailServerConfig:
}

and constructing the IOrderShipper in a OrderShipperBuilder class:

C#:
IOrderShipper orderShipper = new OrderShipper();
orderShipper = new EmailSenderOrderShipper(orderShipper, emailConfig)

I this way you can easily extend the business process by layering the decorators in the right sequence, like stacking Lego blocks. Each decorator implements SRP and is testable on its own

Kind of ends up being the “same” code, organized differently.

Perfect use case for generics and interfaces though :D
Especially when you might want to parse sources in different data formats e.g. CSV, Tab Delimited, JSON, etc
100%



Still wrapping my head around DDD though... actually busy with a real-world implementation using DDD.
I tried DDD long ago before EF, but it was very difficult to get the hydration of an aggregate root going. Especially lazy loading navigation properties and child lists. Once I now embraced EF Core, DDD is a cinch. Just a couple of rules I follow to keep the domain pure e.g. DbContext NEVER enters the domain. I will post some typical examples later.
 
For some reason, Decorator is my favourite pattern :)

A perfect example of why composition is better than inheritance (even though you can kind of implement it using inheritance, assuming you done use sealed classes, and even then, you can only decorate a single implementation)

A really great example of using it is to be able to add caching to any existing application, without changing ANY existing code (you might need to extract an interface on the thing you want to decorate)

The way that Scrutor (https://github.com/khellang/Scrutor) implements Decorator is really great

C#:
public interface IContentClient
{
   Task<Content> GetContentItem(Guid id);
}
 
public class ContentfulContentClient : IContentClient
{
    private readonly HttpClient _httpClient;
 
    public Task<Content> GetContentItem(Guid id)
    {
       // call the Contentful API to get the content you need.
    }
}
 
public class CachedContentClient : IContentClient
{
    private readonly IContentClient _contentClientImplementation; //this will be an instance of ContentfulContentClient. Scurtor handles this via the "Decorate" method
    private readonly CacheService _cacheService; // this is just a wrapper around IDistributedCache. Handles the serialization/deserialization
 
    public Task<Content> GetContentItem(Guid id)
    {
        var key = $"{nameof(CachedContentClient)}.{nameof(GetContentItem)}={id}";
        var item = await _cacheService.GetItemFromCache<Content>(key);
        if (item is not null)
        {
            return item;
        }
 
        item = await _contentClientImplementation.GetContentItem(id);
        await _cacheService.PutItemInCache(key, item);
 
        return item;
    }
}
 
 
services.AddHttpClient<IContentClient, ContentfulContentClient>();
services.Decorate<IContentClient, CachedContentClient>();
 
public class Consumer
{
    private readonly IContentClient _contentClient; //This will be an instance of CachedContentClient
    // If you comment out services.Decorate<IContentClient, CachedContentClient>();, then this will be an instance of ContentfulContentClient
}


You can essentially do the above with any cross-cutting concern (logging, tracing, metrics, etc), without change any code

C#:
services.AddHttpClient<IContentClient, ContentfulContentClient>();
services.Decorate<IContentClient, CachedContentClient>(); // will decorate ContentfulContentClient
services.Decorate<IContentClient, LoggingContentClient>(); // will decorate CachedContentClient
services.Decorate<IContentClient, TimedContentClient>(); // will decorate LoggingContentClient

// code that now has a IContentClient injected will have a LoggingContentClient injected
 
Top
Sign up to the MyBroadband newsletter
X