Modularização do Software - Contribuições para organização do código¶
Algumas heurísticas que são referências para esta contribuição:
- KISS - O propósito dessa contribuição é a busca por simplicidade, sem ignorar os detalhes e as complexidades do software em desenvolvimento, do contexto de utilização, das condições de educação e trabalho do desenvolvedor.
- DRY - Busca-se a proposição de um desenho funcional e estrutural em que os conceitos e códigos que o realizam estejam coerentemente organizados, ainda que não estejam no mesmo arquivo físico, no mesmo projeto, na mesma linguagem de programação.
- Fonte única da verdade - Está refletida nos propósitos funcionais de cada modelo desta contribuição. Apesar dos inevitáveis mapeamentos de atributos, gravações em caches, representações intermediárias de partes da mesma informação.
- ACID - Observado na proposição de rotinas e modelos que executem seus propósitos ou que lidem com os pontos de falhas conhecidos não deixando para outras partes do software um "erro desconhecido".
Modelo de Objetos¶
Em linguagens de programação orientado a objetos, as classes são as peças fundamentais onde o código é escrito. Classes estáticas são um caso a parte e serão discutidas mais adiante.
Num software orientado a objetos, cada classe deve representar um conceito relevante para o funcionamento do software, seja esta relevância de caráter infraestrutural, acessório, funcional. Os atributos de uma classe e as rotinas nela implementadas devem, no mínimo, realizar funcionalmente o conceito que representam.
Além da funcionalidade propriamente dita, ler e gravar os atributos de uma classe em um armazenamento persistente também deve ser possível e é uma tarefa bastante rotineira.
Do ponto de vista da compilação de código, todas as classes de um software são meramente classes, submetidas aos mesmos critérios de validação e compilação.
Do ponto de vista semântico, entretanto, é comum atribuir diferentes finalidades para classes. Esta categorização é útil para que tarefas de configuração, comunicação, processamento de registros, exibição, exportação, leitura e gravação sejam realizadas.
O design e a gestão de uma classe e de suas instâncias num sistema depende muito das finalidades e papéis que ela desempenhará. Algumas das características ligadas ao papel que uma classe desempenha:
- Criadas e descartadas dinamicamente.
- Imutabilidade
- Persistência dos atributos de cada instância
- Tempo de vida de cada instância
- Modelagem orientada às operações que deve desempenhar
- Modelagem orientada à gravação, leitura e transformação dos dados que representa
Atenção
Os modelos de classes recomendados abaixo são estereótipos!
O compilador .net não verificará se estão sendo utilizados para os propósitos descritos abaixo, apenas se a sintaxe está correta. Verificações de mais alto nível podem ser obtidas através de anotações, linters com regras específicas.
Mesmo na presença de quaisquer desses recursos automatizados, é responsabilidade da equipe de desenvolvimento a checagem de adequação dos estereótipos sugeridos nesse catálogo.
O catálogo abaixo apresenta um conjunto básico de perfis capaz de representar aspectos funcionais e não funcionais necessários para um software.
Entidades e Agregações¶
Entidades descrevem estruturas de dados que tem valor prático para o usuário do software. Os registros que representam devem ser graváveis e recuperáveis a partir de um meio de armazenamento persistente. É usual a utilização de banco de dados para a finalidade de armazenamento.
Entidades e Agregações estão bem discutidas em tópicos da arquitetura DDD. Uma referência adicional de conhecimento e implementação pode ser encontrada na documentação do framework ABP.
Em uma arquitetura orientada a objetos, uma classe do tipo Entidade pode e deve ter operações. A esse respeito:
Operações em uma Entidade¶
- Operações de instância são a regra. Operações estáticas devem ser consideradas com cuidado. Padrões como factory, adapter, visitor, provider são alternativas preferenciais aos métodos estáticos em Entidades.
- Nenhuma operação deve criar outras entidades! Existem padrões criacionais que podem ser considerados para isso.
- O escopo de toda e qualquer operação de instância deve ser: os parâmetros de entrada da operação e os atributos armazenados na instância no momento em que a operação é realizada.
- Construtores devem ser modelados de acordo com as abstrações que a Entidade representa. Evite a adoção de construtores públicos vazios apenas como facilitação para mecanismos de gravação e leitura de instâncias. Padrões como factory e repositórios são alternativas não funcionais que mantêm a consistência do design e asseguram a leitura e gravação.
Atributos em uma Entidade¶
- Deve sempre existir um atributo não anulável para identificação inequívoca da entidade. É a base do padrão Entidade.
- Atributos são variáveis e são representados dentro de uma classe com a mesma notação com a qual se declara uma variável.
- Compilador e IDE C# oferecem um atalho para acesso dos atributos armazenados: Properties. Uma property tem notação bastante parecida com a descrição de um atributo e muitas vezes são confundidas com atributos.
- Properties são o principal atalho utilizado por mecanismos de leitura e gravação de instâncias.
- Evite adotar eventos em uma entidade. Eventos são rotinas, não atributos, embora a assinatura seja parecida com as properties.
- Na qualidade de rotinas, properties podem ser escritas de tal forma que se possa fazer verificações, validações, preparações e rejeições sobre o atributo que refletem.
Operações sobre uma entidade¶
- Instâncias de uma entidade podem ser criadas utilizando a palavra reservada new em qualquer ponto do código para o qual a classe esteja referenciável.
- Sugere-se que a criação de instâncias pode ser delegada a factories, para que aspectos mais sofisticados de criação possam ser resolvidos. Factories são úteis à medida que sistemas se tornam mais complexos, mas é um padrão de adoção adiável.
- A leitura e gravação de instâncias de uma entidade deve ser realizada por outros mecanismos no software. Jamais pela própria entidade.
- Dada a natureza dos atributos de uma instância, auditoria e rastreamento das modificações destes atributos é um tipo de gravação e leitura que deve ser considerada no projeto do software.
- A verificação de permissões sobre operações e atributos de uma instância deve ser resolvida externamente à entidade. Em softwares com grande quantidade de instâncias, esta verificação onera o processamento ordinário, principalmente quando existem dependências de outras entidades (que também precisam ser verificadas).
Considerações adicionais¶
- Entidades são e devem ser utilizadas em operações do tipo CRUD, uma vez que são o ingresso de registros de usuário numa software.
- Entidades são insumos para processamentos mais complexos no software, exatamente porque representam registros de interesse para o usuário.
- Devem estar concentradas em módulos do tipo Domínio. Incluir referência aqui
- Cada instância de uma entidade deve refletir um registro de informação que tenha significado para o usuário.
- Uma entidade deve estar exposta apenas às partes do módulo de domínio onde está localizada. C# facilita esta operação através da palavra reservada internal e através do atributo de compilação InternalsVisibleTo.
- DTO, ETO, ViewObjects, InputObjects são uma maneira bastante econômica de apresentar atributos de uma entidade para outros módulos do software ou de receber pacotes de modificações dos atributos.
- Os valores para os atributos são preenchidos a partir de mapeamento, não a partir de serialização/deserialização (lembre, método não se serializa/deserializa). Logo, deve haver ORM, OOM ou similar.
- Uma entidade não representa todas as facetas de um conceito de negócio. Ao contrário, representa apenas os aspectos com o qual o software foi projetado para lidar. Mesmo assim, em um software, não deve existir duas ou mais entidades que implemente as mesmas operações ou que possua os mesmos atributos. Porém, é parte de uma estruturação modularizada, que aspectos complementares de um conceito de negócio estejam refletidos em mais de uma entidade em um software. (Providenciar exemplos sobre esse tópico. Como um conceito de negócio pode estar adequadamente implementados em diferentes entidades)
Value Objects¶
São classes ou structs que:
- Devem ser tratadas de maneira unitária. Portanto devem ser imutáveis
- A comparação entre instâncias de um mesmo tipo de ValueObject é dado pelo valor dos atributos de cada instância.
- Podem disponibilizar getters sobre um ou mais dos atributos que estão definidos nele, mas não setters.
- Podem conter operações, tanto estáticas quanto de instância. O escopo destas operações deve ser os atributos da própria instância e os parâmetros de assinatura da operação.
Value objects podem fazer parte de Entidades e mesmo refletirem atributos complexos. Pense em um CPF ou um código telefônico. Em geral podem ser armazenados como strings, sem maiores celeumas. No entanto uma alternativa mais estruturada é apresentada no diagrama abaixo:
---
title: Value Objects - CPF e Telefone
---
classDiagram
class Telefone {
<<Value Object>>
- int codigoInternacional
- string codigoDDD
- string codigoNumerico
- bool ehCelular
+ bool EhCelular()
+ string ObterParte(ParteTelefone parte)
+ string Formatar()
+ string ToString()
}
class CPF {
<<Value Object>>
- string valorBruto
- Bool formatado
+ Bool EstaValido()
- Bool Validar(texto)
- string Formatar(Bool usarMascara)
}
class ParteTelefone {
<< Enum >>
+ CodigoDDD
+ CodigoNumerico
+ CodigoInternacional
}
Ambos os value object definidos acima ainda podem ser guardados como strings em banco de dados. Para isso, é necessário um trabalho adicional de conversão usando o mecanismo de gravação e leitura que estiver disponível. Para as rotinas de software esta modelagem oferece mais possibilidades de manuseio de códigos telefônicos e de CPF.
Algumas considerações adicionais sobre ValueObjects
- Recomenda-se fortemente a implementação de ValueObjects utilizando structs. Por design, structs não são herdáveis e essa é uma noção importante para um value object. Operações compartilhadas entre value objects pode ser realizada através de extensions methods. A implementação a partir de classes abstratas também é possível, mas deve ser considerada com cautela. Herança e polimorfismo não fazem muito sentido no design de Value Object para um software.
- Embora não seja obrigatório, recomenda-se a implementação de representação como string e obtenção dos atributos a partir de strings. Considere a documentação e utilize ToString e estratégias de parse tais como TryParse, TryParseExact, Parse.
- A implementação de Equals e GetHashCode é obrigatória. São rotinas obrigatórias para que a igualdade de instâncias seja processada de maneira útil para o seu software. Caso não sejam explicitamente implementadas, a plataforma .net ainda fará uma comparação de instâncias utilizando as próprias implementações de Equals e de GetHashCode, embora os resultados dessa comparação possam não ser adequados ao funcionamento de nosso "software em desenvolvimento".
Exemplos simples são: "feriados", "CPF", "CEP", "CNPJ", "telefone", "UniqueIdentifier", "e-mail", "quantidades físicas", "Código Matricula". Estes casos tem ao menos uma regra de validação, formatos de apresentação e regras de conversão.
View Objects¶
- Devem ser read-only em relação a atributos de negócio
- Devem representar os interesses das apresentações de dados (telas, response API, )
Input Object¶
- Mecanismo conveniente para modelar as entradas de dados permitidas pelo Módulo
- Pode conter rotinas com valor de aplicação.
- Deve ser validável (FluentValidation ou vanilla)
- Evitar redundância de atributos através da normalização (outros InputModels) ou de composição (ValueObjects, Records)
- Apenas validações triviais e in-process: obrigatório/opcional; tamanhoMaximo/tamanhoMinimo; lista[min,max], negativos/positivos; regex.
- Não utilizar validações ou verificações dependentes de recursos out-of-process. O lugar para fazer essas operações é nas entidades e nos domain services.
- Regras de validações implementadas em classes especializadas e state-less (vazão, paralelização, portabilidade )
- Deve conter os atributos necessários e suficiente para uma entrada de dados atômica (que não dependa de outra rotina)
- Refletir todos os atributos de uma ou mais entidades não é boa prática. A adaptação e complementação de uma entrada de dados é de responsabilidade de outras camadas do sistema e pode demandar: settings, external-resources, resultados de outros cálculos e assim por diante.
- Podem representar momentos diferentes da compilação de um sistema. Portanto devem suportar versionamento
Validation Helpers¶
- Neste modelo de objeto mais interessa as rotinas que implementa do que os valores que armazena.
- Devem ser stateless. Ou seja, os atributos armazenados em instâncias de validadores não devem ser misturados aos registros que validam.
- São uma alternativa útil para gerenciar validações complexas de maneira consistente.
ETO - Event Transfer Object (message system/event bus payloads)¶
- Input e Response de mecanismos de mensageria (ServiceBus, MessageQueue)
- São DTO cujo desenho é especialmente orientado para serialização/deserialização rápida. Preferência para atributos de tipo primitivos. Strings apenas curtas. Convém estabelecer um limite máximo de caracteres para strings
- Devem conter quantidade reduzida de atributos (mais atributos, mais esforço serialização)
- Podem ser combinados com InBox e OutBox patterns para resiliência e guarda de payloads maiores
Query Objects¶
São um recurso auxiliar para composição de consultas dinâmicas. Neste modelo de objetos, o principal interesse é a coleta e organização de parâmetros para buscas com um número desconhecido de critérios. São um padrão valoroso para substituir a concatenação de strings quando se precisa fazer consultas desse tipo:
~== UI - Exemplo de query objects ~ ~ Model correspondente gerado ==~
Percebe-se, do exemplo acima, que além do atributo filtrado, também se consegue incluir os operadores de filtragem!
DTO - Data Transfer Object¶
- Devem ser readonly e imutáveis
- Modificáveis apenas por serializadores/deserializadores
- Ao menos um construtor público e sem parâmetros
- Podem conter listas de fácil serialização (dicionários e arrays)
- Podem conter outros objetos
- Desejável evitar ramos e árvores
- Podem representar momentos diferentes da compilação de um sistema. Portanto devem suportar versionamento
- DTO com diferentes números de versão não devem ter dependências entre si.
- Namespace deve ser versionável
Report Model (readonly entities views+joins)¶
- Devem ser readonly
- Podem conter operações in-process para consistência e valores calculados
- Podem conter listas associadas
- Obtidos por mapeamento, não por deserialização
- Podem conter Entidades como atributos
- Obtidos por consultas a um ou mais repositórios de entidades a partir de regras mapeadas em service objects
- São o equivalente de views e joins em modelos relacionais
- Internos aos módulos em que estão definidos
- Atributos podem ser tanto entidades quanto DTO
Config Object¶
- Input models que preferencialmente não mudam ao longo do tempo de execução do módulo
- Atributos e operações in-process apenas
- Dependências apenas de outros Config Objects, library objects, ou de tipos primitivos
Processamento de dados e tratamento padronizado de erros¶
Erros acontecem! Organizar uma estrutura para tratar erros num software é tão importante quanto o esforço dedicado em outras disciplinas de um software. Em boa parte dos casos de falhas, é desejável que a falha seja tratada, o resultado seja adequadamente comunicado (API, tela de usuário, arquivo de saída, etc.) e que o software continue em operação normalmente.
Result pattern¶
Exceptions custam caro para o funcionamento do software! A plataforma .net lida bem com exceptions e as utiliza em abundância. Existem bons argumentos que propõe um uso apenas "excepcional" de exceptions. Por brevidade destacamos três dos principais argumentos:
-
As rotinas de processamento de informações num software devem ser capazes de lidar com falhas e fornecer informações relevantes quando falhas ocorrem. É para isso que se testa software! Exceptions devem ser ... exceções, não a regra!
-
Exigem a criação de Exception, que embora seja um objeto como qualquer outro na plataforma .net, representa um custo elevado quando o stackTrace é acessado. Acessar o stackTrace é muito comum quando se utiliza logs1.
-
Diferente da plataforma java, o .net não obriga o desenvolvedor a declarar as exceptions na assinatura da rotina nem tampouco tratá-las durante a chamada da rotina que pode gerar exceptions.
A implementação do pattern Result torna o resultado de cada rotina previsível, seja no resultado esperado, seja em caso de falhas conhecidas pelo desenvolvedor durante a construção do código. Por outro lado a sua implementação deve ser planejada desde o início da construção do código, para que a utilização do pattern Result seja compreensível, para que decisões de design sejam tomadas, realizar adequado de encadeamentos de chamadas e respostas entre rotinas e para evitar códigos ilegíveis ou simplesmente confusos:
class Foo_Tentativa01 {
async Task<Result<String>> Validate(string requestData){
/* ... code */
return Result.Success("OK");
}
private async Task<Result<HttpRequestMessage>> CreateRequest(Result<string> requestData){
if (!validRequestData.IsSuccess)
return Result.Fail<HttpRequestMessage>("invalid");
var usefulResult = new HttpRequestMessage();
/* ... code */
return Result.Success(usefulResult);
}
private async Task<Result<HttpResponse>> ExecuteInternal(Result<HttpRequestMessage> request){
if (!requestObj.IsSuccess)
return Result.Fail<Response>("invalid Request object");
/* ... code */
var usefulResult = new Response();
return Result.Success(usefulResult);
}
public async Task<Result<Response>> Execute_Verbose(String requestData){
/* ... code */
var validRequestData = await Validate(requestData);
var requestObj = CreateRequest_Verbose(validRequestData);
await ExecuteInternal_Verbose(requestObj.Value);
}
}
Em favor de uma abordagem mais legível em que o roteiro processual que valida um parâmetro, constrói e processa a request esteja concentrado em um único lugar. Reserva-se a utilização de result apenas para a resposta das rotinas incumbidas de fazer validação, criação e processamento de requisições. O resultado das modificações é apresentado a seguir:
class Foo_Compact {
async Task<Result<String>> Validate(string requestData){
/* ... code */
return Result.Success("OK");
}
private async Task<Result<HttpRequestMessage>> CreateRequest(string requestData){
var usefulResult = new HttpRequestMessage();
/* ... code */
return Result.Success(usefulResult);
}
private async Task<Result<HttpResponse>> ExecuteInternal(HttpRequestMessage request){
/* ... code */
var usefulResult = new Response();
return Result.Success(usefulResult);
}
public async Task<Result<Response>> Execute_Compact(String requestData){
/* ... code */
var validRequestData = await Validate(requestData);
if (!validRequestData.IsSuccess)
return Result.Fail<Response>("invalid");
var requestObj = CreateRequest(validRequestData);
if (!requestObj.IsSuccess)
return Result.Fail<Response>("invalid Request object");
await ExecuteInternal(requestObj);
}
/* Result signifca */
}
Referências:
- Gerenciamento de exceptions utilizando Result Pattern. Link: https://www.milanjovanovic.tech/blog/functional-error-handling-in-dotnet-with-the-result-pattern
Modelo de dados para erros¶
Uma estrutura mínima e eficaz para tratamento de erros é fundamental para interceptar Descrever uma estrutura Trata-se de uma descrição minimamente estrutura para falhas Deve ter como atributos, pelo menos:
- Código único para caracterizar o erro. Um número inteiro ou uma string de até 50 caracteres alfanuméricos, sem espaços, tabulação ou acentuação.
- Mensagem descritiva de linha única limitado até 500 caracteres.
---
title: Ilustração estrutura de erros
---
classDiagram
ErroBase "1" --> "0.*" ChaveValor
class ErroBase{
+String Codigo
+String MensagemFinal
+ChaveValor InfoAdicional
+swim()
+quack()
}
class ChaveValor{
+ String Codigo
+ object Valor
}
Catálogo padronizado de Erros¶
Uma classe estática ou não herdável que agrupa códigos de erros e modelo de mensagens de falhas. Em princípio, os erros registrados são um catálogo definido em tempo de compilação, com itens identificados e considerados relevantes pelo desenvolvimento. Cada item identificado nesse catálogo deve identificar um e apenas um tipo de erro, evitando assim que vários itens no catálogo descrevam a mesma condição de erro. Informações adicionais num mesmo tipo de erro podem ser acrescentadas como um objeto adicional ou mesmo como uma string.
Algumas das razões para adotar um Catálogo de Erros:
- Agrupamento dos erros em categorias significativas.
- Simplifica a identificação e solução de falhas.
- A utilização de "erros nomeados" é uma maneira incrementar a legibilidade e documentação do código-fonte.
- Auxílio na sustentação do software.
Também na construção de Catálogo de Erros pode-se adotar algumas boas práticas:
- Um Catálogo único pode ser o suficiente para um software composto de algumas poucas funcionalidades.
- Determine quais famílias de erros serão descritas em um catálogo. Isso ajudará a manter o propósito do catálogo inteligível.
- Evite classes com mais de 500 linhas. Essa regra vale também para os catálogos. Muitos itens de erros podem ser uma oportunidade de revisão e melhor agrupamento em catálogos menores.
Na programação com .net, de acordo com os objetivos de cada projeto, pode-se adotar um catálogo por projetos, ou manter um catálogo que sirva para vários projetos. A visibilidade do catálogo de erros pode ser controlada através do atributo InternalsVisibleTo, de tal maneira que se possa ter catálogos diferentes.
Uma implementação de um catálogo trivial de erros pode ser vista no seguinte exemplo:
public sealed class CatalogoErros
{
#region Erros de validação
public static FamiliaErros FalhasValidacao { get; } = new FamiliaErros();
public static ErroValidacao NomeProprioInvalido = new ErroValidacao(FalhasValidacao);
public static ErroValidacao IdadeNaoDeveSerMaiorQue18Anos = new ErroValidacao(FalhasValidacao);
#endregion
#region Erros de gravação
public static FamiliaErros FalhasGravacao { get; } = new FamiliaErros();
public static ErroGravacao NenhumRegistroValidoParaGravar = new ErroGravacao(FalhasGravacao);
public static ErroGravacao FalhaDuranteRollbackTransacao = new ErroGravacao(FalhasGravacao);
#endregion
#region Erros de validação - Usando rotinas para gerar erros com valores específicos
public static ErroValidacao CadaCpfDeveSerUnico(string cpf, int codUsuario)
{
return new ErroValidacao(FalhasValidacao)
.ComMensagemFinal("Já existe um CPF idêntico!")
.ComInformacaoAdicional(new { cpf, codUsuario });
}
#endregion
}
/* Em que família de erros e os erros propriamente ditos podem ser */
public sealed class FamiliaErros
{
private Dictionary<string, string> _membros = new Dictionary<string, string>();
public string CodigoBase { get; set; }
public string DescricaoBase { get; set; }
}
public abstract class ErroBasicoAplicacao
{
public FamiliaErros Familia { get; }
public string CodigoBase { get; protected set; }
public string MensagemFinal { get; protected set; }
public IReadOnlyDictionary<string, object> InfoAdicional { get; protected set; }
protected ErroBasicoAplicacao(string codigoBase)
{
this.CodigoBase = codigoBase;
Familia = null;
}
protected ErroBasicoAplicacao(string codigoBase, FamiliaErros familia)
{
this.CodigoBase = codigoBase;
Familia = familia;
}
}
public sealed class ErroValidacao : ErroBasicoAplicacao
{
public ErroValidacao(FamiliaErros familia, string sufixo = null)
: base(typeof(ErroValidacao).Name, familia) { }
public ErroValidacao ComMensagemFinal(string mensagem)
{
return new ErroValidacao(this.Familia)
{
MensagemFinal = mensagem,
InfoAdicional = this.InfoAdicional
};
}
public ErroValidacao ComInformacaoAdicional(object infoAdicional)
{
var result = new ErroValidacao(this.Familia)
{
MensagemFinal = this.MensagemFinal,
InfoAdicional = GerarDicionario(infoAdicional)
};
return result;
}
private static IReadOnlyDictionary<string, object> GerarDicionario(object amostra)
{
return Array
.Empty<KeyValuePair<string, object>>()
.ToImmutableDictionary();
}
}
public sealed class ErroGravacao: ErroBasicoAplicacao
{
public ErroGravacao(FamiliaErros familia, string sufixo = null)
: base(typeof(ErroGravacao).Name, familia) { }
}
Exceptions amigáveis¶
O tratamento estruturado de exceptions exige, além de modelo de classe adequado, configurabilidade, decisões sobre gerenciamento ou exposição, registro em logs, serialização, dentre outros aspectos.
O modelo abr
Factory de Exception¶
ConfigObjects¶
Validation¶
Services¶
Services genéricos¶
Repository¶
-
Uma discussão mais extensa sobre o custo do stack trace pode ser encontrada no artigo The Exceptional Performance of Lil' Exception ↩