Architettura pulita: caso d'uso contenente il presentatore o la restituzione dei dati?


42

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 Presenterdell'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?


3
Il cross-posting è fortemente scoraggiato. Se è qui che vuoi che la tua domanda viva, allora dovresti eliminarla dallo Stack Overflow.
Robert Harvey,

Risposte:


48

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.

Non è certamente architettura pulita , cipolla o esagonale . Questo è questo :

inserisci qui la descrizione dell'immagine

Non che MVC debba essere fatto in questo modo

inserisci qui la descrizione dell'immagine

È possibile utilizzare molti modi diversi per comunicare tra i moduli e chiamarlo MVC . Dire che qualcosa usa MVC non mi dice davvero come comunicano i componenti. Questo non è standardizzato. Tutto ciò che mi dice è che ci sono almeno tre componenti focalizzati sulle loro tre responsabilità.

Ad alcuni di questi modi sono stati dati nomi diversi : inserisci qui la descrizione dell'immagine

E ognuno di questi può legittimamente essere chiamato MVC.

Ad ogni modo, nessuno di quelli cattura davvero ciò che le architetture di parole d'ordine (Clean, Onion e Hex) ti stanno chiedendo di fare.

inserisci qui la descrizione dell'immagine

Aggiungi le strutture dati che vengono lanciate (e capovolgile per qualche motivo) e otterrai :

inserisci qui la descrizione dell'immagine

Una cosa che dovrebbe essere chiara qui è che il modello di risposta non passa attraverso il controller.

Se sei attento, potresti aver notato che solo le architetture di parole d'ordine evitano completamente le dipendenze circolari . È importante sottolineare che significa che l'impatto di una modifica del codice non si diffonderà scorrendo ciclicamente i componenti. La modifica si interromperà quando colpisce il codice a cui non importa.

Mi chiedo se lo abbiano capovolto in modo che il flusso di controllo passasse attraverso in senso orario. Più su questo, e queste teste di freccia "bianche", più tardi.

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?

Poiché la comunicazione da Controller a Presenter è destinata a passare attraverso il "livello" dell'applicazione, sì, rendere il Controller parte del lavoro di Presenter è probabilmente una perdita. Questa è la mia principale critica all'architettura VIPER .

Il motivo per cui separarli è così importante potrebbe probabilmente essere meglio compreso studiando la segregazione della responsabilità delle query di comando .

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'API attraverso cui si invia l'output, per questo particolare caso d'uso. Non è altro. L'interactactor per questo caso d'uso non ha bisogno di sapere, né di voler sapere, se l'output sta andando a una GUI, una CLI, un registro o un altoparlante audio. Tutto l'interattatore deve sapere è l'API più semplice possibile che gli permetterà di riportare i risultati del suo lavoro.

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.

Il motivo per cui la porta di output è diversa dalla porta di input è che non deve essere PROPRIO dal layer che viene estratto. Cioè, al layer che viene estratto non deve essere consentito dettare modifiche ad esso. Solo il livello dell'applicazione e l'autore dovrebbero decidere che la porta di output può cambiare.

Ciò è in contrasto con la porta di input che è di proprietà del layer che viene estratto. Solo l'autore del livello applicazione dovrebbe decidere se cambiare la porta di input.

Seguire queste regole mantiene l'idea che il livello dell'applicazione, o qualsiasi livello interno, non sappia nulla dei livelli esterni.


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).

La cosa importante di quella freccia "bianca" è che ti permette di fare questo:

inserisci qui la descrizione dell'immagine

Puoi lasciare che il flusso di controllo vada nella direzione opposta della dipendenza! Ciò significa che lo strato interno non deve conoscere lo strato esterno e tuttavia è possibile immergersi nello strato interno e tornare indietro!

Ciò non ha nulla a che fare con l'uso della parola chiave "interfaccia". Potresti farlo con una lezione astratta. Diamine, potresti farlo con una classe (ick) concreta fintanto che può essere estesa. È semplicemente bello farlo con qualcosa che si concentra solo sulla definizione dell'API che Presenter deve implementare. La freccia aperta chiede solo polimorfismo. Che tipo dipende da te.

Perché invertire il senso di tale dipendenza è così importante può essere appreso studiando il principio di inversione di dipendenza . Ho mappato questo principio su questi diagrammi qui .

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 dell'interfaccia Presenter è di essere abbastanza astratto da rappresentare diversi tipi di presentatori (GUI, Web, CLI, ecc.), E che in realtà significa solo "output", che è qualcosa di utile potrebbe benissimo averlo, ma ancora non ne sono completamente sicuro.

No, è proprio così. Il punto per assicurarsi che gli strati interni non siano a conoscenza degli strati esterni è che possiamo rimuovere, sostituire o riformattare gli strati esterni sicuri che così facendo non si romperà nulla negli strati interni. Quello che non sanno non li farà del male. Se riusciamo a farlo, possiamo cambiare quelli esterni in qualunque cosa desideriamo.

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.

Il problema qui ora è tutto ciò che sa come chiedere i dati deve essere anche la cosa che accetta i dati. Prima che il Controller potesse chiamare l'Usecase Interactor beato inconsapevolmente di come sarebbe il modello di risposta, dove dovrebbe andare e, eh, come presentarlo.

Ancora una volta, si prega di studiare la segregazione della responsabilità delle query di comando per capire perché è importante.

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 case, perché ora il controller utilizza solo il metodo getData () (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.

Sì! Dire, non chiedere, aiuterà a mantenere questo oggetto orientato piuttosto che procedurale.

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?

Tutto ciò che funziona è praticabile. Ma non direi che la seconda opzione che hai presentato segue fedelmente Clean Architecture. Potrebbe essere qualcosa che funziona. Ma non è ciò che chiede Clean Architecture.


4
Grazie per aver dedicato del tempo a scrivere una spiegazione così approfondita.
Swahnee,

1
Ho cercato di avvolgere la mia testa intorno a Clean Architecture e questa risposta è stata una risorsa fantastica. Molto ben fatto!
Nathan,

Ottima risposta e in dettaglio .. Grazie per quello. Puoi darmi qualche consiglio (o puntare a una spiegazione) sull'aggiornamento della GUI durante l'esecuzione di UseCase, ovvero l'aggiornamento della barra di avanzamento durante il caricamento di file di grandi dimensioni?
Ewoks,

1
@Ewoks, come risposta rapida alla tua domanda, dovresti esaminare il modello osservabile. Il tuo caso d'uso potrebbe restituire un Soggetto e informare il Soggetto sugli aggiornamenti di avanzamento. Il relatore si iscriverebbe all'oggetto e rispondere alle notifiche.
Nathan,

7

In una discussione relativa alla tua domanda , lo zio Bob spiega lo scopo del presentatore nella sua architettura pulita:

Dato questo esempio di codice:

namespace Some\Controller;

class UserController extends Controller {
    public function registerAction() {
        // Build the Request object
        $request = new RegisterRequest();
        $request->name = $this->getRequest()->get('username');
        $request->pass = $this->getRequest()->get('password');

        // Build the Interactor
        $usecase = new RegisterUser();

        // Execute the Interactors method and retrieve the response
        $response = $usecase->register($request);

        // Pass the result to the view
        $this->render(
            '/user/registration/template.html.twig', 
            array('id' =>  $response->getId()
        );
    }
}

Lo zio Bob ha detto questo:

" Lo scopo del presentatore è di separare i casi d'uso dal formato dell'interfaccia utente. Nel tuo esempio, la variabile $ response viene creata dall'interattatore, ma viene utilizzata dalla vista. Questo accoppia l'interattatore alla vista. Ad esempio , diciamo che uno dei campi nell'oggetto $ response è una data. Quel campo sarebbe un oggetto data binario che potrebbe essere reso in molti formati di data diversi. Vuole un formato data molto specifico, forse GG / MM / AAAA. Di chi è la responsabilità di creare il formato? Se l'interattatore crea quel formato, allora conosce troppo la vista. Ma se la vista prende l'oggetto data binario, allora sa troppo sull'interattatore.

"Il lavoro del presentatore è quello di prendere i dati dall'oggetto risposta e formattarli per la vista. Né la vista né l'interattatore sono a conoscenza dei reciproci formati. "

--- Zio Bob

(AGGIORNAMENTO: 31 maggio 2019)

Data la risposta di zio Bob, penso che non abbia molta importanza se facciamo l' opzione n. 1 (lascia che l'interattatore usi il presentatore) ...

class UseCase
{
    private Presenter presenter;
    private Repository repository;

    public UseCase(Repository repository, Presenter presenter)
    {
        this.presenter = presenter;
        this.repository = repository;
    }

    public void Execute(Request request)
    {
        ...
        Response response = new Response() {...}
        this.presenter.Show(response);
    }
}

... oppure facciamo l' opzione n. 2 (lascia che l'interattatore restituisca la risposta, crei un presentatore all'interno del controller, quindi passi la risposta al presentatore) ...

class Controller
{
    public void ExecuteUseCase(Data data)
    {
        Request request = ...
        UseCase useCase = new UseCase(repository);
        Response response = useCase.Execute(request);
        Presenter presenter = new Presenter();
        presenter.Show(response);
    }
}

Personalmente, preferisco l'opzione # 1 perché voglio essere il controllo in grado all'interno del interactor momento di mostrare i dati e messaggi di errore, come in questo esempio qui di seguito:

class UseCase
{
    private Presenter presenter;
    private Repository repository;

    public UseCase(Repository repository, Presenter presenter)
    {
        this.presenter = presenter;
        this.repository = repository;
    }

    public void Execute(Request request)
    {
        if (<invalid request>) 
        {
            this.presenter.ShowError("...");
            return;
        }

        if (<there is another error>) 
        {
            this.presenter.ShowError("another error...");
            return;
        }

        ...
        Response response = new Response() {...}
        this.presenter.Show(response);
    }
}

... Voglio essere in grado di fare ciò if/elseche è correlato alla presentazione all'interno interactore non all'esterno dell'interattatore.

Se d'altra parte facciamo l'opzione n. 2, dovremmo memorizzare i messaggi di errore responsenell'oggetto, restituire responsequell'oggetto dal interactoral controllere rendere l' controller analisi l' responseoggetto ...

class UseCase
{
    public Response Execute(Request request)
    {
        Response response = new Response();
        if (<invalid request>) 
        {
            response.AddError("...");
        }

        if (<there is another error>) 
        {
            response.AddError("another error...");
        }

        if (response.HasNoErrors)
        {
            response.Whatever = ...
        }

        ...
        return response;
    }
}
class Controller
{
    private UseCase useCase;

    public Controller(UseCase useCase)
    {
        this.useCase = useCase;
    }

    public void ExecuteUseCase(Data data)
    {
        Request request = new Request() 
        {
            Whatever = data.whatever,
        };
        Response response = useCase.Execute(request);
        Presenter presenter = new Presenter();
        if (response.ErrorMessages.Count > 0)
        {
            if (response.ErrorMessages.Contains(<invalid request>))
            {
                presenter.ShowError("...");
            }
            else if (response.ErrorMessages.Contains("another error")
            {
                presenter.ShowError("another error...");
            }
        }
        else
        {
            presenter.Show(response);
        }
    }
}

Non mi piace analizzare i responsedati per gli errori all'interno del controllerperché perché se lo facciamo stiamo facendo un lavoro ridondante --- se cambiamo qualcosa nel interactor, dobbiamo anche cambiare qualcosa nel controller.

Inoltre, se in seguito decidiamo di riutilizzare i nostri interactordati per presentare utilizzando la console, ad esempio, dobbiamo ricordare di copiare e incollare tutti quelli if/elsenella controllernostra app console.

// in the controller for our console app
if (response.ErrorMessages.Count > 0)
{
    if (response.ErrorMessages.Contains(<invalid request>))
    {
        presenterForConsole.ShowError("...");
    }
    else if (response.ErrorMessages.Contains("another error")
    {
        presenterForConsole.ShowError("another error...");
    }
}
else
{
    presenterForConsole.Present(response);
}

Se utilizziamo l'opzione n. 1 avremo questo if/else solo in un posto : il interactor.


Se si utilizza ASP.NET MVC (o altri framework MVC simili), l'opzione 2 è il modo più semplice di procedere.

Ma possiamo ancora fare l'opzione n. 1 in quel tipo di ambiente. Ecco un esempio di fare l'opzione n. 1 in ASP.NET MVC:

(Nota che dobbiamo avere public IActionResult Resultnel presentatore della nostra app ASP.NET MVC)

class UseCase
{
    private Repository repository;

    public UseCase(Repository repository)
    {
        this.repository = repository;
    }

    public void Execute(Request request, Presenter presenter)
    {
        if (<invalid request>) 
        {
            this.presenter.ShowError("...");
            return;
        }

        if (<there is another error>) 
        {
            this.presenter.ShowError("another error...");
            return;
        }

        ...
        Response response = new Response() {
            ...
        }
        this.presenter.Show(response);
    }
}
// controller for ASP.NET app

class AspNetController
{
    private UseCase useCase;

    public AspNetController(UseCase useCase)
    {
        this.useCase = useCase;
    }

    [HttpPost("dosomething")]
    public void ExecuteUseCase(Data data)
    {
        Request request = new Request() 
        {
            Whatever = data.whatever,
        };
        var presenter = new AspNetPresenter();
        useCase.Execute(request, presenter);
        return presenter.Result;
    }
}
// presenter for ASP.NET app

public class AspNetPresenter
{
    public IActionResult Result { get; private set; }

    public AspNetPresenter(...)
    {
    }

    public async void Show(Response response)
    {
        Result = new OkObjectResult(new { });
    }

    public void ShowError(string errorMessage)
    {
        Result = new BadRequestObjectResult(errorMessage);
    }
}

(Nota che dobbiamo avere public IActionResult Resultnel presentatore della nostra app ASP.NET MVC)

Se decidiamo di creare un'altra app per la console, possiamo riutilizzare quanto UseCasesopra e creare solo la Controllere Presenterper la console:

// controller for console app

class ConsoleController
{    
    public void ExecuteUseCase(Data data)
    {
        Request request = new Request() 
        {
            Whatever = data.whatever,
        };
        var presenter = new ConsolePresenter();
        useCase.Execute(request, presenter);
    }
}
// presenter for console app

public class ConsolePresenter
{
    public ConsolePresenter(...)
    {
    }

    public async void Show(Response response)
    {
        // write response to console
    }

    public void ShowError(string errorMessage)
    {
        Console.WriteLine("Error: " + errorMessage);
    }
}

(Si noti che NON ABBIAMO public IActionResult Resultnel presentatore della nostra app console)


Grazie per il contributo Leggendo la conversazione, tuttavia, c'è una cosa che non capisco: dice che il relatore dovrebbe rendere i dati provenienti dalla risposta e allo stesso tempo che la risposta non dovrebbe essere creata dall'interattatore. Ma allora chi sta creando la risposta? Direi che l'interattatore dovrebbe fornire i dati al presentatore, nel formato specifico dell'applicazione, che è noto al presentatore, poiché il livello degli adattatori può dipendere dal livello dell'applicazione (ma non viceversa).
Swahnee,

Mi dispiace. Forse diventa confuso perché non ho incluso l'esempio di codice della discussione. Lo aggiornerò per includere l'esempio di codice.
Jboy Flaga,

Lo zio Bob non ha detto che la risposta non dovrebbe essere creata dall'interattatore. La risposta verrà creata dall'interattatore . Ciò che lo zio Bob sta dicendo è che la risposta creata dall'interattatore verrà utilizzata dal presentatore. Il presentatore quindi "formatterà", inserirà la risposta formattata in un modello di visualizzazione, quindi passerà quel modello di visualizzazione alla vista. <br/> Ecco come lo capisco.
Jboy Flaga,

1
Questo ha più senso. Avevo l'impressione che "view" fosse sinonimo di "presentatore", dal momento che Clean Architecture non menziona né "view" né "viewmodel", che credo siano solo concetti MVC, che possono o meno essere utilizzati durante l'implementazione di un adattatore.
Swahnee,

2

Un caso d'uso può contenere il presentatore o i dati di ritorno, dipende da ciò che è richiesto dal flusso dell'applicazione.

Comprendiamo alcuni termini prima di comprendere i diversi flussi di applicazione:

  • Oggetto di dominio : un oggetto di dominio è il contenitore di dati nel livello di dominio su cui vengono eseguite le operazioni di logica aziendale.
  • Visualizza modello : gli oggetti dominio sono generalmente mappati per visualizzare i modelli nel livello applicazione per renderli compatibili e intuitivi con l'interfaccia utente.
  • Presentatore : mentre un controller nel livello applicazione in genere richiama un caso d'uso, ma è consigliabile delegare il dominio per visualizzare la logica di mappatura del modello in una classe separata (seguendo il principio di responsabilità singola), che si chiama "Presentatore".

Un caso d'uso contenente i dati di ritorno

In un normale caso, un caso d'uso restituisce semplicemente un oggetto di dominio al livello applicazione che può essere ulteriormente elaborato nel livello applicazione per renderlo semplice da mostrare nell'interfaccia utente.

Poiché il responsabile del trattamento è tenuto a invocare il caso d'uso, in questo caso contiene anche un riferimento del rispettivo presentatore per condurre il dominio per visualizzare la mappatura del modello prima di inviarlo per visualizzare il rendering.

Ecco un esempio di codice semplificato:

namespace SimpleCleanArchitecture
{
    public class OutputDTO
    {
        //fields
    }

    public class Presenter 
    {
        public OutputDTO Present(Domain domain)
        {
            // Mapping takes action. Dummy object returned for demonstration purpose
            // Usually frameworks like automapper to the mapping job.
            return new OutputDTO();
        }
    }

    public class Domain
    {
        //fields
    }

    public class UseCaseInteractor
    {
        public Domain Process(Domain domain)
        {
            // additional processing takes place here
            return domain;
        }
    }

    // A simple controller. 
    // Usually frameworks like asp.net mvc provides url routing mechanism to reach here through this type of class.
    public class Controller
    {
        public View Action()
        {
            UseCaseInteractor userCase = new UseCaseInteractor();
            var domain = userCase.Process(new Domain());//passing dummy domain(for demonstration purpose) to process
            var presenter = new Presenter();//presenter might be initiated via dependency injection.

            return new View(presenter.Present(domain));
        }
    }

    // A simple view. 
    // Usually frameworks like asp.net mvc provides mechanism to render html based view through this type of class.
    public class View
    {
        OutputDTO _outputDTO;

        public View(OutputDTO outputDTO)
        {
            _outputDTO = outputDTO;
        }

    }
}

Un caso d'uso contenente presentatore

Sebbene non sia comune, ma è possibile che il caso d'uso debba chiamare il presentatore. In tal caso invece di contenere il riferimento concreto del presentatore, è consigliabile considerare un'interfaccia (o classe astratta) come punto di riferimento (che deve essere inizializzato in tempo di esecuzione tramite iniezione di dipendenza).

Avere il dominio per visualizzare la logica di mappatura del modello in una classe separata (anziché all'interno del controller) interrompe anche la dipendenza circolare tra controller e caso d'uso (quando la classe del caso d'uso fa riferimento alla logica di mappatura).

inserisci qui la descrizione dell'immagine

Di seguito è implementata in modo semplificato il flusso di controllo, come illustrato nell'articolo originale, che dimostra come può essere fatto. Si noti che a differenza di quanto mostrato nel diagramma, per semplicità UseCaseInteractor è una classe concreta.

namespace CleanArchitectureWithPresenterInUseCase
{
    public class Domain
    {
        //fields
    }

    public class OutputDTO
    {
        //fields
    }

    // Use Case Output Port
    public interface IPresenter
    {
        OutputDTO Present(Domain domain);
    }

    public class Presenter: IPresenter
    {
        public OutputDTO Present(Domain domain)
        {
            // Mapping takes action. Dummy object returned for demonstration purpose
            // Usually frameworks like automapper to the mapping job.
            return new OutputDTO();
        }
    }

    // Use Case Input Port / Interactor   
    public class UseCaseInteractor
    {
        IPresenter _presenter;
        public UseCaseInteractor (IPresenter presenter)
        {
            _presenter = presenter;
        }

        public OutputDTO Process(Domain domain)
        {
            return _presenter.Present(domain);
        }
    }

    // A simple controller. 
    // Usually frameworks like asp.net mvc provides url routing mechanism to reach here through this type of class.
    public class Controller
    {
        public View Action()
        {
            IPresenter presenter = new Presenter();//presenter might be initiated via dependency injection.
            UseCaseInteractor userCase = new UseCaseInteractor(presenter);
            var outputDTO = userCase.Process(new Domain());//passing dummy domain (for demonstration purpose) to process
            return new View(outputDTO);
        }
    }

    // A simple view. 
    // Usually frameworks like asp.net mvc provides mechanism to render html based view through this type of class.
    public class View
    {
        OutputDTO _outputDTO;

        public View(OutputDTO outputDTO)
        {
            _outputDTO = outputDTO;
        }

    }
}

1

Anche se in genere sono d'accordo con la risposta di @CandiedOrange, vedrei anche dei benefici nell'approccio in cui l'interattatore ritira semplicemente i dati che vengono poi passati dal controller al presentatore.

Questo ad esempio è un modo semplice per usare le idee di Clean Architecture (Dependency Rule) nel contesto di Asp.Net MVC.

Ho scritto un post sul blog per approfondire questa discussione: https://plainionist.github.io/Implementing-Clean-Architecture-Controller-Presenter/


1

Caso d'uso contenente il presentatore o la restituzione dei dati?

Quindi, una di queste due alternative è l'interpretazione "corretta" della porta di output del caso d'uso secondo Clean Architecture? Sono entrambi vitali?


In breve

Sì, sono entrambi fattibili fintanto che entrambi gli approcci prendono in considerazione l' inversione del controllo tra il livello aziendale e il meccanismo di consegna. Con il secondo approccio, siamo ancora in grado di introdurre il CIO facendo uso di osservatori, mediatori di pochi altri modelli di progettazione ...

Con la sua architettura pulita , il tentativo di zio Bob è di sintetizzare un gruppo di architetture conosciute per rivelare concetti e componenti importanti per farci rispettare ampiamente i principi OOP.

Sarebbe controproducente considerare il suo diagramma di classe UML (il diagramma seguente) come l'esclusivo design Clean Architecture . Questo diagramma avrebbe potuto essere disegnato per il bene di esempi concreti ... Tuttavia, poiché è molto meno astratto del solito rappresentazioni di architettura, ha dovuto fare delle scelte concrete tra cui il design della porta di uscita dell'interattatore che è solo un dettaglio di implementazione ...

Diagramma di classe UML di zio Bob di Clean Architecture


I miei due centesimi

Il motivo principale per cui preferisco restituire il UseCaseResponseè che questo approccio mantiene i miei casi di utilizzo flessibile , che consente sia la composizione tra loro e genericità ( generalizzazione e la generazione specifica ). Un esempio di base:

// A generic "entity type agnostic" use case encapsulating the interaction logic itself.
class UpdateUseCase implements UpdateUseCaseInterface
{
    function __construct(EntityGatewayInterface $entityGateway, GetUseCaseInterface $getUseCase)
    {
        $this->entityGateway = $entityGateway;
        $this->getUseCase = $getUseCase;
    }

    public function execute(UpdateUseCaseRequestInterface $request) : UpdateUseCaseResponseInterface
    {
        $getUseCaseResponse = $this->getUseCase->execute($request);

        // Update the entity and build the response...

        return $response;
    }
}

// "entity type aware" use cases encapsulating the interaction logic WITH the specific entity type.
final class UpdatePostUseCase extends UpdateUseCase;
final class UpdateProductUseCase extends UpdateUseCase;

Si noti che è analogamente più vicino ai casi d'uso UML compresi / estesi a vicenda e definito come riutilizzabile su diversi soggetti (le entità).


Sull'interattatore che restituisce i dati

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).

Non sei sicuro di capire cosa intendi con questo, perché dovresti "controllare" l'esecuzione della presentazione? Non lo controlli fino a quando non restituisci la risposta del caso d'uso?

Il caso d'uso può restituire nella sua risposta un codice di stato per far sapere al livello client cosa è successo esattamente durante il suo funzionamento. I codici di stato della risposta HTTP sono particolarmente adatti per descrivere lo stato di funzionamento di un caso d'uso ...

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.