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;
}
}