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:
fuente
Respuestas:
A pesar de las largas líneas de consejos sobre su
WebApp
agregado, estoy totalmente de acuerdo en que tirar de élrepository
no 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, elWebAppService
).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
WebAppRegistered
evento 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!
fuente
WebApp
AR 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!