Поиск  
Always will be ready notify the world about expectations as easy as possible: job change page
Oct 26, 2023

Practicing Domain-Driven Design in C#

Автор:
Источник:
Просмотров:
3103

Key strategic and tactical considerations to take when building a new product with the domain-driven design concepts in mind

Sport Team

I have just finished reading Learning Domain-Driven Design by Vlad Khononov. It’s quite a short book (c. 300 pages) aiming to teach beginners all about domain-driven design.

I wanted to check I understood the concepts in the book by trying to put them into practise. I hope you will learn something by reading this too! This is how I got on…

 Learning Domain-Driven Design

What is Domain-Driven Design?

The idea of domain-driven design is to use the business requirements you are building for as the basis for your architecture.

The business team that is asking you to build the product are the ‘domain experts’ and the area of business they work in is the ‘domain’.

There is a lot of talk of using a ‘ubiquitous language’, which means rather than using coding words that non-techies might not understand, you name your classes, methods and variables and discuss stuff using words that would be used by the business team to describe the product.

The idea of DDD is to write code in every day English!
The idea of DDD is to write code in every day English!

There are lots of different technical concepts that are covered in the book on how to implement domain-driven design and I will try to explain some of them in this article. I am going to do this by working through the thought process behind the building of an imaginary product as an example.

What am I going to build?

My idea for a product is something that allows you to connect with other people to play sport. I am going to call it “Sports Connect”.

Here are some requirements that I have just made up:

  • I want users to be able to message other people who are interested in playing the same sport and who are of a similar standard.
  • I want users to be able to rate the experience of playing with the other person after they have finished.
  • I want the user to be able to log in and out.
  • I want sports coaches to be able to see players ratings and standard level in their sport in their area so that they can scout them for teams and message them in the app.
  • I want sports teams to be able to manage fixtures and training schedules and players picked for the team for each fixture and send notifications to the players in the teams.
  • I want teams in leagues to be rated based on past performance and for fixture predictions to be available.

Strategic Decisions

Before you start building anything, the first thing to think about is what ‘type’ of different subdomains you have. This is a domain-driven design concept. Here’s a bit of an explanation.

Domains and sub-domains

The domain of my product is a kind of sports-player marketplace and sports team management. For domain-driven design, we are really interested in what the subdomains are.

The subdomains are the different parts of the domain, and they are divided into different types.

1. Core subdomains

The ‘core subdomains’ are the most important type of subdomain and are what makes the business money and stand out from its competitors. In my example above I think the core subdomains are:

  • The analysis that finds out other players who are based nearby, interested in the same sport and of a similar standard.
  • Working out the rating for a sports player.
  • Sports coaches can pick players for the team and put in their positions.
  • Rating of sports teams.
  • Score predictions in sports leagues.
  • A really user friendly website with a great design.

These are relatively complex bits of logic that are likely to change in requirements frequently based on the business requirements. The core subdomains are things that should be built in-house by your best developers. In this case by me in my bedroom!

Developers

2. Generic subdomains

‘Generic subdomains’ are the next type of subdomain. They are typically complicated and hard to implement but do not provide any competitive edge. These kind of subdomains are usually bought in from 3rd party since it’s a waste of time building these yourself.

In my example, I think the generic subdomains are:

  • The messaging system for users to reach out to each other.
  • The logging in and out system.

3. Supporting subdomains

The final type of subdomain is the ‘supporting subdomain’. This is something that doesn’t provide any competitive advantage but is really easy to implement e.g. entering and getting data from CRUD interfaces. For this product, my supporting subdomains are:

  • Entering and storing the user’s personal data.
  • Entering and storing the rating that users give to each other.
  • The team fixtures and training schedules.

Ok great! We have our subdomains figured out. We have to think about these separately and how they fit into our solution. Let’s now learn about bounded contexts.

Bounded Contexts

Bounded context is another concept of domain-driven design. The way I understand it is it’s a kind of scope in which the ‘ubiquitous language’ (i.e. the language of the domain) applies.

If two different areas of the business have the same word for something with a different meaning, then this is a good sign that these are two different bounded contexts.

In my example, I might find that in the context of the functionality used by individual players to find people to play with, the term Player has a totally different meaning to the term Player used by sports coaches when managing their team.

Football team

Therefore, it might be sensible to have a bounded context for individual players and another for team management.

In general, the smaller the bounded context the better it is as it is more flexible. There is a bit of a pay off though as making the contexts too small can lead to unnecessary complexity when integrating.

In practice, only one team of developers should work on a single bounded context (but the same team might work on more than one bounded context).

That is all the stuff we will consider from a ‘strategic’ design point of view. Now let’s look at ‘tactical’ design.

Tactical Design

Time to get into the nitty gritty of how our code is going to look.

Within each subdomain that we defined above, we should have a think about how we want to design the architecture. Each will have its own architectural design based on its complexity.

The book has a flow diagram that helps to show when you might want to use each type of architecture.

In case you were confused on the terminology, ports and adapters architecture is also called clean architecture.

Architecture

Generic subdomains

The idea is to match the architecture of the subdomain with its complexity — for the generic subdomains, which have basic data access functionality, it will probably make sense to design it with a simple ‘Active Record’ type architecture.

This will allow you to perform CRUD operations but without worrying about writing SQL scripts for example.

Core subdomains

For core subdomains — the juicy parts of the product — the business logic is probably going to be more complicated. You can see from the flowchart that it is a good idea to use clean architecture or CQRS.

In this article I am going to have a go at building something with a domain model so that I can practise the domain-driven design principles (of course!). A domain model lends itself well to a clean architecture so I will crack on with that since it is something I am familiar with.

You can see from the flowchart that the book I read covers event sourcing and event sourcing domain models as well… but as it’s my first attempt at using these concepts I will keep things simple and use a more straightforward domain model. I will leave practising event sourcing and CQRS for another day!

Domain Models

In one of my core subdomains, I want to match players with each other who play the same sport and are a similar standard.

I will create a Player model. When thinking about the domain model, there are a few important things to think about.

Developer


Types

Value type

A value type is something that becomes something else if one of its properties are changed.

An example in the Player model might be the Address type. It will have a first line, second line, city and postcode. If any of those things change then the address becomes a new different address.

Entity type

An entity type is something that you expect to change over time and needs some kind of ID to be able to distinguish it from other objects.

The Player model itself will be an entity type as the data for the player might change over time. The player will be identifiable by an ID number.

As a general rule, if you can break an entity type down with value types then you should. Here is a rough first draft of my Player model:

public class Player
{
    public Player(PlayerId id, Name name, Sport sport, Address address, int rating, List<Player> playerMatches)
    {
        PlayerId = id;
        Name = name;
        Sport = sport;
        Address = address;
        Rating = rating;
        PlayerMatches = playerMatches;
    }
 
    public PlayerId PlayerId { get; private set; }
    public Name Name { get; private set; }
    public Sport Sport { get; private set; }
    public Address Address { get; private set; }
    public int Rating { get; private set; }
    public List<Player> PlayerMatches { get; private set; }
}

The Player class is an Entity but the properties are all Value types. If any of the properties of any of the value types were to change then it would give a new version of that object.

Aggregates

An aggregate is another concept of domain-driven design. It is arguably the most important one and the hardest to get right.

It is a single or group of entity models that link together and whose behaviours impact each other.

A really important part of an aggregate is that the logic has to validate all incoming changes and ensure that the changes are in line with the business rules for the entity.

Everything external to the entity is only allowed to read the aggregate’s state. That is why there is a private setter.

The state of the aggregate is only changed by executing methods of the aggregate’s public interface.

Here I have adapted my Player model to make it into a Player aggregate:

namespace Domain.Entities
{
    public class Player
    {
        public Player(PlayerId id, Name name, Sport sport, Address address, int rating, List<Player> playerMatches)
        {
            PlayerId = id;
            Name = name;
            Sport = sport;
            Address = address;
            Rating = rating;
            PlayerMatches = playerMatches;
        }

        public static Player Create(PlayerId id, Name name, Sport sport, Address address, int rating)
        {
            return new Player(id, name, sport, address, rating, new List<Player>());
        }

       
        public void ComparePlayer(Player newPlayer)
        {
            if (SportIsAMatch(newPlayer) && RatingIsAMatch(newPlayer))
                PlayerMatches.Add(newPlayer);
        }

        public void UpdateAddress(Address address)
        {
            Address = address;
        }

        public void UpdateRating(int rating)
        {
            Rating = rating;
        }

        public PlayerId PlayerId { get; private set; }
        public Name Name { get; private set; }
        public Sport Sport { get; private set; }
        public Address Address { get; private set; }
        public int Rating { get; private set; }
        public List<Player> PlayerMatches { get; private set; }

        private bool SportIsAMatch(Player newPlayer)
        {
            return Sport == newPlayer.Sport;
        }

        private bool RatingIsAMatch(Player newPlayer)
        {
            return Math.Abs(Rating - newPlayer.Rating) <= 3;
        }

    }
}

All of the properties have a private setter. I have added methods to compare players and if they are a “match” add them to the list, and also methods to update the rating and address of the player.

These “commands” (as they are known in domain-driven design) are called via the aggregate’s public interface.

Clean architecture

If you are not familiar with clean architecture, I have built a mini example project in C# previously here — it could be useful to have a quick read of that as I won’t go into too much detail of clean architecture here.

Domain layer

This layer has all the business logic for the solution. In my domain layer, I will have my Player aggregate that I showed you before.

Use cases

For the purposes of this article, I’ve created just a couple of use cases that will be used by my application:

  1. Adding a new player; and
  2. Comparing a player with another and adding it to the player matches list if it is a match

You can see that the code in my use case classes implements the commands in the Player aggregate.

The Player model is totally responsible for updating the state of the Player entity. A player cannot be modified by logic outside of the entity due to the private setters.

Here is the Add Player use case code:

using Domain.Entities;
using Domain.Repository;

namespace Domain.UseCases
{
    public class AddPlayerInteractor
    {
        private readonly IRepository _repository;
        public AddPlayerInteractor(IRepository repository)
        {
            _repository = repository ?? throw new ArgumentNullException(nameof(repository));
        }
        
        public void Handle(PlayerId id, Name name, Sport sport, Address address, int rating)
        {
            if (id == null || name == null || sport == null || address == null)
            {
                throw new ArgumentNullException();
            }

            var player = Player.Create(id, name, sport, address, rating);

            _repository.AddPlayer(player);            
        }
    }
}

This code calls the Create command in the Player aggregate. It then adds the Player to the Player repository.

Here is the code for the compare player use case:

using Domain.Entities;
using Domain.Repository;

namespace Domain.UseCases
{
    public class ComparePlayerInteractor
    {
        private readonly IRepository _repository;
        public ComparePlayerInteractor(IRepository repository)
        {
            _repository = repository ?? throw new ArgumentNullException(nameof(repository));
        }
        
        public void Handle(Player player1, Player player2)
        {
            if (player1 == null || player2 == null)
            {
                throw new ArgumentNullException();
            }

             player1.ComparePlayer(player2);

            _repository.UpdatePlayer(player1);            
        }
    }
}

This code uses the ComparePlayer command in the Player aggregate to update the PlayerMatches list with a new player if the conditions for being a match are met.

Presentation layer

I haven’t bothered to build a presentation layer for this example but the beauty of clean architecture is that any kind of presentation type can be used without disturbing any of the core business logic in the code we’ve written so far.

Ultimately for this app, we would want to build out a snazzy UI either for the web or mobile.. but we don’t need to make any decisions on that at this stage since the business logic is totally independent of the choice of the presentation layer.

Conclusion

So there you have it! A rather long but nice article about the key strategic and tactical considerations to make when building a new product with the domain-driven design concepts in mind.

I have talked you through how to work out the different types of subdomains and the types of architectures that should be used for each type.

I have also had a go at making a very simple pared-back version of one of the core subdomains of the product using a domain model and clean architecture.

I hope that you found this article useful to give you a bit of background knowledge of domain-driven design and to see an example of how it might look in practice!

Похожее
Sep 23
Author: Tiago Martins
This is a behavioral pattern where the main goal is to split a complex job into multiple steps, each with a specific functionality. It’s really good for several types of architectures. For a system composed of various microservices with numerous...
Oct 21, 2023
AntiPattern Problem A Boat Anchor is a piece of software or hardware that serves no useful purpose on the current project. Often, the Boat Anchor is a costly acquisition, which makes the purchase even more ironic. The reasons for acquiring...
Jan 28, 2023
Author: Rokas
Prerequisites: VSCode and .Net 7 installed. Don't need to copy the code, GitHub link provided in the end. Open up a terminal in VSCode or any IDE of your choice, run: dotnet new console 2 files will appear, default Program.cs...
Mar 15
Author: Jonathan Boccara
Understanding is key to building quality software. Here are 3 levels of understanding we should go through before we start coding. Since the beginning of the year, I’ve had the position of a team lead/manager/dev lead, call this what you...
Написать сообщение
Тип
Почта
Имя
*Сообщение