Disallineamento concettuale tra DDD Application Services e REST API

19

Sto provando a progettare un'applicazione con un dominio aziendale complesso e un requisito per supportare un'API REST (non rigorosamente REST, ma orientata alle risorse). Ho qualche problema a trovare un modo per esporre il modello di dominio in un modo orientato alle risorse.

In DDD, i client di un modello di dominio devono passare attraverso il livello procedurale "Application Services" per accedere a qualsiasi funzionalità aziendale, implementata da entità e servizi di dominio. Ad esempio, esiste un servizio di applicazione con due metodi per aggiornare un'entità utente:

userService.ChangeName(name);
userService.ChangeEmail(email);

L'API di questo servizio di applicazione espone comandi (verbi, procedure), non stati.

Ma se abbiamo anche bisogno di fornire un'API RESTful per la stessa applicazione, allora c'è un modello di risorse utente, che assomiglia a questo:

{
name:"name",
email:"[email protected]"
}

L'API orientata alle risorse espone stato , non comandi . Ciò solleva le seguenti preoccupazioni:

  • ogni operazione di aggiornamento rispetto a un'API REST può eseguire il mapping a una o più chiamate di procedure del servizio applicazioni, a seconda delle proprietà che vengono aggiornate sul modello risorsa

  • ogni operazione di aggiornamento sembra atomica al client REST API, ma non è implementata in questo modo. Ogni chiamata al servizio dell'applicazione è progettata come una transazione separata. L'aggiornamento di un campo su un modello risorsa potrebbe modificare le regole di convalida per altri campi. Pertanto, è necessario convalidare tutti i campi del modello di risorsa insieme per garantire che tutte le potenziali chiamate al Servizio applicazioni siano valide prima di iniziare a produrle. Convalidare un insieme di comandi contemporaneamente è molto meno banale che eseguirne uno alla volta. Come possiamo farlo su un client che non sa nemmeno che esistono comandi individuali?

  • chiamare i metodi del servizio di applicazione in ordine diverso potrebbe avere un effetto diverso, mentre l'API REST fa sembrare che non ci siano differenze (all'interno di una risorsa)

Potrei trovare altri problemi simili, ma fondamentalmente sono tutti causati dalla stessa cosa. Dopo ogni chiamata a un servizio applicativo, lo stato del sistema cambia. Regole di ciò che è un cambiamento valido, l'insieme di azioni che un'entità può eseguire il prossimo cambiamento. Un'API orientata alle risorse tenta di rendere tutto simile a un'operazione atomica. Ma la complessità di superare questa lacuna deve andare da qualche parte, e sembra enorme.

Inoltre, se l'interfaccia utente è più orientata ai comandi, che spesso è il caso, allora dovremo mappare tra comandi e risorse sul lato client e poi di nuovo sul lato API.

Domande:

  1. Tutta questa complessità dovrebbe essere gestita da un layer di mappatura REST-to-AppService (spesso)?
  2. O mi manca qualcosa nella mia comprensione di DDD / REST?
  3. Potrebbe REST essere semplicemente non pratico per esporre la funzionalità dei modelli di dominio su un certo grado (abbastanza basso) di complessità?
posta astreltsov 17.04.2015 - 06:45
fonte

3 risposte

10

Ho avuto lo stesso problema e "risolto" modellando le risorse REST in modo diverso, ad esempio:

/users/1  (contains basic user attributes) 
/users/1/email 
/users/1/activation 
/users/1/address

Quindi ho sostanzialmente suddiviso la risorsa più grande e complessa in molte più piccole. Ognuno di questi contiene un gruppo un po 'coeso di attributi della risorsa originale che dovrebbe essere elaborata insieme.

Ogni operazione su queste risorse è atomica, anche se può essere implementata usando diversi metodi di servizio - almeno in Spring / Java EE non è un problema creare una transazione più grande da diversi metodi che originariamente intendevano avere una propria transazione ( utilizzando la propagazione della transazione REQUIRED). Spesso hai ancora bisogno di fare una validazione extra per questa risorsa speciale, ma è ancora abbastanza gestibile dal momento che gli attributi sono (supposti per essere) coesi.

Questo è positivo anche per l'approccio HATEOAS, perché le tue risorse più dettagliate trasmettono più informazioni su ciò che puoi fare con loro (invece di avere questa logica su client e server perché non può essere facilmente rappresentata nelle risorse) .

Naturalmente non è perfetto: se le interfacce utente non sono modellate con queste risorse (in particolare le interfacce utente orientate ai dati), possono creare alcuni problemi, ad es. L'interfaccia utente presenta una grande forma di tutti gli attributi delle risorse date (e delle sue sottorisorse) e consente di modificarli tutti e salvarli contemporaneamente: ciò crea l'illusione di atomicità anche se il client deve chiamare diverse operazioni di risorse (che sono esse stesse atomiche ma l'intera sequenza non è atomico).

Inoltre, questa scissione di risorse a volte non è facile o ovvia. Lo faccio principalmente su risorse con comportamenti complessi / cicli di vita per gestirne la complessità.

    
risposta data 24.04.2015 - 10:59
fonte
0

Il problema chiave qui è: in che modo la logica aziendale viene invocata in modo trasparente quando viene effettuata una chiamata REST? Questo è un problema non direttamente risolto da REST.

Ho risolto questo problema creando il mio livello di gestione dei dati su un provider di persistenza come JPA. Utilizzando un meta modello con annotazioni personalizzate, è possibile richiamare la logica aziendale appropriata quando lo stato dell'entità cambia. Ciò garantisce che, indipendentemente da come lo stato dell'entità cambia, viene invocata la logica aziendale. Mantiene la tua architettura ASCIUTTA e anche la tua logica di business in un unico posto.

Utilizzando l'esempio precedente, possiamo richiamare un metodo di business logic chiamato validateName quando il campo del nome viene modificato usando REST:

class User { 
      String name;
      String email;

      /**
       * This method will be transparently invoked when the value of name is changed
       * by REST.
       * The XorUpdate annotation becomes effective for PUT/POST actions
       */
      @XorPostChange
      public void validateName() {
        if(name == null) {
          throw new IllegalStateException("Name cannot be set as null");
        }
      }
    }

Con questo strumento a tua disposizione, tutto ciò che devi fare è annotare i tuoi metodi di business logic in modo appropriato.

    
risposta data 20.05.2015 - 01:04
fonte
0

I have some trouble coming up with a way to expose domain model in a resource-oriented manner.

Non dovresti esporre il modello di dominio in un modo orientato alle risorse. Dovresti esporre l'applicazione in un modo orientato alle risorse.

if the UI is more command-oriented, which often is the case, then we'll have to map between commands and resources on the client side and then back on the API side.

Per niente - invia i comandi alle risorse dell'applicazione che si interfacciano con il modello di dominio.

each update operation against a REST API can map to one or more Application Service procedure call, depending on what properties are being updated on the resource model

Sì, anche se c'è un modo leggermente diverso di scrivere ciò che può rendere le cose più semplici; ogni operazione di aggiornamento con una API REST si associa a un processo che invia comandi a uno o più aggregati.

each update operation looks like atomic to REST API client, but it is not implemented like that. Each Application Service call is designed as a separate transaction. Updating one field on a resource model could change validation rules for other fields. So we need to validate all resource model fields together to ensure that all potential Application Service calls are valid before we start making them. Validating a set of commands at once is much less trivial that doing one at a time. How do we do that on a client that doesn't even know individual commands exist?

Stai inseguendo la coda sbagliata qui.

Immagina: estrai completamente REST dall'immagine. Immagina invece di scrivere un'interfaccia desktop per questa applicazione. Immaginiamo inoltre di avere dei buoni requisiti di progettazione e di implementare un'interfaccia utente basata su attività. In questo modo l'utente ottiene un'interfaccia minimalista perfettamente sintonizzata per il compito che sta svolgendo; l'utente specifica alcuni input quindi colpisce il "VERBO!" pulsante.

Cosa succede ora? Dal punto di vista dell'utente, questo è un singolo compito atomico da eseguire. Dal punto di vista di domainModel, è un numero di comandi eseguiti da aggregati, in cui ogni comando viene eseguito in una transazione separata. Questi sono completamente incompatibili! Abbiamo bisogno di qualcosa nel mezzo per colmare il divario!

Il qualcosa è "l'applicazione".

Nel percorso felice, l'applicazione riceve alcuni DTO e analizza quell'oggetto per ottenere un messaggio che comprende e utilizza i dati nel messaggio per creare comandi ben formati per uno o più aggregati. L'applicazione si assicurerà che tutti i comandi inviati agli aggregati siano ben formati (ovvero il livello anti-corruzione in esecuzione) e caricherà gli aggregati e salverà gli aggregati se la transazione viene completata correttamente. L'aggregato deciderà da solo se il comando è valido, dato il suo stato attuale.

Possibili risultati - tutti i comandi vengono eseguiti correttamente - il livello anti-corruzione rifiuta il messaggio - alcuni dei comandi vengono eseguiti correttamente, ma poi uno degli aggregati si lamenta e hai una probabilità da mitigare.

Ora, immagina di aver creato quell'applicazione; come interagisci con esso in modo RESTful?

  1. Il client inizia con una descrizione ipermediale dello stato corrente (ovvero l'interfaccia utente basata sull'attività), inclusi i controlli hypermedia.
  2. Il client invia una rappresentazione dell'attività (ovvero: il DTO) alla risorsa.
  3. La risorsa analizza la richiesta HTTP in arrivo, acquisisce la rappresentazione e la consegna all'applicazione.
  4. L'applicazione esegue l'attività; dal punto di vista della risorsa, questa è una scatola nera che ha uno dei seguenti risultati
    • l'applicazione ha aggiornato con successo tutti gli aggregati: la risorsa segnala il successo al client, indirizzandolo a un nuovo stato dell'applicazione
    • il livello anti-corruzione rifiuta il messaggio: la risorsa riporta un errore 4xx al client (probabilmente Bad Request), eventualmente passando una descrizione del problema riscontrato.
    • l'applicazione aggiorna alcune aggregazioni: la risorsa segnala al client che il comando è stato accettato e indirizza il client a una risorsa che fornirà una rappresentazione dello stato di avanzamento del comando.

Accettato è il solito cop-out quando l'applicazione rinvia l'elaborazione di un messaggio fino a dopo aver risposto al client, comunemente usato quando accetta un comando asincrono. Ma funziona anche bene per questo caso, in cui un'operazione che dovrebbe essere atomica ha bisogno di attenuazione.

In questo idioma, la risorsa rappresenta l'attività stessa: si avvia una nuova istanza dell'attività pubblicando la rappresentazione appropriata alla risorsa attività e tale risorsa si interfaccia con l'applicazione e indirizza al successivo stato dell'applicazione.

Nelle , praticamente ogni volta che si coordinano più comandi, vuoi pensare in termini di un processo (detto anche business process, aka saga).

C'è una corrispondenza similare concettuale simile nel modello letto. Ancora una volta, si consideri l'interfaccia basata su attività; se l'attività richiede la modifica di più aggregati, l'interfaccia utente per la preparazione dell'attività include probabilmente i dati di un numero di aggregati. Se il tuo schema di risorse è 1: 1 con aggregati, sarà difficile organizzarlo; invece, fornire una risorsa che restituisce una rappresentazione dei dati da diversi aggregati, insieme a un controllo ipermediale che associa la relazione "start task" all'endpoint dell'attività come discusso sopra.

Vedi anche: REST in pratica di Jim Webber.

    
risposta data 30.01.2016 - 05:39
fonte

Leggi altre domande sui tag