Operazioni di aggregazione incrociata, utilizzo di servizi di dominio o eventi di dominio?

1

Sto refactoring un modulo di insediamenti in una grande applicazione sanitaria. Sto cercando di seguire DDD.

Lascia che ti dia una descrizione breve e semplificata del codice refactored:

  1. Un paziente ha un appuntamento durante il quale si sottopongono a diverse procedure mediche. Ogni procedura ha il suo prezzo.
  2. Il paziente può pagare soldi alla clinica (sia per le procedure che per alcuni prodotti medici)
  3. Quando pagano le procedure, le procedure diventano contabilizzate
  4. Viene emessa una fattura o una ricevuta
  5. Alcuni dei soldi che il paziente paga sono assegnati al dottore

I punti sopra riportati definiscono fondamentalmente i processi principali ei processi dei miei refactoring 2-5.

I processi sono più complicati di quanto sembri (carichi di logica di dominio). Per disaccoppiare la logica ho deciso di trattare ogni processo come un piccolo sottosistema. E per ogni sottosistema ho creato un aggregato separato. Consentitemi di presentarlo con qualche pseudo-codice:

// Patient pays for whatever they're charged for
class PatientForPayments : AggregateRoot {
   private Payment[] Payments
   public Pay(amount){...}
}

// You can use the payment to account procedures in an appointment
class Appointment : AggregateRoot {
   private Procedure[] Procedures
   public IsFullyAccounted(){...}
   public Account(payment){...} // use the payment to account as many procedures as possible with the money paid
}

// An invoice can be issued
class PatientForInvoices : AggregateRoot {
   private Invoice[] Invoices
   public IssueInvoice(payment, procedures[]){...}
}

// Settle the money with a doctor
class ProcedureForSettlements : AggregateRoot {
   private Payment Payment
   private Doctor Doctor
   private Procedure Procedure
   private decimal Value
}

Mi chiedo quale sia il modo migliore per orchestrarlo.

  • Alcuni passaggi sono facoltativi: dipendono dall'input dell'utente (ad esempio emettendo una fattura)
  • C'è ancora un accoppiamento temporale (ad es. non è possibile emettere una fattura per un pagamento che non esiste ancora)
  • Alcune delle operazioni dovrebbero andare insieme (ad esempio quando una procedura viene contabilizzata, dovremmo accontentarci del medico)

Per ora l'ho orchestrato in un servizio di dominio, chiamato direttamente dall'interfaccia utente (codice nascosto in un'app di WinForms legacy). Qualche altro pseudo-codice:

class PatientPaymentDomainService {
   public void HandlePayment(bool isInvoiceNeeded, PaymentDto paymentDto){
       payment = CreatePayment(paymentDto)
       database.Save(payment)

       appointment = database.GetAppointment(paymentDto.AppointmentId)
       appointment.Account(payment)
       database.Save(appointment)

       if(isInvoiceNeeded){
          patient = database.GetPatient(paymentDto.PatientId)
          patient.IssueInvoice(payment)
          database.Save(patient)
       }

       for(procedureDto in paymendDto.ProceduresDtos) {
          doctorSettlement = CreateDoctorSettlement(procedureDto, payment)
          database.Save(doctorSettlement)
       }
   }
   ...
}

Il codice sopra incapsula sicuramente alcune logiche di dominio (il flusso di lavoro), quindi credo che appartenga al livello del dominio e vorrei che not lo collochi ViewModel / CodeBehind.

Mettere tutto in un singolo aggregato non ha molto senso. Probabilmente finirò dove ho iniziato, un enorme groviglio di logica aggrovigliata e accoppiata.

Tuttavia, orchestrarlo con gli eventi e i gestori di domini ha senso.

Che ne pensi? Come lo modellerai?

    
posta Andrzej Gis 19.11.2018 - 18:18
fonte

1 risposta

1

Prima di tutto è necessario identificare le regole aziendali e i requisiti di coerenza per tali regole. Questo porta al design degli Aggregati. L'aggregato è il più grande confine di consistenza strong. Ciò significa che, indipendentemente da ciò che accade, un aggregato è sempre coerente.

An Aggregate è qualcosa di molto bello ma ha un costo: limita la scalabilità del sistema. Ciò significa che è necessario rendere gli Aggregati non più grandi e non più piccoli di quelli che devono essere.

Alcune regole possono essere applicate in modo coerente alla fine. Arrivano i manager di Sagas / Process. Un Saga garantisce che il sistema nel suo complesso alla fine si trovi in uno stato coerente. Una Saga modella i processi aziendali che di solito si estendono su più Aggregati. Dovrebbe essere ripristinabile / riavviabile: in caso di guasto della macchina, può essere riavviato e dovrebbe riprendere dal punto in cui era stato interrotto. Una Saga è l'inverso di un aggregato. In un'architettura CQRS, una Saga reagisce agli eventi del dominio e invia comandi agli Aggregati. Può avere lo stato, che utilizza per memorizzare i dati di processo iniziali o per ricordare l'avanzamento del processo.

Dal punto di vista del DDD, una Saga è un servizio di dominio: è usato per gestire le interazioni tra più Aggregati ma non ruba la logica di dominio che dovrebbe essere inserita in un Aggregato.

Nel tuo caso, supponendo di aver trovato correttamente i limiti di aggregazione (hai identificato correttamente gli Aggregati), hai bisogno di una Saga per implementare il processo di business dei pagamenti. Dovrebbe essere inizializzato con tutti i dati necessari per avviare il processo. In generale, una Saga dovrebbe avere 3 metodi pubblici:

  • init(data) - utilizzato per inizializzare i dati
  • resume() : utilizzato per riprenderlo dopo un errore
  • isEnded() - utilizzato dal sistema per sapere quando escludere un'istanza di Saga da eseguire

Il metodo più interessante è resume . Dovrebbe avere il seguente contenuto:

if(!phaseOneEnded()) {
    processPhaseOne(); //it should send commands to the Aggregates in phase one
    markPhaseOneAsEnded(); //it changes its internal status and persist it
}

if(!phaseTwoEnded()) {
    processPhaseTwo(); //it should send commands to the Aggregates in phase one
    markPhaseTwoAsEnded(); //it changes its internal status and persist it
}

...

Dovresti notare che la Saga non può sapere con certezza se una fase è terminata o meno, e può inviare gli stessi comandi più volte. Ad esempio, se la macchina viene riavviata dopo processPhaseOne() ma prima di markPhaseOneAsEnded() . Ciò significa che gli Aggregati dovrebbero essere in grado di rilevare un comando duplicato e ignorarlo.

Nel tuo caso esatto, la Saga potrebbe apparire come questa:

class PatientPaymentSaga {
   public static void init(bool isInvoiceNeeded, PaymentDto paymentDto, Persistence sagaPersistence){
      var saga = new PatientPaymentSaga( isInvoiceNeeded,  paymentDto);
      sagaPersistence.persist( saga );
   }


   public void resume(){
       if(!paymentCreated()){
          createAndSavePayment(this.paymentDto);
       }

       if(!appoimentAccounted()){
          accountAppointment(this.paymentDto.AppointmentId);
       }

       if(isInvoiceNeeded && !invoiceIssued()){
          issueInvoice(this.paymentDto);
       }


       for(procedureDto in paymendDto.ProceduresDtos) {
          if(!isDoctorSettledForPayment()){
              settleDoctor(procedureDto, this.payment);
          }

       }
   }

   public function isEnded()
   {
       return paymentCreated() && (!isInvoiceNeeded || invoiceIssued()) && doctorsSettled();
   }
   ...
}

La modalità di esecuzione di Sagas dipende molto dal linguaggio / framework di programmazione. Il modo più semplice è avere un lavoro cron che carichi tutti i Sagas non terminati e chiama saga.resume() per ciascuno.

    
risposta data 20.11.2018 - 14:21
fonte