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

9

Ho studiato DDD e attualmente sto cercando di trovare un modo per applicare i concetti nel codice reale. Ho circa 10 anni di esperienza con N-tier, quindi è molto probabile che la ragione per cui sto lottando sia che il mio modello mentale è troppo accoppiato 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 da monitorare. L'app web ha un nome descrittivo e punta a un URL;
  • L'app web effettuerà periodicamente il polling di uno stato (online / offline);
  • L'app web eseguirà periodicamente il polling della sua versione corrente (l'app web avrà 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 giusto per ogni cosa (convalida, regola aziendale, ecc.). Di seguito, ho scritto del codice e ho 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 Creates and Delete, che ritengo non riguardi lo stesso Aggregate.

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;
    }
}

LAYER APPLICAZIONE

La classe seguente fornisce un'interfaccia per il dominio WebMonitoring al mondo esterno (interfacce web, rest api, ecc.). In questo momento è solo una shell, che reindirizza le chiamate ai servizi appropriati, ma in futuro sarebbe destinata a orchestrare più logica (realizzata 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);
    }
}

Chiusura degli argomenti

Dopo aver raccolto le risposte qui e in questa altra domanda , che ho aperto per un motivo diverso ma che ultimamente è arrivata allo stesso punto di questa, ho trovato questa soluzione più pulita e migliore:

Proposta della soluzione in Github Gist

    
posta Levidad 02.01.2018 - 16:01
fonte

1 risposta

1

A lungo le linee di consulenza sul tuo WebApp aggregato, concordo pienamente sul fatto che l'inserimento di repository non sia l'approccio giusto qui. Nella mia esperienza, l'Aggregato farà la 'decisione' se un'azione è ok o meno in base al suo stato. Quindi non sullo stato potrebbe trarre da altri servizi. Se avessi bisogno di un simile controllo, generalmente lo sposterei al servizio che chiama l'aggregato (nel tuo esempio WebAppService ).

Inoltre, potresti arrivare al caso d'uso in cui diverse applicazioni vogliono chiamare contemporaneamente il tuo aggregato. Se ciò accadesse, mentre stai facendo chiamate in uscita come questa che potrebbero richiedere molto tempo, stai bloccando il tuo aggregato per altri usi. Questo alla fine rallenterebbe la gestione degli aggregati, cosa che a mio avviso non è nemmeno auspicabile.

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

Suggerisco anche di spostare la pubblicazione dell'evento WebAppRegistered nel tuo aggregato. L'aggregato è il ragazzo che viene creato, quindi se il processo di creazione ha successo, ha senso lasciarlo pubblicare quel knowlegde al mondo.

Spero che questo ti aiuti @Levidad!

    
risposta data 03.01.2018 - 09:59
fonte

Leggi altre domande sui tag