terça-feira, 23 de fevereiro de 2010

Domain Events

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 List actions;

//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 IHandles where 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();
}

print 1

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 List actions;

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

quarta-feira, 3 de fevereiro de 2010

Boas Práticas para interface

Eu ja havia levantado a bandeira sobre qualidade e organização de código na camada de interface aqui no blog, e tenho percebido que a comunidade tem desprendido um grande esforço nesse sentido também. 

O Jeremy D. Miller publicou no seu blog uma série de posts sobre melhores práticas na elaboração de interface:

The Build Your Own CAB Series Table of Contents
http://codebetter.com/blogs/jeremy.miller/archive/2007/07/25/the-build-your-own-cab-series-table-of-contents.aspx
http://www.jeremydmiller.com/ppatterns/(X(1)S(x4of0b450vkpvp454kqddkvw))/default.aspx?AspxAutoDetectCookieSupport=1

Já temos também vários recursos para facilitar a nossa vida: ASP.Net MVC Framework
http://www.asp.net/%28S%28d35rmemuuono1wvm1gsp2n45%29%29/mvc/

E para quem tem o Visual Studio temos também:
Web Client Developer Guidance
http://www.codeplex.com/webclientguidance

Composite WPF and Silverlight
http://www.codeplex.com/CompositeWPF

Se o grande problema para se utilizar as boas práticas era a perca de produtividade, já faz algum tempo que esse argumento não cola a mais.