Segundo Martin Fowler, Notificação é um objeto que coleta dados sobre erros e outras informações na camada de domínio e que comunica com as outras camadas, principalmente com a camada de apresentação.
O padrão Notification, ou notificação, é uma solução elegante para tratar erros de sistema desnecessários, pois exceção, além de causar uma parada abrupta na execução de todo o programa, tem um custo muito alto para o processador. Por isso, usando uma lista de notificações é possível coletar os problemas previsíveis, listar, classificar e informar às outras camadas, ou até mesmo ao usuário, de uma só vez sobre todos os problemas que ele precisa corrigir antes de continuar.
A testabilidade do código também aumenta, pois é possível definir erros, alertas e mensagens padrões para determinadas situações sem lançar exceções, ou seja, agiliza o teste e fica muito mais claro o que o sistema espera de entrada. Ainda é possível combinar esta abordagem com outros padrões, como Specification e Design By Contract e criar um tratamento poderoso e profissional de validações.
O Artigo de Martin Fowler
O artigo original sobre Notification foi escrito em 2004, e faz parte do livro Patterns of Enterprise Application Architecture de 2003, e nele, Fowler até demonstra como usar o padrão usando C# em uma aplicação windows, mas para que fosse mais fácil entender hoje, eu atualizei a sintaxe usando C# 7.0 e desenvolvi alguns métodos que ele não incluiu no artigo, portanto, se você quiser acessar é só entrar no meu repositório Notification by Martin Fowler do GitHub e conferir a abordagem descrita por ele em uma aplicação console.
O Flunt e o Design By Contract de André Baltieri
André Baltieri, MVP da Microsoft, desenvolveu um pacote Nugget, chamado originalmente de Fluent Validator, no qual tive o prazer de contribuir, e que mais tarde foi rebatizado de Flunt. O Flunt é um sistema poderoso e extensível de validação e notificações em domínios, reduzindo “Ifs” e Testes, contribuindo na otimização do tempo, podendo assim focar na codificação e na regra de negócio do seu domínio do projeto.
Para conhecer os detalhes deste pacote fenomenal e seu código fonte, é só ler o artigo Design By Contracts e também assistir os vídeos do canal dele. Posso garantir que será de grande proveito!
O Meu Projeto de Domain Notification
É possível usar as notificações de um modo simples, apenas para atender um necessidade específica, criando apenas uma lista de mensagens, mas este padrão é poderoso o suficiente para que seja usado em domínios complexos, e combinado com outras abordagens. O projeto que desenvolvi tem um nível de complexidade médio, por este motivo já indiquei o Flunt, pois é uma forma de usar o conceito para validações com o mínimo de esforço e máxima confiabilidade, e assim já terá grandes ganhos, como a redução do uso de exceções e Ifs. Mas se você é como eu, que gostaria de usar ao máximo o padrão e ter controle sobre todo o seu funcionamento, vai ver como desenvolver e usar neste exemplo prático.
Domain Driven Design – DDD
Quase todos os meus projetos são desenvolvidos usando a abordagem de arquitetura DDD, ou seja, quase sempre terá um projeto focado no domínio, assim como o uso de objetos de valor e pelo menos as camadas de domínio, aplicação e apresentação. Se você não faz ideia do que estou falando, não há problemas, é possível entender este artigo mesmo assim, mas se quiser saber mais sobre desenvolvimento dirigido ao domínio, segue a indicação de um excelente vídeo do Eduardo Pires, MVP da Microsoft.
A solução
Abra o Visual Studio, crie uma solução vazia chamada DomainNotification, inclua três Solutions folder chamadas Domain, Application e Presentation.
Na pasta Domain inclua um projeto do tipo Class Library chamado DomainNotification.Domain, na pasta Application inclua um projeto do tipo Class Library chamado DomainNotification.Application e finalmente na pasta Presentation inclua um projeto do tipo Console application chamado DomainNotification.Prompt e defina como Set as StartUp project. A estrutura deve parecer como a imagem a seguir.
Projeto de Domínio
O projeto de domínio será onde residirão as entidades principais, assim como suas classes bases abstratas, também conhecidas como camada de Supertypes. Mas vamos começar criando as estruturas e classes de notificação, que estarão nesta camada.
No projeto de domínio, inclua uma pasta chamada Interfaces, e dentro desta pasta, outra chamada Notifications. Em Notifications, inclua uma interface chamada IDescription e INotification.
1 2 3 4 5 6 7 8 9 |
namespace DomainNotification.Domain.Interfaces.Notifications { public interface IDescription { string Message { get; } string ToString(); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
using System.Collections.Generic; using DomainNotification.Domain.Notifications; namespace DomainNotification.Domain.Interfaces.Notifications { public interface INotification { IList<object> List { get; } bool HasNotifications { get; } bool Includes(Description error); void Add(Description error); } } |
A interface INotification tem como objetivo definir um contrato para criação de notificações, definindo o mínimo que um classe precisa ter, ela facilita também o uso de injeção de dependência, podendo ajudar na redução de acoplamento entre projetos.
IDescription define um contrato mínimo para uma descrição de uma notificação lançada, assim como sobrescreve o método ToString() para que possa ser usada como um texto simples.
Ainda no projeto de domínio, inclua uma pasta chamada Notifications, e dentro desta pasta, inclua duas classes abstratas, uma chamada Description e outra chamada Notification.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
using DomainNotification.Domain.Interfaces.Notifications; namespace DomainNotification.Domain.Notifications { public abstract class Description : IDescription { public string Message { get; } protected Description(string message, params string[] args) { Message = message; for (var i = 0; i < args.Length; i++) { Message = Message.Replace("{" + i + "}", args[i]); } } public override string ToString() => Message; } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
using System.Collections.Generic; using System.Linq; using DomainNotification.Domain.Interfaces.Notifications; namespace DomainNotification.Domain.Notifications { public abstract class Notification : INotification { public IList<object> List { get; } = new List<object>(); public bool HasNotifications => List.Any(); public bool Includes(Description error) { return List.Contains(error); } public void Add(Description description) { List.Add(description); } } } |
A classe Description é abstrata, e tem como objetivo ser usada como herança para a classe que será a descrição da notificação em si, assim como a classe Notification que será usada como Supertype para a classe de notificação.
Estas quatro classes sustentam todo o core das notificações, fornecendo o mínimo para ser usado em uma entidade de domínio, por exemplo. mas também permite uma extensão do recurso, podendo adicionar outras peculiaridades necessárias.
Erros como notificações
Um dos principais usos de notificações é para tratamento de erros e validações de dados, e para isso, é economizado então uma parada e o lançamento desnecessário de uma exceção do Framework. A proposta aqui é criar um exemplo que usa uma interface e algumas classes básicas: ILevel, Error, ErrorDescription e classes para a severidade do erro.
A classe Error herda da classe abstrata Notification, mas estende a possibilidade de listar erros com descrição e nível, assim como verificar erros somente de um nível de severidade ou simplesmente checar se há erros de um tipo específico ou de modo geral.
No projeto de domínio, na pasta de Interfaces crie uma pasta chamada Errors e dentro dela inclua a interface a seguir:
1 2 3 4 5 6 7 8 9 |
namespace DomainNotification.Domain.Interfaces.Errors { public interface ILevel { string Description { get; } string ToString(); } } |
No projeto de domínio, crie uma pasta chamada Errors e dentro dela inclua as três classes a seguir de nível de severidade:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
using DomainNotification.Domain.Interfaces.Errors; namespace DomainNotification.Domain.Errors { public class Critical : ILevel { public Critical(string description = "Critical") { Description = description; } public string Description { get; } public override string ToString() { return Description; } } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
using DomainNotification.Domain.Interfaces.Errors; namespace DomainNotification.Domain.Errors { public class Warning : ILevel { public Warning(string description = "Warning") { Description = description; } public string Description { get; } public override string ToString() { return Description; } } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
using DomainNotification.Domain.Interfaces.Errors; namespace DomainNotification.Domain.Errors { public class Information : ILevel { public Information(string description = "Information") { Description = description; } public string Description { get; } public override string ToString() { return Description; } } } |
A classe ErrorDescription recebe as informações básicas de uma notificação e ainda um objeto do tipo ILevel com a severidade do erro para incluir na classe Error.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
using DomainNotification.Domain.Interfaces.Errors; using DomainNotification.Domain.Notifications; namespace DomainNotification.Domain.Errors { public class ErrorDescription : Description { public ILevel Level { get; } public ErrorDescription(string message, ILevel level, params string[] args) : base(message, args) { Level = level; } } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
using System.Collections.Generic; using System.Linq; using DomainNotification.Domain.Notifications; namespace DomainNotification.Domain.Errors { public class Error : Notification { public IList<ErrorDescription> Errors => List.Cast<ErrorDescription>().Where(x => x.Level is Critical).ToList(); public IList<ErrorDescription> Warnings => List.Cast<ErrorDescription>().Where(x => x.Level is Warning).ToList(); public IList<ErrorDescription> Informations => List.Cast<ErrorDescription>().Where(x => x.Level is Information).ToList(); public bool HasErrors => List.Cast<ErrorDescription>().Any(x => x.Level is Critical); public bool HasWarnings => List.Cast<ErrorDescription>().Any(x => x.Level is Warning); public bool HasInformations => List.Cast<ErrorDescription>().Any(x => x.Level is Information); } } |
Com estas classes, personalizamos a notificação para uso com exceções, no qual agora poderá ser usada por outra classe que queira listar e comunicar entre camadas este tipo de informação.
Validando uma entidade
Já temos a estrutura básica para que uma entidade de domínio possa usar este recurso, e ainda ter a possibilidade de verificar se ela é válida ou não para ser usada em outra lógica e/ou salvar no banco de dados. Conseguimos implementar adicionando uma propriedade e dois métodos.
A propriedade Errors é do tipo Error, que é uma instância de notificação, será aqui que as validações serão armazenadas e consultas.
Para permitir que as validações possam ser globais, específicas ou opcionais, criamos um método chamado Validate, que só pode ser disparado na entidade, ou seja, se alguma validação que a maioria está usando não fizer sentido para o objeto pode ser ignorada ou se houver outras validações que são tão específicas para aquela entidade, ela pode ser criada e chamada na entidade.
E para fechar as características mínimas para uma classe poder usar as notificações de erros, temos uma função chamada IsValid, que apenas verifica se há ao menos um erro de nível Critical. Erros dos níveis Warning e Information não tornam a classe inválida neste exemplo.
Porém, para não ficar validando coisas que são comuns ou que a maioria das entidades precisam validar, é possível armazenar estas verificações direto na classe base abstrata do domínio, e para isso criei uma região na classe chamada Validations, onde foi colocada todas as funções que são globais ou bastante usadas. Mesmo assim é preciso chamar na classe principal, pois elas são opcionais, mas estão prontas para usar. Para exemplificar o cenário, escrevi uma função para validar Guid e outra para Nomes, assim como duas descrições de erros, em uma região chamada Errors, com suas devida mensagens e níveis de severidade.
No projeto de domínio, crie uma pasta chamada ValueObjects, e dentro dela inclua as classes a seguir:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
using System.Text.RegularExpressions; using DomainNotification.Domain.Errors; namespace DomainNotification.Domain.ValueObjects { public class ValueObject { public Error Notification { get; } = new Error(); public virtual void Validate() { } protected void Fail(bool condition, ErrorDescription error) { if (condition) Notification.Add(error); } public bool IsValid() { return !Notification.HasErrors; } #region Validations protected void IsInvalidEmail(string s, ErrorDescription error) { const string pattern = @"^\w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$"; Fail(!Regex.IsMatch(s, pattern), error); } #endregion #region Errors public static ErrorDescription InvalidEmail = new ErrorDescription("Invalid E-mail address", new Critical()); #endregion } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
namespace DomainNotification.Domain.ValueObjects { public class Email : ValueObject { public Email(string address) { Address = address; Validate(); } public sealed override void Validate() { IsInvalidEmail(Address, InvalidEmail); } public string Address { get; } } } |
Crie outra pasta na raiz do projeto chamada Entities, e dentro dela inclua as classes a seguir:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
using System; using DomainNotification.Domain.Errors; namespace DomainNotification.Domain.Entities { public class Entity { public Error Errors { get; } = new Error(); public virtual void Validate() { } protected void Fail(bool condition, ErrorDescription description) { if (condition) Errors.Add(description); } public bool IsValid() { return !Errors.HasErrors; } #region Validations protected void IsInvalidGuid(Guid guid, ErrorDescription error) { Fail(guid == Guid.Empty, error); } protected void IsInvalidName(string s, ErrorDescription error) { Fail(string.IsNullOrWhiteSpace(s), error); } #endregion #region Errors public static ErrorDescription InvalidId = new ErrorDescription("Invalid Id", new Critical()); public static ErrorDescription InvalidName = new ErrorDescription("Invalid Name", new Critical()); #endregion } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
using System; using DomainNotification.Domain.Errors; using DomainNotification.Domain.ValueObjects; namespace DomainNotification.Domain.Entities { public class Person : Entity { public Person(Guid personId, string name, Email email) { PersonId = personId; Name = name; Email = email; Validate(); } public sealed override void Validate() { IsInvalidGuid(PersonId, InvalidId); IsInvalidName(Name, InvalidName); IsInvalidEmail(Email, InvalidPersonEmail); } protected void IsInvalidEmail(Email s, ErrorDescription error) { Fail(Email.Notification.HasErrors, error); } public Guid PersonId { get; } public string Name { get; } public Email Email { get; } #region Errors public static ErrorDescription InvalidPersonEmail = new ErrorDescription("Invalid E-mail, see object notifications for more details.", new Critical()); #endregion } } |
Comandos, Aplicação e Console
Para testar as notificações, vamos criar os comandos, uma camada de serviço de aplicação e um aplicativo console para simular uma entrada de dados e visualizar os retornos. A montagem desta parte do exemplo não é foco do artigo, se quiser saber os detalhes destes padrões, sugiro que busque informações sobre Commands, Application Layer e Presentation Layer.
Comandos
Na raiz do projeto de domínio, crie uma pasta chamada Commands, e nela inclua uma classe abstrata chamada Command e outra concreta chamada SavePerson, como a seguir:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
using DomainNotification.Domain.Entities; using DomainNotification.Domain.Errors; namespace DomainNotification.Domain.Commands { public abstract class Command { protected Command(Entity entity) { Entity = entity; } protected Entity Entity; protected Error Errors => Entity.Errors; } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
using DomainNotification.Domain.Entities; using DomainNotification.Domain.Errors; namespace DomainNotification.Domain.Commands { public class SavePerson : Command { private readonly Person _person; public SavePerson(Person person) : base(person) { _person = person; var description = new ErrorDescription("New person create on memory.", new Warning()); _person.Errors.Add(description); } public void Run() { if (!Errors.HasErrors) { SavePersonInBackendSystems(); } else { var error = new ErrorDescription("Registration not saved.", new Critical()); _person.Errors.Add(error); } } private void SavePersonInBackendSystems() { var message = new ErrorDescription("Registration succeeded.", new Information()); _person.Errors.Add(message); } } } |
Com os comandos, a camada de domínio está concluída, agora precisamos criar uma camada de aplicação, que será consumida pela camada de apresentação.
Aplicação
A camada aplicação, conhecida como Service Aplication ou Application Layer, coordena a chamada das camadas de apresentação com o domínio e repositórios. Para criar esta camada, no projeto DomainNotification.Application crie uma pasta chamadas Services, e dentro dela inclua uma classe abstrata chamada Service e outra concreta chamada PersonService, como seguem:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
using System.Collections; using DomainNotification.Domain.Entities; namespace DomainNotification.Application.Services { public abstract class Service { protected Entity NotificationEntity; public bool HasNotifications => NotificationEntity != null && NotificationEntity.Errors.HasNotifications; public bool HasErrors => NotificationEntity != null && NotificationEntity.Errors.HasErrors; public bool HasWarnings => NotificationEntity != null && NotificationEntity.Errors.HasWarnings; public bool HasInformations => NotificationEntity != null && NotificationEntity.Errors.HasInformations; public IEnumerable Errors() { return NotificationEntity?.Errors.Errors; } public IEnumerable Warnings() { return NotificationEntity?.Errors.Warnings; } public IEnumerable Informations() { return NotificationEntity?.Errors.Informations; } } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
using System; using DomainNotification.Domain.Commands; using DomainNotification.Domain.Entities; using DomainNotification.Domain.ValueObjects; namespace DomainNotification.Application.Services { public class PersonService : Service { private readonly Person _entity; public PersonService(Guid personId, string name, string email) { _entity = new Person(personId, name, new Email(email)); NotificationEntity = _entity; } public void SavePerson(Guid personId, string name) { var cmd = new SavePerson(_entity); cmd.Run(); } } } |
Apresentação
A camada de apresentação poderia ser uma página Web, um App Mobile, um Windows Forms, mas para simplificar o nosso exemplo, vamos usar uma Aplicação Console. Esta aplicação vai receber dois campos que será o Nome e o E-Mail, no qual esperamos que o domínio valide e retorne as notificações. As informações serão transportadas através de DTOs (Data Transfer Objects), que podem o não representar uma entidade de domínio. No nosso caso será a representação da classe Person.
No projeto DomainNotification.Prompt crie uma pasta chamada Dto, e dentro dela inclua uma classe chamada PersonDto, como segue:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
using System; namespace DomainNotification.Prompt.Dto { public class PersonDto { public PersonDto(Guid personId, string name, string email) { PersonId = personId; Name = name; Email = email; } public Guid PersonId { get; } public string Name { get; } public string Email { get; } } } |
E por fim, altere a classe Program para que consuma a camada de aplicação:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 |
using System; using DomainNotification.Application.Services; using DomainNotification.Prompt.Dto; namespace DomainNotification.Prompt { internal class Program { private static void Main() { Console.WriteLine("Type your name"); var name = Console.ReadLine(); Console.WriteLine("Type your E-mail"); var email = Console.ReadLine(); var personDto = new PersonDto(Guid.NewGuid(), name, email); var personService = new PersonService(personDto.PersonId, personDto.Name, personDto.Email); Submit(personService, personDto); Console.ReadKey(); } public static void Submit(PersonService personService, PersonDto personDto) { personService.SavePerson(personDto.PersonId, personDto.Name); if (personService.HasNotifications) ShowNotifications(personService); } private static void ShowNotifications(Service personService) { if (!personService.HasNotifications) return; if (personService.HasErrors) { Console.ForegroundColor = ConsoleColor.Red; Console.WriteLine("\nErrors\n"); foreach (var error in personService.Errors()) { Console.WriteLine(error.ToString()); } } if (personService.HasWarnings) { Console.ForegroundColor = ConsoleColor.Yellow; Console.WriteLine("\nWarnings\n"); foreach (var error in personService.Warnings()) { Console.WriteLine(error.ToString()); } } if (personService.HasInformations) { Console.ForegroundColor = ConsoleColor.Blue; Console.WriteLine("\nInformations\n"); foreach (var error in personService.Informations()) { Console.WriteLine(error.ToString()); } } } } } |
A aplicação está concluída, agora podemos testar, como por exemplo, não preencher nenhum campo.
Se preencher o nome corretamente, mas preencher o e-mail de forma incorreta, uma notificação será exibida, sem lançar exceção, informando o problema.
Assim como um notificação de sucesso quando os dados são inseridos corretamente.
Conclusão
Domain Notification ou simplesmente Notification é uma padrão de arquitetura que economiza processamento e torna a aplicação mais clara e versátil, podendo trabalhar com outros padrões e tipos de validação.
Acesse o projeto completo no GitHub: