Architettura esagonale: gestisce più ritorni dalla logica del dominio

0

Sto guardando usando hexagonal / ports & gli adattatori disegnano in qualche codice su cui sto lavorando e per la maggior parte sono contento, ma c'è una parte che non riesco a capire come gestire la necessità di valori di ritorno diversi da una singola chiamata.

Userò un esempio per illustrare meglio, questo è pseudo codice, quindi i nomi non sono rappresentativi del codice reale.

Se ho un "servizio" di dominio che ha un metodo AddEntity posso avere 3 resi:

  1. Oggetto appena aggiunto
  2. Dati non validi
  3. L'entità esiste già

L'adattatore che utilizza questo "servizio" prenderà quindi il ritorno e invierà i dati nell'oggetto (potrebbe trasformarlo prima in un "modello di visualizzazione").

In C # l'adattatore potrebbe essere un controller WebApi con codice simile a questo:

public IHttpActionResult Post(DataViewModel entityData)
{
    var item = this.service.AddEntity(entityData.Name, entityData.Description);

    if (item == null) 
    {
        return this.StatusCode(HttpStatusCode.Conflict);
    }

    var viewModel = Mapper.Map<EntityDTO, DataViewModel>(item);
    return this.Created(this.Url.Link("DefaultApi", new { controller = "Entity", id = viewModel.Id }), viewModel);
}

Il problema, come puoi vedere, è che al momento può gestire solo 2 ritorni, o ottiene l'entità DTO che mappa e ritorna al client o diventa nullo e restituisce semplicemente un codice HttpStatus di conflitto.

Quello che sto cercando di capire è come potrei tornare all'adattatore, controller nell'esempio sopra, sia l'oggetto o uno dei due messaggi.

Gli approcci che ho considerato sono:

  • estendere la classe entityDTO per avere informazioni aggiuntive che mi permettano di passare qualcosa che indica un problema (forse un enum?)
  • innalzamento delle eccezioni digitate e gestione delle eccezioni
  • raccolta di eventi

se usassi l'enum, il codice del controller sarebbe simile a questo:

public IHttpActionResult Post(DataViewModel entityData)
{
    var item = this.service.AddEntity(entityData.Name, entityData.Description);

    if(item.result == Result.Conflict)
    {
        return this.StatusCode(HttpStatusCode.Conflict);
    }

    if (item.result == Result.Invalid) 
    {
        return this.StatusCode(HttpStatusCode.Invalid);
    }

    var viewModel = Mapper.Map<EntityDTO, DataViewModel>(item);
    return this.Created(this.Url.Link("DefaultApi", new { controller = "Entity", id = viewModel.Id }), viewModel);
}

Di queste estensioni del DTO sembra l'opzione meno valida, ma non sono sicuro se mi sia sfuggito qualcosa nel modello che fornisce un modo semplice per farlo.

C'è qualche modo specifico per farlo che ho perso? o è una delle opzioni per l'ordinamento di questo ho menzionato sopra un modo migliore rispetto agli altri?

    
posta Nathan 15.10.2014 - 11:41
fonte

2 risposte

2

Comunicare errori è non parte della responsabilità di un DTO, quindi aggiungere un campo al DTO a tale scopo dovrebbe essere davvero un'opzione di ultima istanza.

Le soluzioni tipiche per il problema in cui si restituisce un oggetto o una di una serie di errori sono:

  • Restituzione di un record Variant che contiene l'oggetto o il codice di errore
  • Restituzione di una tupla di oggetto e codice di errore,
  • Restituzione di un codice di errore (con un valore speciale per "nessun errore") e utilizzo di un parametro esterno per l'oggetto
  • Restituzione dell'oggetto e utilizzo di un parametro out per il codice di errore
  • Restituzione dell'oggetto e utilizzo di un'eccezione per comunicare l'errore.

Quale opzione scegli dipende in parte dalle possibilità e dalla cultura che circonda la lingua che usi. L'opzione con il record Variant probabilmente comunica il più chiaramente che si ottiene un errore un oggetto o .

    
risposta data 15.10.2014 - 12:23
fonte
1

Ci sono un paio di principi da tenere a mente.

Il primo è command-query separation here (CQS). O stai aggiornando, o stai interrogando, ma non entrambi, perché questo crea un problema di effetti collaterali. Aggiungere una nuova X dovrebbe essere un'operazione di scrittura e quindi non restituire un valore.

Il problema pragmatico qui è che potresti non essere in grado di utilizzare un algoritmo Guid o Hi-lo per creare l'identità per la X che stai per aggiungere. In tal caso è necessario fornire un tipo di reso o aggiungere un parametro out per recuperarlo. Questo dovrebbe essere l'Id e non l'istanza.

In questo caso, con uno stile di architettura gerarchico, potrei comunque utilizzare un modello di comando e considerare l'aggiunta di una proprietà sul comando che può essere aggiornata per l'id creato, se necessario. Questo è essenzialmente un trucco per aggirare l'incapacità di creare l'id qui. Vedi la mia discussione qui: link

Quindi userei qualche tipo di recupero per restituire l'istanza di X se ne avevo bisogno per la mia risposta, cioè una query separata. Ancora una volta, non restituirei l'istanza di X sul comando o come parametro out o tipo restituito. Ciò riduce al minimo la tua esposizione agli effetti collaterali se devi farlo.

Quando si considerano i codici di errore e le eccezioni, tenere presente che un codice di errore è effettivamente una query: qual era lo stato di tale operazione? In molti casi è possibile eseguire il comando e quindi eseguire la query per determinare il risultato. Ad esempio, se è comune che la gente proverà a ricreare una X esistente, la sua ricerca prima che l'operazione avvenga può essere un'alternativa al lancio di un'eccezione.

Una volta comprese le nostre letture e scritture come tutte utilizzando un livello di porta per un modello, puoi valutare di chiamarle dal livello dell'adattatore.

In altri casi, pensa agli invarianti che dichiari: le pre-condizioni e le post-condizioni.

  • Non esiste già una X.
  • X è stato creato con successo

Se violate queste condizioni, vorrei lanciare un'eccezione, per indicare che uno degli invarianti ha fallito.

    
risposta data 29.10.2014 - 13:53
fonte

Leggi altre domande sui tag