Del 2 av 10

DDD-grunder — språk, kontexter och byggstenar

Det taktiska och strategiska DDD som krävs för att förstå aggregat. Ubiquitous Language, Bounded Context, Entity, Value Object och Domain Event — så att vi i nästa lektion kan koda en riktig AggregateRoot.

Lektion 2 — V1 tor. Vi lägger DDD-vokabulären på plats. Inga händelser persisteras än — fokus på modellen.

Domain-Driven Design i två meningar

DDD är en uppsättning strategiska verktyg för att dela upp ett stort system efter affärsdomäner, och taktiska verktyg för att modellera koden inuti varje domän så att den speglar verksamhetens språk. Boken som myntade begreppen är Eric Evans Domain-Driven Design från 2003 — fortfarande den bästa källan.

Strategisk DDD — kartan

Ubiquitous Language

Ett gemensamt språk för utvecklare, domänexperter och dokumentation. Om verksamheten säger "policy" så heter klassen Policy — inte InsuranceContract. Översättningsfriktion är en ständig källa till buggar.

Test för ubiquitous language Kan domänexperten läsa en metodsignatur i din kod och förstå vad den gör — utan att en utvecklare översätter? Om inte: byt namn.

Bounded Context

Ett bounded context är en gräns där en modell (och ett språk) gäller. Ordet "Customer" betyder olika saker i Försäljning, Fakturering och Support — varje kontext har sin egen Customer-klass. Att försöka tvinga in en gemensam modell är klassisk källa till stora, oförståeliga system.

// Sales-kontexten
namespace TicketHub.Sales;
public class Customer
{
    public CustomerId Id { get; }
    public string Name { get; }
    public LoyaltyTier Tier { get; }  // bara intressant vid försäljning
}

// Billing-kontexten
namespace TicketHub.Billing;
public class Customer
{
    public CustomerId Id { get; }
    public Address BillingAddress { get; }
    public VatNumber? Vat { get; }     // bara intressant vid fakturering
}

De två klasserna delar bara ett id. Översättning mellan kontexter sker via context maps (Shared Kernel, Customer/Supplier, Anti-Corruption Layer m.fl.) — i kursen håller vi oss till en kontext per gång.

Taktisk DDD — byggstenarna

ByggstenIdentitetFöränderlig?Exempel
Value ObjectNej — likhet via värdeNej (immutable)Money, Address, DateRange
EntityJa — har IDJa (genom tid)Customer, Booking, Account
Aggregate RootJa — entitet som är "ingång"JaOrder (med OrderLine-children)
Domain EventJa — har eventIdNej (har redan hänt)MoneyDeposited, SeatReserved
Domain ServiceNejStatslösExchangeRateService
RepositoryIAccountRepository (load/save aggregat)

Value Objects i C#

Sedan C# 9 är record-typer med strukturell likhet det naturliga valet:

public sealed record Money(decimal Amount, string Currency)
{
    public static Money Zero(string currency) => new(0m, currency);

    public Money Add(Money other)
    {
        if (other.Currency != Currency)
            throw new InvalidOperationException(
                $"Cannot add {other.Currency} to {Currency}");
        return this with { Amount = Amount + other.Amount };
    }

    public Money Subtract(Money other) => Add(other with { Amount = -other.Amount });

    public override string ToString() => $"{Amount:0.00} {Currency}";
}
Regler för bra value objects Immutable. Validera invarianter i konstruktorn (kasta vid ogiltigt). Operationer returnerar nya instanser (with). Inga ID-fält. Likhet via värde — får i C# automatiskt med record.

Strongly typed IDs

Skicka inte runt Guid:er — slå in dem i typer som inte går att blanda ihop:

public readonly record struct AccountId(Guid Value)
{
    public static AccountId New() => new(Guid.NewGuid());
    public override string ToString() => Value.ToString("N");
}

public readonly record struct CustomerId(Guid Value);

// Kompilatorn hjälper dig:
void Transfer(AccountId from, AccountId to, Money amount);
// Transfer(customerId, accountId, ...) går inte att skriva av misstag.

Entities

Entiteter har identitet som överlever förändring. Två insättningar på 500 kr är inte samma sak — varje är en egen entitet med eget id, även om de har samma belopp.

public sealed class Booking
{
    public BookingId Id { get; }
    public SeatId Seat { get; private set; }
    public BookingStatus Status { get; private set; }
    // ... beteende, inte bara properties
}

Domain Events

Ett domain event är en kort, oföränderlig beskrivning av något som har hänt. Namnges i dåtid. Innehåller all data som behövs för att läsare ska kunna agera — utan att fråga tillbaka.

public interface IDomainEvent
{
    Guid EventId { get; }
    DateTimeOffset OccurredAt { get; }
}

public sealed record MoneyDeposited(
    Guid EventId,
    DateTimeOffset OccurredAt,
    AccountId Account,
    Money Amount,
    string Reference) : IDomainEvent;

public sealed record SeatReserved(
    Guid EventId,
    DateTimeOffset OccurredAt,
    ConcertId Concert,
    SeatId Seat,
    CustomerId By) : IDomainEvent;
Event vs Command En command kan avvisas ("Sätt in 500 kr" — nekas om kontot är stängt). En event kan inte avvisas — den har redan hänt. Namnge därefter: DepositMoney (command, imperativ) → MoneyDeposited (event, dåtid).

Aggregat — en introduktion

Ett aggregat är en grupp av entiteter och värdeobjekt som måste vara konsistenta tillsammans. Aggregate root är den enda som omvärlden får referera till — all åtkomst till barn-entiteterna sker genom roten.

Regler (som vi fördjupar i nästa lektion):

  1. En transaktion ändrar exakt ett aggregat.
  2. Andra aggregat refereras endast via id, aldrig via objektreferens.
  3. Roten skyddar invarianter — barnen kan vara dumma datastrukturer.
  4. Aggregatet ska vara så litet som möjligt — bara det som måste vara konsistent tillsammans.

Repositories — utan att avslöja persistens

Ett repository ser ut som en samling av aggregat i minnet. Implementeringen kan vara EF Core, Dapper, in-memory eller — som i denna kurs — SqlStreamStore.

public interface IAccountRepository
{
    Task<Account?> LoadAsync(AccountId id, CancellationToken ct = default);
    Task SaveAsync(Account aggregate, CancellationToken ct = default);
}

Notera frånvaron av Update, Find, Query. Repositoriet hanterar bara hela aggregat — frågor ställs mot read models (CQRS, lektion 4).

Att modellera en domän — workshop-tankesätt

Event Storming (Alberto Brandolini) är en effektiv teknik: samla domänexperter och utvecklare framför en vägg med post-it-lappar. Orange = events (dåtid), blå = commands, gula = aggregat. Mönstren framträder snabbt och språket blir verkligen ubiquitous.

För TicketHub (examinationsdomänen) skulle Event Storming ge ungefär: Events: ConcertScheduled, SeatsCreated, SeatReserved, BookingConfirmed, BookingCancelled, PaymentReceived, RefundIssued.
Commands: ScheduleConcert, ReserveSeat, ConfirmBooking, CancelBooking.
Aggregat: Concert (innehåller Seats), Booking, Payment.

Referenser

Elektroniska resurser

Böcker

Övningar

Lös övningarna självständigt. Det finns inget facit — lärandet sker i processen.

  1. Skriv ett value object Implementera EmailAddress som value object i C# med record. Validera formatet i konstruktorn. Lägg till en Domain-property som returnerar delen efter @. Testa likhet mellan två instanser.
  2. Strongly typed IDs Konvertera ett kodexempel där du arbetat med Guid för flera olika entiteter till strongly typed IDs. Visa minst ett fall där kompilatorn nu hindrar en bugg som tidigare hade gått igenom.
  3. Hitta bounded contexts Ta ett system du känner till (gärna ditt nuvarande jobb). Hitta tre olika kontexter där samma ord (t.ex. "User", "Product", "Order") betyder olika saker. Beskriv skillnaderna i en tabell.
  4. Event Storming på papper Välj en konkret affärsprocess (t.ex. boka resa, beställa mat, registrera sjukfrånvaro). Lista 8–12 events i tidsordning. Markera vilka commands som triggar varje event. Identifiera aggregaten.

Soloprojektor

Projekt 1 — Domänmodell i kod Bygg en C#-klassbiblioteksprojekt för en domän du valt själv (förslag: ett enkelt biblioteks-utlåningssystem, en pizzeriabokning, eller en mötesrumsbokare). Skapa minst 3 value objects, 2 entiteter, 1 aggregate root och 5 domain events. Inga databasberoenden — bara Domain-projektet. Skriv 10+ enhetstester som beskriver beteendet.
Projekt 2 — Kontextkarta (fördjupning) Bryt ut din domän från projekt 1 till två bounded contexts (t.ex. "Booking" och "Billing" för pizzerian). Visa hur samma koncept (t.ex. Order) ser olika ut i de två. Rita en kontextkarta som visar relationen (Shared Kernel? Customer/Supplier? Anti-Corruption Layer?). Motivera valet.

← Föregående: Helhet Nästa: AggregateRoot →