Come applicare alcuni concetti di DDD al codice reale? Domande specifiche all'interno


9

Ho studiato DDD e attualmente sto lottando per trovare un modo per applicare i concetti nel codice reale. Ho circa 10 anni di esperienza con N-tier, quindi è molto probabile che il motivo per cui sto lottando è che il mio modello mentale è troppo abbinato a quel design.

Ho creato un'applicazione Web Asp.NET e sto iniziando con un dominio semplice: un'applicazione di monitoraggio web. Requisiti:

  • L'utente deve essere in grado di registrare una nuova app Web per il monitoraggio. L'app Web ha un nome descrittivo e punta a un URL;
  • L'app Web eseguirà periodicamente il polling per uno stato (online / offline);
  • L'app Web eseguirà periodicamente il polling per la sua versione corrente (si prevede che l'app Web abbia un "/version.html", che è un file che dichiara la sua versione di sistema in un markup specifico).

I miei dubbi riguardano principalmente la divisione delle responsabilità, la ricerca del posto adeguato per ogni cosa (convalida, regola aziendale, ecc.). Di seguito, ho scritto del codice e aggiunto commenti con domande e considerazioni.

Si prega di criticare e consigliare . Grazie in anticipo!


MODELLO DOMINIO

Modellato per incapsulare tutte le regole aziendali.

// Encapsulates logic for creating and validating Url's.
// Based on "Unbreakable Domain Models", YouTube talk from Mathias Verraes
// See https://youtu.be/ZJ63ltuwMaE
public class Url: ValueObject
{
    private System.Uri _uri;

    public string Url => _uri.ToString();

    public Url(string url)
    {
        _uri = new Uri(url, UriKind.Absolute); // Fails for a malformed URL.
    }
}

// Base class for all Aggregates (root or not).
public abstract class Aggregate
{
    public Guid Id { get; protected set; } = Guid.NewGuid();
    public DateTime CreatedAt { get; protected set; } = DateTime.UtcNow;
}

public class WebApp: Aggregate
{
    public string Name { get; private set; }
    public Url Url { get; private set; }
    public string Version { get; private set; }
    public DateTime? VersionLatestCheck { get; private set; }
    public bool IsAlive { get; private set; }
    public DateTime? IsAliveLatestCheck { get; private set; }

    public WebApp(Guid id, string name, Url url)
    {
        if (/* some business validation fails */)
            throw new InvalidWebAppException(); // Custom exception.

        Id = id;
        Name = name;
        Url = url;
    }

    public void UpdateVersion()
    {
        // Delegates the plumbing of HTTP requests and markup-parsing to infrastructure.
        var versionChecker = Container.Get<IVersionChecker>();
        var version = versionChecker.GetCurrentVersion(this.Url);

        if (version != this.Version)
        {
            var evt = new WebAppVersionUpdated(
                this.Id, 
                this.Name, 
                this.Version /* old version */, 
                version /* new version */);
            this.Version = version;
            this.VersionLatestCheck = DateTime.UtcNow;

            // Now this eems very, very wrong!
            var repository = Container.Get<IWebAppRepository>();
            var updateResult = repository.Update(this);
            if (!updateResult.OK) throw new Exception(updateResult.Errors.ToString());

            _eventDispatcher.Publish(evt);
        }

        /*
         * I feel that the aggregate should be responsible for checking and updating its
         * version, but it seems very wrong to access a Global Container and create the
         * necessary instances this way. Dependency injection should occur via the
         * constructor, and making the aggregate depend on infrastructure also seems wrong.
         * 
         * But if I move such methods to WebAppService, I'm making the aggregate
         * anaemic; It will become just a simple bag of getters and setters.
         *
         * Please advise.
         */
    }

    public void UpdateIsAlive()
    {
        // Code very similar to UpdateVersion().
    }
}

E una classe DomainService per gestire le creature e le eliminazioni, che credo non riguardino l'aggregato stesso.

public class WebAppService
{
    private readonly IWebAppRepository _repository;
    private readonly IUnitOfWork _unitOfWork;
    private readonly IEventDispatcher _eventDispatcher;

    public WebAppService(
        IWebAppRepository repository, 
        IUnitOfWork unitOfWork, 
        IEventDispatcher eventDispatcher
    ) {
        _repository = repository;
        _unitOfWork = unitOfWork;
        _eventDispatcher = eventDispatcher;
    }

    public OperationResult RegisterWebApp(NewWebAppDto newWebApp)
    {
        var webApp = new WebApp(newWebApp);

        var addResult = _repository.Add(webApp);
        if (!addResult.OK) return addResult.Errors;

        var commitResult = _unitOfWork.Commit();
        if (!commitResult.OK) return commitResult.Errors;

        _eventDispatcher.Publish(new WebAppRegistered(webApp.Id, webApp.Name, webApp.Url);
        return OperationResult.Success;
    }

    public OperationResult RemoveWebApp(Guid webAppId)
    {
        var removeResult = _repository.Remove(webAppId);
        if (!removeResult) return removeResult.Errors;

        _eventDispatcher.Publish(new WebAppRemoved(webAppId);
        return OperationResult.Success;
    }
}

STRATO DI APPLICAZIONE

La classe seguente fornisce un'interfaccia per il dominio WebMonitoring verso il mondo esterno (interfacce web, API di riposo, ecc.). In questo momento è solo una shell che reindirizza le chiamate ai servizi appropriati, ma in futuro crescerebbe per orchestrare più logica (eseguita sempre tramite modelli di dominio).

public class WebMonitoringAppService
{
    private readonly IWebAppQueries _webAppQueries;
    private readonly WebAppService _webAppService;

    /*
     * I'm not exactly reaching for CQRS here, but I like the idea of having a
     * separate class for handling queries right from the beginning, since it will
     * help me fine-tune them as needed, and always keep a clean separation between
     * crud-like queries (needed for domain business rules) and the ones for serving
     * the outside-world.
     */

    public WebMonitoringAppService(
        IWebAppQueries webAppQueries, 
        WebAppService webAppService
    ) {
        _webAppQueries = webAppQueries;
        _webAppService = webAppService;
    }

    public WebAppDetailsDto GetDetails(Guid webAppId)
    {
        return _webAppQueries.GetDetails(webAppId);
    }

    public List<WebAppDetailsDto> ListWebApps()
    {
        return _webAppQueries.ListWebApps(webAppId);
    }

    public OperationResult RegisterWebApp(NewWebAppDto newWebApp)
    {
        return _webAppService.RegisterWebApp(newWebApp);
    }

    public OperationResult RemoveWebApp(Guid webAppId)
    {
        return _webAppService.RemoveWebApp(newWebApp);
    }
}

Chiudere gli argomenti

Dopo aver raccolto le risposte qui e in questa altra domanda , che ho aperto per un motivo diverso, ma alla fine sono arrivato allo stesso punto di questo, ho trovato questa soluzione più pulita e migliore:

Proposta di soluzione in Github Gist


Ho letto molto, ma non ho trovato esempi pratici, ad eccezione di quelli che applicano CQRS e altri schemi e pratiche ortogonali, ma sto cercando questa cosa semplice in questo momento.
Levidad,

1
Questa domanda potrebbe essere più adatta a codereview.stackexchange.com
VoiceOfUnreason

2
Anche tu mi piaci con molto tempo trascorso con le app di livello superiore. Conosco DDD solo da libri, forum, ecc., Quindi posterò solo un commento. Esistono due tipi di convalida: convalida dell'input e convalida delle regole aziendali. La convalida dell'input va nel livello Applicazione e la convalida del dominio nel Livello dominio. WebApp assomiglia più a un'entità e non a aggreagate e WebAppService assomiglia più a un servizio applicativo che a DomainService. Anche il tuo aggregato fa riferimento al Container che è una preoccupazione infrastrutturale. Sembra anche un localizzatore di servizi.
Adrian Iftode,

1
Sì, perché non modella una relazione. Gli aggregati modellano le relazioni tra gli oggetti dominio. WebApp ha solo dati non elaborati e alcuni comportamenti e potrebbe gestire ad esempio il seguente invariante: non va bene aggiornare le versioni come un matto, ad esempio passare alla versione 3 quando la versione corrente è 1.
Adrian Iftode

1
Finché ValueObject ha un metodo che implementa l'uguaglianza tra istanze, penso che sia ok. Nel tuo scenario è possibile creare un oggetto valore versione. Controlla il controllo delle versioni semantico, otterrai molte idee su come modellare questo oggetto di valore, inclusi invarianti e comportamento. WebApp non dovrebbe parlare con un repository, in realtà credo sia sicuro non avere alcun riferimento dal tuo progetto che contenga le cose del dominio a qualsiasi altra cosa relativa all'infrastruttura (repository, unità di lavoro) né direttamente né indirettamente (tramite interfacce).
Adrian Iftode,

Risposte:


1

A lungo le linee di consulenza sul tuo WebAppaggregato, sono pienamente d'accordo che tirare repositoryqui non è l'approccio giusto qui. Nella mia esperienza, l'aggregato prenderà la "decisione" se un'azione va bene o meno in base al proprio stato. Pertanto, non allo stato potrebbe estrarre da altri servizi. Se avessi bisogno di un tale controllo, generalmente lo sposterei al servizio che chiama l'aggregato (nel tuo esempio il WebAppService).

Inoltre, è possibile atterrare sul caso d'uso in cui diverse applicazioni desiderano chiamare contemporaneamente il proprio aggregato. Se ciò dovesse accadere, mentre fai chiamate in uscita come questa che potrebbero richiedere molto tempo, stai bloccando così il tuo aggregato per altri usi. Questo alla fine rallenterebbe la gestione degli aggregati, cosa che penso non sia neppure desiderabile.

Quindi, anche se potrebbe sembrare che il tuo aggregato diventi piuttosto sottile se sposti quel bit di convalida, penso che sia meglio spostarlo su WebAppService.

Suggerirei anche di spostare la pubblicazione WebAppRegistereddell'evento nel tuo aggregato. L'aggregato è il ragazzo che viene creato, quindi se il suo processo di creazione ha successo, ha senso lasciarlo pubblicare tale conoscenza al mondo.

Spero che questo ti aiuti @Levidad!


Ciao Steven, grazie per il tuo contributo. Ho aperto un'altra domanda qui che alla fine è arrivato allo stesso punto di questa domanda e alla fine ho trovato un tentativo di soluzione più pulita per questo problema. Per favore, dai un'occhiata e condividi i tuoi pensieri? Penso che vada nella direzione dei tuoi suggerimenti sopra.
Levidad,

Certamente Levidad, darò un'occhiata!
Steven

1
Ho appena controllato entrambe le risposte, da "Voice of Unreason" e "Erik Eidt". Entrambi sono sulla falsariga di ciò che vorrei commentare sulla domanda che hai lì, quindi non posso davvero aggiungere valore lì. E, per rispondere alla tua domanda: il modo in cui il tuo WebAppAR è impostato nella "Soluzione più pulita" che condividi è in linea con quello che considererei un buon approccio per un aggregato. Spero che questo ti aiuti Levidad!
Steven
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.