Engenharia de Software • Módulo 9

Engenharia de Sistemas Corporativos

Capítulo 6: Ontologia Orientada a Objetos, Padrões Arquiteturais e o Ecossistema Blazor (.NET)

Introdução: A Crise da Complexidade

O desenvolvimento de software corporativo (como sistemas ERP, CRM ou plataformas bancárias) diverge dramaticamente da construção de pequenos scripts ou aplicativos de protótipo. Quando um sistema ultrapassa a marca de 50.000 linhas de código, dezenas de tabelas de banco de dados e múltiplos desenvolvedores trabalhando simultaneamente, o inimigo principal deixa de ser a lógica computacional e passa a ser a entropia de software.

Entropia, na termodinâmica, é a medida da desordem de um sistema. No software, ela se manifesta como código espaguete, dependências circulares e a infame "lógica de negócio espalhada pelas telas". Mudar uma regra de cálculo de imposto exige alterar cinco arquivos diferentes, e atualizar a versão de uma biblioteca quebra partes não relacionadas do sistema.

Para combater essa complexidade, a Engenharia de Software desenvolveu padrões arquiteturais rigorosos. Neste capítulo, faremos um mergulho profundo (deep dive) em como utilizamos a Programação Orientada a Objetos (POO) — não como um mero exercício acadêmico de abstração do mundo real — mas como a principal ferramenta para construir Arquiteturas em Camadas escaláveis, seguras e testáveis, utilizando o ecossistema .NET e o framework Blazor.

1. Ontologia Orientada a Objetos no C# Moderno

A Orientação a Objetos é, fundamentalmente, uma forma de organizar o pensamento e agrupar dados (estado) e as funções que operam sobre esses dados (comportamento) em unidades coesas. No entanto, no desenvolvimento enterprise, a forma como pensamos sobre "Classes" muda.

1.1. Classes vs. Objetos: A Batalha na Memória (Stack e Heap)

Uma Classe é uma estrutura estática, um contrato, uma planta-baixa. Ela não ocupa espaço significativo na memória durante a execução do programa (além de sua definição de tipo).

Um Objeto (Instância) é a materialização dessa classe. Quando você usa a palavra-chave new (ou quando o framework faz isso por você), o .NET runtime aloca espaço na memória RAM para guardar o estado específico daquela instância.

💡 Stack vs. Heap no C#

No C#, tipos de valor (como int, bool, structs) são alocados na Stack, uma memória rápida e sequencial. Já as instâncias de Classes (tipos de referência) são alocadas na Heap. O que fica na Stack é apenas um "ponteiro" (endereço de memória) apontando para o objeto real na Heap. O Garbage Collector (GC) do .NET é o responsável por varrer a Heap e limpar objetos que não têm mais ponteiros apontando para eles.

1.2. O Modelo de Domínio Anêmico vs. Rico

Em sistemas de informação, as classes que representam os conceitos centrais do negócio são chamadas de Entidades de Domínio (Domain Entities). Um anti-padrão comum é o Modelo de Domínio Anêmico, onde as classes são apenas sacos de dados (propriedades com get e set públicos) sem nenhuma lógica.

// Anti-padrão: Domínio Anêmico
public class PedidoAnemico {
    public int Id { get; set; }
    public decimal ValorTotal { get; set; }
    public string Status { get; set; } // Alguém pode alterar para "StatusInvalido"
}

A verdadeira Engenharia de Software prega o Domínio Rico (fortemente associado ao Domain-Driven Design - DDD). As propriedades devem ser protegidas, e o estado da classe só deve ser alterado através de métodos que garantam a consistência das regras de negócio (Encapsulamento).

// Padrão correto: Domínio Rico
public class Pedido {
    public int Id { get; private set; }
    public decimal ValorTotal { get; private set; }
    public StatusPedido Status { get; private set; }

    public void AdicionarItem(Item item) {
        if (Status != StatusPedido.Aberto) throw new Exception("Pedido fechado.");
        ValorTotal += item.Preco;
    }
}

2. Injeção de Dependência (DI) e o Controle de Instâncias

Se as Entidades representam os dados, quem executa as operações pesadas, como buscar no banco de dados ou chamar APIs externas? São as Classes de Serviço (Services).

2.1. A Inversão de Controle (IoC)

Em um código amador, se um componente precisa de um serviço, ele o instancia diretamente: var servico = new ProdutoService();.

Isso cria um acoplamento forte (Tight Coupling). O componente fica amarrado àquela implementação exata. Para resolver isso, usamos o princípio da Inversão de Dependência (Letra 'D' do SOLID): classes de alto nível não devem depender de classes de baixo nível. Ambas devem depender de abstrações (Interfaces).

Em vez de usar new, nós declaramos no construtor (ou via @inject no Razor) que precisamos de uma instância que cumpra um contrato (Interface). O framework .NET se encarrega de criar essa instância e entregá-la pronta. Isso é a Injeção de Dependência.

2.2. Ciclos de Vida: Transient, Scoped e Singleton no Blazor

Quando o .NET injeta uma instância, ele precisa saber quanto tempo essa instância deve viver na memória. Configurar isso incorretamente é a principal causa de vazamento de memória (memory leaks) e bugs de estado cruzado entre usuários.

Tempo de Vida Comportamento no ASP.NET / Blazor Quando Usar
Transient Sempre cria uma nova instância toda vez que for injetado. Serviços muito leves, sem estado (stateless), que apenas processam dados de entrada e saída.
Scoped Cria uma instância por "Escopo".
No Blazor Server: Um escopo é igual a um usuário conectado (Circuito SignalR). A instância vive enquanto a aba estiver aberta.
No Blazor WebAssembly: Funciona exatamente igual ao Singleton (apenas um escopo no navegador).
A grande maioria dos serviços em Blazor, DbContext do Entity Framework, serviços que guardam estado do carrinho de compras do usuário.
Singleton Cria apenas uma instância para toda a aplicação. Todos os usuários no servidor compartilham a mesma instância na memória. Cache global da aplicação, serviços de configuração carregados na inicialização, clients HTTP estáticos. Perigo: Nunca use Singleton para guardar dados de usuário logado no servidor!
⚠️ O Perigo do Singleton no Blazor Server

Se você configurar um CarrinhoDeComprasService como Singleton no Blazor Server, quando o Usuário A adicionar um "Notebook" ao carrinho, o Usuário B, que está em outra casa acessando o sistema, verá o "Notebook" no carrinho dele também! Ambos estão compartilhando o mesmo objeto na memória do servidor. Para estado do usuário, use sempre Scoped.


3. O Paradigma MVC e a Web Baseada em Componentes

3.1. A Morte do Controller Tradicional

O padrão Model-View-Controller (MVC) dominou o desenvolvimento web por quase duas décadas (ex: ASP.NET MVC, Spring MVC, Django). O fluxo era: O usuário clica em um link, a requisição HTTP vai para um Controller central no servidor, o Controller busca o Model no banco, mistura com um template HTML (a View) e devolve a página inteira renderizada.

A Web moderna exige reatividade, sem recarregar a página inteira (Single Page Applications - SPAs). Frameworks como React, Angular e Blazor aboliram o Controller global em favor de uma arquitetura baseada em Componentes (muitas vezes adotando o padrão MVVM - Model-View-ViewModel).

3.2. Separação de Responsabilidades no Razor

No Blazor, a View e a Lógica de Controle estão contidas no mesmo "Componente". Um arquivo .razor é compilado em uma Classe C#. Para manter a sanidade arquitetural (Princípio da Responsabilidade Única - SRP), devemos evitar poluir o componente com regras de negócio ou acessos a banco de dados.

O Componente deve agir como a View pura, delegando o controle para Serviços injetados.

<!-- FornecedoresList.razor (Camada de Apresentação) -->
@page "/fornecedores"
<!-- Injetamos o contrato, não a classe concreta -->
@inject IFornecedorService FornecedorService

<h3>Lista de Fornecedores</h3>
@if (fornecedores == null) {
    <p>Carregando...</p>
} else {
    <ul>
    @foreach (var f in fornecedores) {
        <li>@f.NomeFantasia - @f.CNPJ</li>
    }
    </ul>
}

@code {
    // O componente só guarda DTOs para renderizar, não Entidades.
    private List<FornecedorDTO> fornecedores;

    protected override async Task OnInitializedAsync() {
        // A regra de "como buscar" está no serviço. O componente é ignorante.
        fornecedores = await FornecedorService.ObterTodosAtivosAsync();
    }
}

4. Herança Estratégica e Polimorfismo Tático

A Herança (quando a Classe B "é uma" Classe A) é um relacionamento muito forte, o mais forte da Orientação a Objetos. Se abusada, cria arquiteturas engessadas. No entanto, em sistemas enterprise, ela é a chave para a padronização horizontal (Cross-cutting concerns).

4.1. O Padrão EntityBase (Auditoria e Padronização)

Em um ERP com dezenas de tabelas, todas as tabelas precisam de uma Chave Primária (ID), e geralmente precisam de campos de auditoria (quem criou, quando atualizou) e um controle de deleção lógica (Soft Delete).

Em vez de escrever isso dezenas de vezes, usamos herança abstrata:

// A classe é abstract: não pode ser instanciada diretamente com 'new EntityBase()'
public abstract class EntityBase {
    public Guid Id { get; protected set; } = Guid.NewGuid();
    public DateTime CriadoEm { get; protected set; } = DateTime.UtcNow;
    public DateTime? AtualizadoEm { get; protected set; }
    public bool Excluido { get; protected set; } = false;

    public void MarcarComoExcluido() {
        Excluido = true;
        AtualizadoEm = DateTime.UtcNow;
    }
}

// Cliente ganha Id, CriadoEm, etc., gratuitamente de forma padronizada.
public class Cliente : EntityBase {
    public string RazaoSocial { get; set; }
    public string CNPJ { get; set; }
}

4.2. ComponentBase: Reuso de Lógica de Interface

No Blazor, todo componente herda implicitamente da classe Microsoft.AspNetCore.Components.ComponentBase. Mas você pode criar sua própria classe base para compartilhar lógica de UI, como exibição de Toasts (Notificações) ou controle de loading global, sem precisar repetir o código do bloco @code em toda página.


5. Arquitetura em Camadas (N-Tier e Clean Architecture Lite)

Sistemas complexos não devem ter todo o seu código em um único balde. Nós agrupamos arquivos em Projetos distintos dentro da mesma Solução (Solution - .sln) no .NET. Cada projeto representa uma Camada.

5.1. A Regra da Dependência

A regra mais sagrada da arquitetura limpa: As dependências do código fonte devem apontar apenas para dentro, em direção às políticas de negócio.

Isso significa que a regra de cálculo de imposto (Domínio) não pode importar bibliotecas de banco de dados (Infraestrutura) ou de interface (Blazor Web). O negócio é agnóstico à tecnologia.

5.2. Anatomia das Camadas

1. A Camada de Domínio (Core / Domain)

É o coração do ERP. Não tem dependências externas (sem pacotes Nuget de banco, web, etc.).

  • O que mora aqui: Entidades (Produto, Cliente), Objetos de Valor (CPF, Endereco), Enumerações, Exceções de Domínio personalizadas e Interfaces de Repositórios (IClienteRepository).
2. A Camada de Aplicação (Application)

É o orquestrador. Pense nela como o antigo "Controller" do MVC. Ela coordena as Entidades, mas não contém as regras intrínsecas delas. Ela diz ao banco "salve isso", mas não sabe como o banco faz isso.

  • O que mora aqui: Classes de Serviço (ClienteService), DTOs (Data Transfer Objects), Mapeadores (AutoMapper) e Casos de Uso.
3. A Camada de Infraestrutura (Infrastructure)

São os "detalhes de implementação". É aqui que o sistema toca o mundo real.

  • O que mora aqui: Contexto do Banco (ApplicationDbContext via Entity Framework), implementações concretas dos Repositórios (ClienteRepository que faz o INSERT real), serviços de envio de Email (SmtpClient), integração com APIs externas (ex: consulta de CEP via HTTP).
4. A Camada de Apresentação (Web / Presentation)

A casca do sistema. Apenas renderiza e captura inputs.

  • O que mora aqui: Páginas Blazor (.razor), arquivos CSS/JS, injeção de dependência raiz (Program.cs), Controllers de API (se existirem). Depende da Aplicação para fazer o trabalho pesado.

6. A Fronteira dos Dados: Entidades vs. DTOs

Por que não podemos simplesmente passar a Entidade Usuario, que buscamos no banco de dados, diretamente para o HTML do Blazor, permitir que o usuário altere os dados e salvar a mesma entidade de volta no banco? Esta é uma das perguntas mais comuns de arquitetura.

A resposta envolve segurança, acoplamento e performance.

6.1. O Problema do Over-Posting (Mass Assignment)

Imagine que a sua entidade Usuario tem os campos: Id, Nome, Email, SenhaHash, EhAdministrador.

Você cria uma tela de "Atualizar Perfil" para o usuário alterar apenas seu Nome e Email. Se você usar a entidade diretamente, um usuário mal-intencionado pode interceptar a requisição HTTP POST (usando ferramentas como Postman ou Burp Suite) e enviar no payload JSON um campo extra: "EhAdministrador": true.

Se o seu serviço apenas pegar esse objeto e mandar salvar no banco, parabéns: o usuário acabou de se conceder privilégios de administrador sem você saber. Isso é o Over-posting.

6.2. Mapeamento e Segurança (O papel do DTO)

Para proteger o sistema, criamos um Data Transfer Object (DTO). Um DTO é um pacote de dados burro, feito sob medida para uma tela ou caso de uso específico.

// DTO feito especificamente para a tela de Atualizar Perfil
public class UsuarioAtualizarPerfilDTO {
    public string Nome { get; set; }
    public string Email { get; set; }
    // Note: Não existem os campos SenhaHash ou EhAdministrador aqui!
}

Na Camada de Aplicação, o Serviço recebe o DTO, valida, busca a Entidade original no banco, copia (mapeia) apenas os campos permitidos do DTO para a Entidade, e então salva a Entidade protegida de volta no banco. Bibliotecas como o AutoMapper ou métodos manuais são usados para fazer essa conversão (Mapping) de DTO para Entity e vice-versa.


7. Traduzindo UML para Código C#

A linguagem UML (Unified Modeling Language) não serve para desenhar o banco de dados; serve para desenhar a estrutura de classes de software. É crucial saber traduzir setas para código C#.

Relação UML Como aparece no Diagrama Implementação em C#
Herança (Generalização)
"É um"
Linha sólida com Triângulo vazio fechado apontando para a base. public class Fornecedor : PessoaJuridica { }
Realização (Implementação)
"Comporta-se como"
Linha tracejada com Triângulo vazio fechado apontando para a interface. public class FornecedorService : IFornecedorService { }
Composição
"Parte inseparável"
Linha com Losango preenchido no lado do dono. public Pedido() { Itens = new List<Item>(); } (O Pedido cria e gerencia o tempo de vida do Item).
Agregação
"Possui, mas independente"
Linha com Losango vazio no lado do dono. Uma Turma tem vários Alunos. Se a Turma acabar, os Alunos continuam existindo no sistema. Passado via construtor ou lista.
Dependência / Associação
"Usa um"
Linha (sólida ou tracejada) com seta aberta apontando para quem é usado. public class MeuServico(IRepository repo) { } (O Serviço usa o Repositório via injeção de dependência).

Conclusão e Reflexão

Construir sistemas ERP requer disciplina. Quando deixamos a lógica vazar da Camada de Domínio para a View, ou quando ignoramos o uso de DTOs para economizar tempo, não estamos apenas sendo preguiçosos; estamos acumulando Dívida Técnica.

A Injeção de Dependência, as Interfaces, a Herança estruturada e o padrão MVC em componentes são as barricadas que impedem a entropia de destruir o seu projeto à medida que ele escala de 10 telas para 500 telas.

Exercícios Práticos para o Projeto

  1. Arquitetura Mental: Olhe para a funcionalidade principal que o seu grupo está desenvolvendo. Liste em um papel: Qual é a Entity? Qual é o DTO que a tela precisa? Qual é a interface do Service?
  2. Código Limpo: Revise um componente Blazor (.razor) do seu projeto. Existe alguma chamada direta ao Entity Framework (DbContext) no bloco @code? Se sim, mova isso imediatamente para uma classe de Serviço na camada de Aplicação e injete o serviço no componente.
  3. Herança na Prática: Crie uma classe EntityBase abstrata no seu projeto contendo Id (Guid) e DataCriacao. Faça as suas entidades do banco de dados herdarem dessa base. O que aconteceu com a repetição de código no seu projeto?