Quando você utiliza DDD e começa a trabalhar com o modelo rico, você pode começar a se deparar com algumas situações que nem sempre é muito simples achar a melhor a solução, mas uma dessas soluções que você pode utilizar são os eventos de domínio (Domain Events).
Vamos imaginar o seguinte cenário: “Eu preciso cadastrar uma ordem de compra, que no momento da criação ela fica aguardando aprovação.”
Antes de adicionar algum tempero nesse cenário vamos começar a desenhar a classe:
namespace Domain.Entities
{
public enum PurchaseOrderStateEnum
{
WaitingApproval,
Approval
}
public class PurchaseOrder
{
public virtual int Id { get; private set; }
public virtual int OrderNumber { get; private set; }
public virtual PurchaseOrderStateEnum State { get; private set; }
protected PurchaseOrder() { }
public PurchaseOrder(int orderNumber)
{
OrderNumber = orderNumber;
State = PurchaseOrderStateEnum.WaitingApproval;
}
public virtual void Approval()
{
State = PurchaseOrderStateEnum.Approval;
}
}
}
Agora vamos começar a adicionar os temperos: “Quando a ordem de aprovação for aprovada é necessário enviar um e-mail para o gestor da área de compas informando da aprovação da ordem.”
Bom nós já discutimos aqui que enviar e-mail é algo que deve ser feito na camada de infraestrutura, e também sabemos que o domínio não pode depender da camada de infraestrutura, certo?
Uma possível solução para o problema poderia ser o seguinte:
public class PurchaseOrder
{
public virtual ISendEmailBeforeApprovedPurchase SendEmailBeforeApprovedPurchase { get; private set; }
public virtual int Id { get; private set; }
public virtual int OrderNumber { get; private set; }
public virtual PurchaseOrderStateEnum State { get; private set; }
protected PurchaseOrder() { }
public PurchaseOrder(int orderNumber)
{
OrderNumber = orderNumber;
State = PurchaseOrderStateEnum.WaitingApproval;
}
public virtual void Approval()
{
State = PurchaseOrderStateEnum.Approval;
SendEmailBeforeApprovedPurchase.SendEmail(this);
}
}
public interface ISendEmailBeforeApprovedPurchase()
{
void SendEmail(PurchaseOrder purchaseOrder);
}
Dessa forma podemos “injetar” o serviço de envio de e-mail através de um DI/IoC, mas como tudo nessa vida nada é perfeito, só Deus.
Como faríamos para implementar um segundo comportamento quando uma ordem compra fosse aprovada? Vamos ver como fazer isso com eventos de domínio:
public virtual void Approval()
{
State = PurchaseOrderStateEnum.Approval;
DomainEvents.Raise(new PurchaseOrderAprrovedEvent { PurchaseOrder = this });
}
O que estamos fazendo é dizendo para o gerenciador de eventos de domínio que, quando o método Approval for chamado, o evento PurchaseOrderApprovedEvent deve ser disparado. Vamos analisar a estrutura do evento:
namespace Domain.Events
{
public interface IDomainEvent { }
public class PurchaseOrderAprrovedEvent : IDomainEvent
{
public PurchaseOrder PurchaseOrder { get; set; }
}
}
Veja o código do gerenciador de eventos:
public static class DomainEvents
{
[ThreadStatic] //so that each thread has its own callbacks
private static Listactions;
//Registers a callback for the given domain event
public static void Register(Action callback) where T : IDomainEvent
{
if (actions == null)
actions = new List();
actions.Add(callback);
}
//Clears callbacks passed to Register on the current thread
public static void ClearCallbacks()
{
actions = null;
}
//Raises the given domain event
public static void Raise(T args) where T : IDomainEvent
{
if (actions != null)
foreach (var action in actions)
if (action is Action)
((Action)action)(args);
}
}
Agora que disparamos o evento precisamos criar interessados em saber quando esse evento é disparado.
namespace Domain.Handles
{
public interface IHandleswhere T : IDomainEvent
{
void Handles(T args);
}
}
namespace Infrastructure.Handles
{
public class SendEmailBeforeApprovedPurchaseHandler : IHandles
{
public void Handles(PurchaseOrderAprrovedEvent args)
{
Console.WriteLine("mail sent successfully");
}
}
}
Agora só precisamos adicionar “o interessado” na classe de gerenciamento de eventos de domínio para que ele seja notificado:
static void Main(string[] args)
{
DomainEvents.Register(
new Action(delegate(PurchaseOrderAprrovedEvent evt)
{
new SendEmailBeforeApprovedPurchaseHandler().Handles(evt);
}));
var purchase = new PurchaseOrder(123456);
purchase.Approval();
Console.Read();
}
Talvez você esteja pensando o seguinte: “Cara, consigo fazer a mesma coisa com mas de outro jeito.”. Com certeza existe outras formas de se chegar no mesmo resultado, mas vamos adicionar um pouco mais de tempero para ver o potencial dos eventos de domínio: “Ao aprovar uma ordem de compra, além de enviar o e-mail, eu preciso que um recebimento seja gerado com as mesmas informações da ordem de compra automaticamente.”.
Primeiramente vamos criar mais um “interessado” em saber que a ordem de compra foi aprovada.
public class GenerateReceiptFromPurchaseOrderApprovedHandler : IHandles
{
public void Handles(PurchaseOrderAprrovedEvent args)
{
Console.WriteLine("Receipt order created.");
}
}
Agora basta apenas adicioná-lo ao gerenciador de eventos de domínio:
static void Main(string[] args)
{
DomainEvents.Register(
new Action(delegate(PurchaseOrderAprrovedEvent evt)
{
new SendEmailBeforeApprovedPurchaseHandler().Handles(evt);
}));
DomainEvents.Register(
new Action(delegate(PurchaseOrderAprrovedEvent evt)
{
new GenerateReceiptFromPurchaseOrderApprovedHandler().Handles(evt);
}));
var purchase = new PurchaseOrder(123456);
purchase.Approval();
Console.Read();
}
Pronto, conseguimos adicionar mais umcomportamento ao processo de aprovação de ordem de compra sem a necessidade de alterar a classe, respeitando assim o OCP, e cada novo comportamento tem apenas uma única responsabilidade respeitando o SRP.
Mas perceba o ato de adicionar os interessado, esta sendo feito manualmente pra facilitar um puco podemos utilizar um DI/IoC para adiciona-los ao gerenciador de eventos. Vamos ver um exemplo com o StructureMap.
Precisamos fazer a seguinte alteração no gerenciador de eventos:
public static class DomainEvents
{
[ThreadStatic] //so that each thread has its own callbacks
private static Listactions;
public static IContainer HandlesContainer { get; set; }
//Registers a callback for the given domain event
public static void Register(Action callback) where T : IDomainEvent
{
if (actions == null)
actions = new List();
actions.Add(callback);
}
//Clears callbacks passed to Register on the current thread
public static void ClearCallbacks()
{
actions = null;
}
//Raises the given domain event
public static void Raise(T args) where T : IDomainEvent
{
if (HandlesContainer != null)
foreach (var handler in HandlesContainer.GetAllInstances>())
handler.Handles(args);
if (actions != null)
foreach (var action in actions)
if (action is Action)
((Action)action)(args);
}
}
Agora vamos juntar tudo:
static void Main(string[] args)
{
var container = new Container(x =>
{
x.For<IHandles<PurchaseOrderAprrovedEvent>>().Add<SendEmailBeforeApprovedPurchaseHandler>();
x.For<IHandles<PurchaseOrderAprrovedEvent>>().Add<GenerateReceiptFromPurchaseOrderApprovedHandler>();
});
DomainEvents.HandlesContainer = container;
var purchase = new PurchaseOrder(123456);
purchase.Approval();
Console.Read();
}
Legal né, mas no mundo real, quem faz uma alteração em uma entidade e não persiste no banco né? Como é que colocamos a persistencia no meio de tudo isso?
Vamos criar o repositório, pra isso vou utilizar o Fluent NHibernate:
namespace Domain.Repositories
{
public interface IPurchaseOrderRepository
{
void Save(PurchaseOrder purchaseOrder);
}
}
namespace Infrastructure.Repositories.Mapping
{
public class PurchaseOrderMap : ClassMap
{
public PurchaseOrderMap()
{
Id(x => x.Id).GeneratedBy.Identity();
Map(x => x.OrderNumber).Not.Nullable();
Map(x => x.State);
}
}
}
namespace Infrastructure.Repositories
{
public class PurchaseOrderRepository : IPurchaseOrderRepository
{
private readonly ISession _session;
public PurchaseOrderRepository(ISession session)
{
_session = session;
}
public void Save(PurchaseOrder purchaseOrder)
{
_session.SaveOrUpdate(purchaseOrder);
}
}
}
Vamos salvar a ordem de compra após realizar a aprovação:
static void Main(string[] args)
{
Configuration configuration = null;
var sessionFactory = Fluently.Configure()
.Database(SQLiteConfiguration.Standard.InMemory().ShowSql())
.Mappings(M => M.FluentMappings.AddFromAssembly(typeof(PurchaseOrderRepository).Assembly))
.ExposeConfiguration(c => configuration = c)
.BuildSessionFactory();
var container = new Container(x =>
{
x.For>().Add ();
x.For>().Add ();
x.For().Singleton().Use(sessionFactory);
x.For().Singleton().Use(s => s.GetInstance ().OpenSession());
x.For().Use ();
});
DomainEvents.HandlesContainer = container;
BuildSchema(configuration, container.GetInstance());
var purchase = new PurchaseOrder(123456);
purchase.Approval();
var repository = container.GetInstance();
repository.Save(purchase);
Console.Read();
}
public static void BuildSchema(Configuration cfg, ISession se)
{
SchemaExport export = new SchemaExport(cfg);
export.Execute(true, true, false, se.Connection, null);
}
public class NHibernateTestRegistry : Registry
{
public static Configuration cfg;
public NHibernateTestRegistry()
{
Configuration configuration = null;
var sessionFactory = Fluently.Configure()
.Database(SQLiteConfiguration.Standard.InMemory().ShowSql())
.Mappings(M => M.FluentMappings.AddFromAssembly(typeof(PurchaseOrderRepository).Assembly))
.ExposeConfiguration(c => configuration = c)
.BuildSessionFactory();
For().Singleton().Use(sessionFactory);
For().Use(x => x.GetInstance ().OpenSession());
For().Use ();
}
}
Concluindo: Eventos de domínio é uma grande solução para manter a sua arquitetura saudavel. Podemos incorporar vários comportamentos a uma determinada ação que ocorre em um objeto do domínio sem ter que alterá-lo, com certeza vale a pena tê-lo como uma carta na manga.
Fontes:
http://www.udidahan.com/2009/06/14/domain-events-salvation/
http://martinfowler.com/eaaDev/DomainEvent.html
http://unplugged.giggio.net/unplugged/post/Domain-Events.aspx
Nenhum comentário:
Postar um comentário