Módulo 1 · C# — Capítulo 12

Padrões de Projeto

Cinco padrões que você vai encontrar (e usar) o tempo todo em código C# de produção — incluindo o que organiza o acesso às procedures que você escreveu no Módulo 2.

1. Por que padrões de projeto

Um padrão de projeto é uma solução com nome para um problema recorrente de design de código — não uma biblioteca, mas uma forma de organizar classes que já se provou útil repetidamente. Os cinco abaixo cobrem a maioria dos casos que aparecem num sistema C#/SQL Server típico.

2. Repository — isolando o acesso a dados

O padrão mais relevante para este curso: encapsula toda a lógica de acesso a dados (chamadas às procedures) atrás de uma interface, para que o resto da aplicação nunca precise saber se os dados vêm de SQL Server, de uma API externa ou de um cache.

C# Repository
public interface IClienteRepository
{
    Task<Cliente?> ObterPorIdAsync(int id);
    Task<List<Cliente>> ListarAsync(string? nome, bool? ativo);
    Task<int> InserirAsync(Cliente cliente);
}

public class ClienteRepository : IClienteRepository
{
    private readonly string _connectionString;

    public ClienteRepository(string connectionString)
        => _connectionString = connectionString;

    public async Task<Cliente?> ObterPorIdAsync(int id)
    {
        await using var conexao = new SqlConnection(_connectionString);
        return await conexao.QueryFirstOrDefaultAsync<Cliente>(
            "dbo.usp_ClienteObter", new { Id = id },
            commandType: CommandType.StoredProcedure);
    }

    // ObterPorIdAsync, ListarAsync, InserirAsync — todos chamando as
    // procedures do Módulo 2 via Dapper, como visto no capítulo 13
}

A classe que usa o repositório depende só da interface IClienteRepository, nunca de ClienteRepository diretamente — o que abre caminho para o próximo padrão.

3. Dependency Injection — quem monta as peças não é quem as usa

Em vez de uma classe criar suas próprias dependências com new, elas são "injetadas" de fora — normalmente pelo construtor. Isso permite trocar a implementação real por uma de teste sem mudar o código que a utiliza.

C# injeção via construtor
public class ClienteService
{
    private readonly IClienteRepository _repositorio;

    // a dependência chega de fora — ClienteService não sabe (nem precisa saber)
    // se é o repositório real ou uma versão de teste
    public ClienteService(IClienteRepository repositorio)
        => _repositorio = repositorio;

    public async Task<Cliente> ObterOuFalharAsync(int id)
        => await _repositorio.ObterPorIdAsync(id)
           ?? throw new InvalidOperationException("Cliente não encontrado");
}
Onde isso aparece de verdade

Você vai ver Dependency Injection formalizado como parte do próprio framework no Módulo 3 (ASP.NET Core tem um container de DI embutido) — o padrão acima é a base conceitual de tudo aquilo, sem depender de nenhum framework.

4. Singleton — uma única instância compartilhada

C# Singleton clássico (raramente escrito à mão hoje)
public sealed class ConfiguracaoApp
{
    private static readonly Lazy<ConfiguracaoApp> _instancia = new(() => new ConfiguracaoApp());
    public static ConfiguracaoApp Instancia => _instancia.Value;

    public string ConnectionString { get; }

    private ConfiguracaoApp() // construtor privado — só a própria classe cria a instância
    {
        ConnectionString = "Server=...;Database=...;";
    }
}

var config = ConfiguracaoApp.Instancia; // sempre o mesmo objeto
Nota

Em código ASP.NET Core moderno, você quase nunca escreve um Singleton assim manualmente — o container de Dependency Injection do framework já gerencia isso com services.AddSingleton<T>(). Vale conhecer a implementação manual para entender o que o framework está fazendo por baixo.

5. Factory Method — centralizando a criação de objetos

C# Factory Method
public interface INotificador
{
    Task EnviarAsync(string mensagem);
}

public static class NotificadorFactory
{
    public static INotificador Criar(string canal) => canal switch
    {
        "email" => new NotificadorEmail(),
        "sms"   => new NotificadorSms(),
        _       => throw new ArgumentException($"Canal desconhecido: {canal}")
    };
}

INotificador notificador = NotificadorFactory.Criar("email");
await notificador.EnviarAsync("Pedido confirmado!");

Repare o uso da switch expression do capítulo 3 — a fábrica concentra a lógica de "qual classe concreta criar" num único lugar, e quem chama só conhece a interface INotificador.

6. Strategy — trocando um algoritmo em tempo de execução

Este é o padrão que mais se apoia no que você viu no capítulo anterior: representar um comportamento intercambiável como um Func/interface, decidido em tempo de execução.

C# Strategy com Func (versão leve)
Dictionary<string, Func<decimal, decimal, decimal>> estrategiasDesconto = new()
{
    ["percentual"] = (preco, valor) => preco - (preco * valor / 100),
    ["fixo"]       = (preco, valor) => Math.Max(0, preco - valor),
};

decimal AplicarDesconto(decimal preco, string tipo, decimal valor)
    => estrategiasDesconto[tipo](preco, valor);

AplicarDesconto(100m, "percentual", 10m); // 90
AplicarDesconto(100m, "fixo", 15m);       // 85

7. Onde isso te leva

Repository e Dependency Injection, em particular, são a espinha dorsal de como o Módulo 3 (MVC) e o Módulo 4 (APIs) organizam código em torno de procedures. O próximo capítulo entra na integração formal com o banco via ADO.NET e Entity Framework Core, aprofundando o que o Módulo 2 já introduziu.

📌 Resumo do capítulo

  • Repository: esconde o acesso a dados (procedures) atrás de uma interface.
  • Dependency Injection: dependências chegam de fora (via construtor), não são criadas internamente com new.
  • Singleton: uma única instância compartilhada — hoje geralmente gerenciada pelo container de DI do framework, não escrita à mão.
  • Factory Method: centraliza a decisão de qual classe concreta instanciar.
  • Strategy: comportamento intercambiável, muitas vezes implementado com Func/interface + Dictionary.

✏️ Praticando

  1. Implemente IClienteRepository/ClienteRepository completo, chamando as procedures do Módulo 2 via Dapper.
  2. Escreva ClienteService recebendo IClienteRepository por construtor, e uma versão "fake" do repositório (em memória, sem banco) para testar o serviço sem precisar do SQL Server.
  3. Implemente o Strategy de desconto do exemplo e adicione uma terceira estratégia, "progressivo" (desconto maior quanto maior o valor da compra).