¿Cómo aplicar algunos conceptos de DDD al código real? Preguntas específicas dentro

9

He estado estudiando DDD y actualmente estoy luchando por encontrar una manera de aplicar los conceptos en el código real. Tengo alrededor de 10 años de experiencia con N-tier, por lo que es muy probable que la razón por la que estoy luchando es porque mi modelo mental está demasiado acoplado a ese diseño.

He creado una aplicación web Asp.NET y estoy comenzando con un dominio simple: una aplicación de monitoreo web. Requisitos:

  • El usuario debe poder registrar una nueva aplicación web para monitorear. La aplicación web tiene un nombre descriptivo y apunta a una URL;
  • La aplicación web sondeará periódicamente un estado (en línea / fuera de línea);
  • La aplicación web sondeará periódicamente su versión actual (se espera que la aplicación web tenga un "/version.html", que es un archivo que declara su versión del sistema en un marcado específico).

Mis dudas se refieren principalmente a la división de responsabilidades, encontrar el lugar adecuado para cada cosa (validación, regla de negocios, etc.). A continuación, escribí un código y agregué comentarios con preguntas y consideraciones.

Por favor critica y aconseja . ¡Gracias por adelantado!


MODELO DE DOMINIO

Modelado para encapsular todas las reglas comerciales.

// 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().
    }
}

Y una clase DomainService para manejar Crea y Elimina, que creo que no es asunto del Agregado en sí.

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

CAPA DE APLICACIÓN

La siguiente clase proporciona una interfaz para el dominio de WebMonitoring con el mundo exterior (interfaces web, API de descanso, etc.). Es solo un shell en este momento, redirigiendo las llamadas a los servicios apropiados, pero crecería en el futuro para orquestar más lógica (lograda siempre a través de modelos de 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);
    }
}

Cerrando los asuntos

Después de reunir las respuestas aquí y en esta otra pregunta , que abrí por una razón diferente pero que finalmente llegué al mismo punto que esta, se me ocurrió esta solución más limpia y mejor:

Propuesta de solución en Github Gist

Levidad
fuente
He estado leyendo mucho, pero no he encontrado ejemplos tan prácticos, excepto los que aplican CQRS y otros patrones y prácticas ortogonales, pero estoy buscando esta cosa simple en este momento.
Levidad
1
Esta pregunta podría ser mejor para codereview.stackexchange.com
VoiceOfUnreason el
2
Yo mismo me gustas con mucho tiempo dedicado a las aplicaciones de n niveles. Sé sobre DDD solo de libros, foros, etc., así que publicaré solo un comentario. Hay dos tipos de validación: validación de entrada y validación de reglas de negocio. La validación de entrada va en la capa de aplicación y la validación de dominio va en la capa de dominio. La WebApp se parece más a una Entidad y no a un agregado y WebAppService se parece más a un servicio de aplicación que a un DomainService. También su agregado hace referencia al Contenedor, que es una preocupación de infraestructura. También parece un localizador de servicios.
Adrian Iftode
1
Sí, porque no modela una relación. Los agregados están modelando las relaciones entre los objetos de dominio. WebApp solo tiene datos sin procesar y algo de comportamiento y podría tratar, por ejemplo, con la siguiente invariante: no está bien actualizar las versiones como locas, es decir, pasar a la versión 3 cuando la versión actual es 1.
Adrian Iftode
1
Mientras ValueObject tenga un método que implemente la igualdad entre instancias, creo que está bien. En su escenario, puede crear un objeto de valor Versión. Compruebe las versiones semánticas, obtendrá muchas ideas sobre cómo puede modelar este objeto de valor, incluidos los invariantes y el comportamiento. WebApp no ​​debería hablar con un repositorio, de hecho, creo que es seguro no tener ninguna referencia de su proyecto que contenga el material del dominio a cualquier otra cosa relacionada con la infraestructura (repositorios, unidad de trabajo), ya sea directa o indirectamente (a través de interfaces).
Adrian Iftode

Respuestas:

1

A pesar de las largas líneas de consejos sobre su WebAppagregado, estoy totalmente de acuerdo en que tirar de él repositoryno es el enfoque correcto aquí. En mi experiencia, el Agregado tomará la 'decisión' de si una acción está bien o no en función de su propio estado. Por lo tanto, no en estado, podría extraerse de otros servicios. Si necesita una verificación de este tipo, generalmente la trasladaría al servicio que llama el agregado (en su ejemplo, el WebAppService).

Además, puede llegar al caso de uso en el que varias aplicaciones desean llamar simultáneamente a su agregado. Si esto sucediera, mientras realiza llamadas salientes como esta, lo que puede llevar mucho tiempo, está bloqueando su agregado para otros usos. Esto eventualmente ralentizaría el manejo de agregados, algo que creo que tampoco es deseable.

Entonces, aunque parezca que su agregado se vuelve bastante delgado si mueve ese bit de validación, creo que es mejor moverlo al WebAppService.

También sugeriría mover la publicación del WebAppRegisteredevento a su agregado. El agregado es el tipo que se está creando, por lo que si su proceso de creación tiene éxito, tiene sentido dejar que publique ese conocimiento en el mundo.

Espero que esto te ayude @Levidad!

Steven
fuente
Hola Steven, gracias por tu aporte. Abrí otra pregunta aquí que finalmente llegó al mismo punto de esta pregunta, y finalmente se me ocurrió un intento de solución más limpia para este problema. ¿Podrías echar un vistazo y compartir tus pensamientos? Creo que va en la dirección de sus sugerencias anteriores.
Levidad
Claro que sí Levidad, ¡lo echaré un vistazo!
Steven
1
Acabo de comprobar ambas respuestas, de 'Voice of Unreason' y 'Erik Eidt'. Ambos están en la línea de lo que comentaría sobre la pregunta que tiene allí, por lo que realmente no puedo agregar valor allí. Y, para responder a su pregunta: la forma en que su WebAppAR está configurado en la 'Solución más limpia' que comparte es, de hecho, lo que yo consideraría un buen enfoque para un Agregado. Espero que esto te ayude Levidad!
Steven