L' architettura pulita suggerisce di consentire a un interattore del caso d'uso di chiamare l'implementazione effettiva del presentatore (che viene iniettato, a seguito del DIP) per gestire la risposta / visualizzazione. Tuttavia, vedo le persone implementare questa architettura, restituire i dati di output dall'interattatore e quindi lasciare che il controller (nel livello dell'adattatore) decida come gestirlo. La seconda soluzione sta perdendo le responsabilità dell'applicazione dal livello dell'applicazione, oltre a non definire chiaramente le porte di input e output all'interattatore?
Porte di ingresso e uscita
Considerando la definizione di Clean Architecture , e in particolare il piccolo diagramma di flusso che descrive le relazioni tra un controller, un interattore del caso d'uso e un presentatore, non sono sicuro di comprendere correttamente quale dovrebbe essere la "Porta di output del caso".
L'architettura pulita, come l'architettura esagonale, distingue tra porte primarie (metodi) e porte secondarie (interfacce che devono essere implementate dagli adattatori). Seguendo il flusso di comunicazione, mi aspetto che "Usa porta input case" sia una porta primaria (quindi, solo un metodo), e "Usa case output porta" un'interfaccia da implementare, forse un argomento del costruttore che prende l'adattatore reale, in modo che l'interattatore possa usarlo.
Esempio di codice
Per fare un esempio di codice, questo potrebbe essere il codice del controller:
Presenter presenter = new Presenter();
Repository repository = new Repository();
UseCase useCase = new UseCase(presenter, repository);
useCase->doSomething();
L'interfaccia del presentatore:
// Use Case Output Port
interface Presenter
{
public void present(Data data);
}
Infine, l'interattatore stesso:
class UseCase
{
private Repository repository;
private Presenter presenter;
public UseCase(Repository repository, Presenter presenter)
{
this.repository = repository;
this.presenter = presenter;
}
// Use Case Input Port
public void doSomething()
{
Data data = this.repository.getData();
this.presenter.present(data);
}
}
Sull'interattatore che chiama il presentatore
L'interpretazione precedente sembra essere confermata dal diagramma sopra citato, in cui la relazione tra il controller e la porta di input è rappresentata da una freccia solida con una testa "appuntita" (UML per "associazione", che significa "ha un", dove il controller "ha un" caso d'uso), mentre la relazione tra il presentatore e la porta di output è rappresentata da una freccia solida con una testa "bianca" (UML per "ereditarietà", che non è quella per "implementazione", ma probabilmente questo è comunque il significato).
Inoltre, in questa risposta a un'altra domanda , Robert Martin descrive esattamente un caso d'uso in cui l'interattatore chiama il presentatore su una richiesta di lettura:
Facendo clic sulla mappa, viene richiamato il placePinController. Raccoglie la posizione del clic e qualsiasi altro dato contestuale, costruisce una struttura di dati placePinRequest e la passa a PlacePinInteractor che controlla la posizione del pin, la convalida se necessario, crea un'entità Place per registrare il pin, costruisce un EditPlaceReponse oggetto e lo passa a EditPlacePresenter che visualizza la schermata dell'editor dei luoghi.
Per farlo funzionare bene con MVC, potrei pensare che la logica dell'applicazione che tradizionalmente andrebbe nel controller, qui viene spostata all'interattatore, perché non vogliamo che nessuna logica dell'applicazione vada fuori dal livello dell'applicazione. Il controller nel livello degli adattatori chiamerebbe semplicemente l'interattatore e forse farebbe una piccola conversione del formato dei dati nel processo:
Il software in questo livello è un insieme di adattatori che convertono i dati dal formato più conveniente per i casi d'uso e le entità, nel formato più conveniente per alcune agenzie esterne come il Database o il Web.
dall'articolo originale, parlando di adattatori di interfaccia.
Sull'interattatore che restituisce i dati
Tuttavia, il mio problema con questo approccio è che il caso d'uso deve occuparsi della presentazione stessa. Ora, vedo che lo scopo Presenter
dell'interfaccia è di essere abbastanza astratto da rappresentare diversi tipi di presentatori (GUI, Web, CLI, ecc.), E che in realtà significa semplicemente "output", che è qualcosa che un caso d'uso potrebbe molto bene, ma non ne sono ancora completamente sicuro.
Ora, guardando in giro per il Web alla ricerca di applicazioni di architettura pulita, mi sembra di trovare solo persone che interpretano la porta di output come un metodo che restituisce alcuni DTO. Questo sarebbe qualcosa del tipo:
Repository repository = new Repository();
UseCase useCase = new UseCase(repository);
Data data = useCase.getData();
Presenter presenter = new Presenter();
presenter.present(data);
// I'm omitting the changes to the classes, which are fairly obvious
Questo è interessante perché stiamo spostando la responsabilità di "chiamare" la presentazione fuori dal caso d'uso, quindi il caso d'uso non si preoccupa più di sapere cosa fare dei dati, piuttosto solo di fornire i dati. Inoltre, in questo caso non stiamo ancora violando la regola di dipendenza, perché il caso d'uso non è ancora a conoscenza del livello esterno.
Tuttavia, il caso d'uso non controlla più il momento in cui viene eseguita la presentazione effettiva (il che può essere utile, ad esempio, per fare cose aggiuntive a quel punto, come la registrazione, o per interromperla del tutto se necessario). Inoltre, tieni presente che abbiamo perso la porta di input del caso d'uso, perché ora il controller utilizza solo il getData()
metodo (che è la nostra nuova porta di output). Inoltre, mi sembra che stiamo infrangendo il principio "dillo, non chiedere" qui, perché stiamo chiedendo all'interattatore per alcuni dati di fare qualcosa con esso, piuttosto che dirgli di fare la cosa reale nel primo posto.
Al punto
Quindi, una di queste due alternative è l'interpretazione "corretta" della porta di output del caso d'uso secondo Clean Architecture? Sono entrambi vitali?