Eine Domäne definiert die Entitäten und ihre Attribute. Was oft nicht definiert ist, ist in welcher Konstellation welche Entitäten und Attribute zum Tragen kommen. Dies ist der Fall, wenn viele Attribute auch leer bleiben können. Die Typen beschreiben dann nicht, wann welches dieser Attribute gefüllt werden muss, was zu Fehlern führt.
Wie diese Informationen in den Typen hinterlegt werden können, beschreibt Scott Wlaschin in seinem Buch "Domain Modeling Made Functional".
Typen bloss als Datenkübel nutzen
Gehen wir von einer Rechnungsposition aus, so könnte diese klassisch so aussehen.
type InvoiceItem = {
id: Invoicing.InvoiceItemId,
invoiceId: Invoicing.InvoiceId,
description: string,
amount?: number, // Betrag
catering?: number, // Verpflegung
subsidy?: number, // Unterstützung
discount?: number // Rabatt
}
Was auf den ersten Blick gar nicht so schlecht aussieht, hat folgende Probleme.
- Kommt die Verpflegung zum Betrag hinzu oder ist sie schon im Betrag enthalten? Dasselbe gilt für Unterstützung und Rabatte.
- Kann eine Position nur Verpflegung enthalten? Dasselbe bei Unterstützung und Rabatt.
- Wenn eine Position nicht abgerechnet werden darf, wie wird dies in der Position angezeigt?
- Wie wird mit diesen Daten die Summe berechnet?
Wenn diese Anwendung nun Rechnungen in ein anderes System exportieren soll, sind Fehler vorprogrammiert.
Mit Typen auch "Logik" abbilden
Nach Ansicht von Scott Wlaschin sollte der Typ für die Rechnungsposition eher so aussehen.
type InvoiceItemCommon = {
id: InvoiceItemId
invoiceId: InvoiceId
description: string
}
// Amount types
type CateringShareAmount = {
amount: number
kind: 'CateringShareAmount'
}
type CareShareAmount = {
amount: number
kind: 'CareShareAmount'
}
type CareAndCateringShareAmount = {
careAmount: CareShareAmount
cateringAmount: CateringShareAmount
kind: 'CareAndCateringShareAmount'
}
type Amount = CareShareAmount | CateringShareAmount | CareAndCateringShareAmount
// Item types
type AmountItem = {
kind: 'AmountItem'
amount: Amount
subsidyAmount: number | null
}
type NonChargedAmountItem = {
kind: 'NonChargedAmountItem'
amount: Amount
}
type DiscountItem = {
percentage: number
kind: 'DiscountItem'
}
type InvoiceItem = InvoiceItemCommon &
(AmountItem | NonChargedAmountItem | DiscountItem)
// Types change as business workflows work on the data
type CalculatedAmountItem = AmountItem & {
kind: 'CalculatedAmountItem'
total: number
}
type CalculatedInvoiceItem = InvoiceItemCommon &
(CalculatedAmountItem | NonChargedAmountItem | DiscountItem)
Mit dieser Modellierung sind die oben aufgeführten Probleme nicht mehr vorhanden. Man kann jedoch nicht leugnen, dass es sich um etwas mehr Code handelt. Scott Wlaschin argumentiert, dass diese Modellierung weniger Interpretationsspielraum lässt und somit viele Fehler vermieden werden können.
Damit das funktioniert, muss aber auch mehr Code zwischen der Datenbank und dem Domänenmodell stehen, das den Code korrekt transformiert.
Wer mehr über diesen Ansatz erfahren möchte, kann dies auf der Website von Scott Wlaschin in seiner Reihe "Designing with Types" und in seinem Buch "Domain Modeling Made Functional" tun.