O DDD ou Domain Driven Design é uma abordagem muito conhecida e robusta do desenvolvimento de sistemas. Embora seja muito divulgada e valorizada ela é, muitas vezes, pouco compreendida. O artigo Notification Pattern no DDD visa mostrar que as entidades precisam estar sempre válidas, mas isso tem consequências que precisam ser corretamente trabalhadas. O Padrão em questão auxilia no controle das validações que ocorrem na construção de agregados simples ou não.
Aqui no blog temos um grande conjunto de artigos sobre Domain Driven Design que, embora não sejam obrigatórios para a leitura desse, são certamente uma boa fonte de informação. Vou destacar alguns aqui para conhecimento.
- Domain Driven Design estratégico
- Domain Driven Design tático
- Factory no Domain Driven Design
- Specification Pattern no Domain Driven Design
- Validation Pattern no Domain Driven Design
- Domain Service no Domain Driven Design
- Mediator Pattern no Domain Driven Design
Sumário
O que é Notification Pattern
Para começar é importante entender que esse padrão não tem relação direta com o Domain Driven Design mas ele tem uma afinidade especial com a abordagem. Como o DDD espera que suas entidades estejam sempre num estado válido, estratégias de validação podem gerar certo incomodo quando entidades ou agregados maiores são criados. Isso porque a aplicação ao criar tais objetos de domínio ela poderá receber erros de validação, mas um por vez: isso é bastante chato.
Para lidar com as questões de validação é muito usual trabalhar com o Validation Pattern, de forma a garantir a consistência do agregado e pela constante preocupação com os princípios SOLID. Também é Comum o uso de Factories do DDD para a construção de agregações inteiras ou várias agregações. Tais validações são características do domínio da aplicação, ou seja, aplicações distintas deveriam utilizar as mesmas regras, portanto, tudo isso deve estar preservado no Domínio e não espalhado nas demais camadas.
Então, para lidar com a situação incomoda dos erros que surgem um por vez, acumulando-os num só agrupo de erros, o Notification Pattern no DDD é uma abordagem conveniente. Para nossos exemplos utilizaremos o FluentValidation na linguagem C# para aplicar esse padrão.
Como utilizar o Notification Pattern
Essa primeira parte apresenta um ResultObject para empacotar a potencial falha numa estrutura específica. Trata-se de um código bastante simples que será mais explorado no final do artigo.
public class NotificationResult
{
public NotificationResult(
bool success,
string message,
object data)
{
Success = success;
Message = message;
Data = data;
}
public bool Success { get; private set; }
public string Message { get; private set; }
public object Data { get; private set; }
}
Agora é possível ver a classe User herda da classe Notifiable (classe abstrata do Fluent Validator). Ela possui algumas estruturas para lidar com listas de notificações. Note no exemplo a chamada para AddNotifications(…) que vem dessa herança.
public class User: Notifiable
{
public User(string name, string emailAddress)
{
Name = name;
EmailAddress = emailAddress;
AddNotifications(new ValidationContract()
.HasMinLen(Name, 3, "Name", "Name property must have at least 3 chars")
.HasMaxLen(Name, 10, "Name", "Name must not exceed 10 chars")
.IsEmail(EmailAddress, EmailAddress, "Email is not valid")
);
}
public Guid Id { get; private set; }
public string Name { get; private set; }
public string EmailAddress { get; private set; }
}
Agora temos uma classe chamada Verifier que também herda de Notifiable entregando o ResultObject criado anteriormente chamado NotificationResult. Note que essa validação poderia fazer parte de uma Factory do DDD, uma vez que ela agrupa potenciais validações. Mas não há nenhuma prescrição nesse sentido.
public class Verifier : Notifiable
{
public NotificationResult Execute()
{
var user = new User("Thiago", "[email protected]");
return Verify(user);
}
private NotificationResult Verify(User user)
{
//Validates entity
AddNotifications(user.Notifications);
if (Invalid)
{
return new NotificationResult(false,"Please validate following fields:", Notifications);
}
return new NotificationResult(true,"Validation success",null);
}
}
Por fim, veja a utilização prática em uma aplicação console, apenas chamando o método Execute() do Verifier() instanciado. O success dá a informação necessária para a decisão.
class Program
{
static void Main(string[] args)
{
var verify = new Verifier().Execute();
if(verify.success)
{
// ... Success code
}
else
{
// ... Error code
}
}
}
Result Objects
O contexto do Notification Pattern tem uma afinada relação com os ResultObjects, que são classes que representam um envelope com o objeto que efetivamente tem o interesse de expor. Além do objeto há informações de controle como o status de sucesso ou mensagem de erro, caso haja. A pilha de protocolos utilizados nas diferentes camadas OSI implementam conceitualmente esse padrão, de muitos modos diferenetes.
Vale esclarecer que esse formato não se limita a tais atributos. Veja que o protocolo HTTP, por exemplo, retorna um código de status (como 404, 200, 301, etc.) como outra importante referência. Além disso ele retorna headers e diversas outras informações de controle.
Note que esse modelo faz com que as exceções tradicionais em linguagens como C#, Java, sejam descartáveis. Elas de fato não são, portanto é importante saber escolher quando utilizar esse formato para comunicar as excepcionalidades dos sistemas que desenvolve.
public class Result<T>
{
public bool Success { get; }
public T Data { get; }
public string ErrorMessage { get; }
private Result(bool success, T data, string errorMessage)
{
Success = success;
Data = data;
ErrorMessage = errorMessage;
}
public static Result<T> SuccessResult(T data)
{
return new Result<T>(true, data, null);
}
public static Result<T> ErrorResult(string errorMessage)
{
return new Result<T>(false, default, errorMessage);
}
}
public class Calculator
{
public Result<int> Divide(int dividend, int divisor)
{
if (divisor == 0)
{
return Result<int>.ErrorResult("Divisor cannot be zero.");
}
int result = dividend / divisor;
return Result<int>.SuccessResult(result);
}
}
Throwing Exceptions
A forma mais naturais de pensar em tratamento de exceções é utilizando try-catch-throw. Essa estrutura é utilizada em grande parte das linguagens e transmite semântica ao código, coisa que o Result Object não transmite. Além disso essa estrutura funciona implementando o padrão chain of responsability, ou seja, uma exceção é lançada para um contexto superior que pode ser trabalhada e lançada para outro contexto superior, sucessivas vezes. Isso dá a vantagem óbvia de ser possível fazer um rastreamento superior ao que o Result Object é capaz de dar.
Exceções empacotadas ou Exceções explícitas?
As exceções empacotadas através de Result Objects trazem algumas características como:
- Métodos explícitos
- Menor consumo de memória
- Maior flexibilidade
E por outro lado, as características mais marcantes das exceções comuns são:
- Semântica para o código
- Comunicação natural com a IDE ou mesmo com o sistema operacional
- Debugging facilitado
- Menor desempenho
Quando lançar exceções e quando empacotar?
Essa polêmica é boa, mas vamos ser pragmáticos: toda comunicação para fora do assembly base para características infraestruturais devem gerar exceções. Por exemplo, escrita em disco, leitura em banco, permissão para lidar com arquivos, etc. Por outro lado, estruturas de negócio podem utilizar Assertion Concerns em pacotes de Result Objects.
Entretanto é um dever do desenvolvedor/arquiteto de software ponderar quais atributos casam melhor com o momento do software e com as características do negócio em questão. Note que linguagens como Go, por exemplo, não possuem tratamento de exceção nativamente, ou seja, é uma linguagem construídas para o empacotamento de exceções (ou lançamento de booleanos em tuplas, que é o mais usual).
Conclusão de Notification Pattern no DDD
O artigo Notification Pattern no DDD é interessante mas levanta algumas questões importantes sobre o design de código. Não é raro encontrar desenvolvedores defendendo que ele deveria ser utilizado para tudo eliminando o uso de exceptions. Por outro lado, há uma outra corrente que usa exceptions para todas as validações de negócio. Particularmente prefiro um caminho intermediário, utilizando as exceptions que interrompem o fluxo do código quando isso é fundamnetal, caso contrário entidades poderiam ficar não estáveis. Mas também utilizando o notification pattern para validações simples de entidades e quando há a construção de entidades complexas. Bom, o artigo mostrou exemplos e sobrevoou essa polêmica.
Ele atua/atuou como Dev Full Stack C# .NET / Angular / Kubernetes e afins. Ele possui certificações Microsoft MCTS (6x), MCPD em Web, ITIL v3 e CKAD (Kubernetes) . Thiago é apaixonado por tecnologia, entusiasta de TI desde a infância bem como amante de aprendizado contínuo.