Przejdź do głównej zawartości

Domain-driven design z pomocą AI

Pół roku w projekcie i „Order” oznacza trzy różne rzeczy w bazie kodu: wiersz w orders, DTO w usłudze checkout i view-model w Reakcie. AI wciąż wymyśla pola, których nie ma na prawdziwej encji, każda refaktoryzacja po cichu wprowadza z powrotem kwestie persystencji do warstwy domeny, a termin, którego twoi eksperci domenowi używają na spotkaniach, nie pojawia się nigdzie w kodzie. Model i język się rozjechały.

Domain-Driven Design naprawia ten rozjazd, kotwicząc kod we wspólnym słownictwie — języku wszechobecnym (Ubiquitous Language). Haczyk w tym, że DDD wymaga dyscypliny, którą asystent AI ochoczo zignoruje, dopóki nie uczynisz reguł jawnymi i ich nie wyegzekwujesz. Zrobione dobrze, AI staje się kustoszem twojej wiedzy domenowej, a nie tym, co ją eroduje.

  • Konfigurację pliku kontekstowego dla każdego narzędzia, tak by AI domyślnie mówiło językiem twojej domeny
  • Prawdziwy obieg w obie strony: prompt → wygenerowany agregat w TypeScript → jak sprawdzasz go względem niezmiennika
  • Gotowe prompty do modelowania agregatu, generowania implementacji i pilnowania granic ograniczonych kontekstów
  • Tryby awarii, które pojawiają się, gdy DDD spotyka AI, i jak je wyłapać

Fundamentem DDD jest język wszechobecny — słownictwo wspólne dla deweloperów i ekspertów domenowych. Pojedynczym ruchem o najwyższej dźwigni w DDD z asystentem AI jest umieszczenie tego słownictwa w pliku kontekstowym, który agent czyta przy każdym żądaniu, żeby przestał zgadywać.

Treść pliku to ta sama idea w różnych narzędziach; różni się tylko nazwa pliku i lokalizacja.

Cursor czyta reguły projektu z .cursor/rules/*.mdc. Utwórz .cursor/rules/domain.mdc i ustaw go na zawsze stosowany, aby każda tura agenta w repozytorium widziała glosariusz.

Treść tego pliku, niezależnie od narzędzia, to faktyczny glosariusz domeny — dlatego ogrodzony blok Markdown jest tu właściwy (to treść pliku, nie prompt):

# Ubiquitous Language — Order Context
- **Order**: a customer's request to purchase products. Aggregate Root.
- **LineItem**: an entry in an Order for a product + quantity. Value Object (immutable).
- **OrderStatus**: state of an Order — Pending | Paid | Shipped | Delivered | Cancelled. Enum.
- **Customer**: the person who placed the Order. Referenced by `CustomerId`, never embedded.
Invariants:
- An Order has at most 10 unique LineItems.
- Status transitions are one-way along the list above (no Shipped → Pending).
- Total is derived from LineItems; never set directly.

Teraz poproś AI o zaprojektowanie agregatu Order względem tego języka i od razu zweryfikuj, że egzekwuje niezmiennik, a nie tylko się kompiluje.

  1. Poproś o model. Odwołaj się do języka jawnie, by AI używało twoich terminów, a nie swoich domyślnych.

  2. Przeczytaj to, co wygenerowało. Poprawny addItem odrzuca 11. odrębny produkt i przelicza total; LineItem nie ma setterów. Jeśli AI wystawiło publiczny setTotal albo pozwoliło statusowi skoczyć wstecz, model przeciekł.

    addItem(productId: ProductId, qty: number): void {
    const distinct = new Set(this.items.map(i => i.productId.value));
    if (!distinct.has(productId.value) && distinct.size >= 10) {
    throw new OrderInvariantError('An order cannot exceed 10 line items');
    }
    // ...merge or push, then recompute derived total
    }
  3. Przypnij niezmiennik testem. Nie ufaj prozie — poproś o test przypadku niepowodzenia, żeby niezmiennik nie mógł później po cichu się zregresować.

Drugim miejscem, gdzie AI pomaga, jest bieżące zarządzanie architektoniczne — wyłapywanie, kiedy jeden ograniczony kontekst sięga do wnętrzności innego, zamiast komunikować się przez zdarzenia lub warstwę antykorupcyjną. Tu przepływ pracy rozchodzi się w zależności od narzędzia.

Użyj trybu agenta do interaktywnego przeglądu, gdy masz otwarte pliki. Skieruj go na granicę, o którą się martwisz.