Padrões de Projeto Essenciais para Desenvolvedores .NET

Os padrões de projeto (design patterns) são soluções elegantes para problemas recorrentes no desenvolvimento de software. Eles representam as melhores práticas acumuladas por desenvolvedores experientes ao longo de décadas e oferecem um vocabulário comum que facilita a comunicação entre profissionais.
Como desenvolvedor .NET, dominar esses padrões não apenas melhora a qualidade do seu código, mas também acelera o desenvolvimento e facilita a manutenção a longo prazo. Neste artigo, exploraremos os padrões de design mais relevantes para o ecossistema .NET, com exemplos práticos em C# e casos de uso reais.
Por que Padrões de Projeto são Importantes?
Antes de mergulharmos nos padrões específicos, é importante entender por que eles são tão valiosos:
Soluções comprovadas: Padrões de projeto representam soluções que foram testadas e refinadas ao longo do tempo.
Reutilização de conhecimento: Em vez de reinventar a roda, você aproveita o conhecimento coletivo da comunidade.
Vocabulário comum: Dizer "estou usando um Factory Method aqui" comunica instantaneamente sua intenção a outros desenvolvedores.
Código mais flexível: Padrões geralmente promovem princípios como baixo acoplamento e alta coesão.
Facilidade de manutenção: Código estruturado seguindo padrões conhecidos é mais fácil de entender e modificar.
No ecossistema .NET, muitos desses padrões são incorporados no próprio framework e bibliotecas populares. Entender esses padrões ajuda não apenas a criar seu próprio código, mas também a compreender melhor as ferramentas que você usa diariamente.
Categorias de Padrões de Projeto
Os padrões de projeto são tradicionalmente divididos em três categorias:
Padrões Criacionais: Lidam com a criação de objetos, tentando criar objetos de maneira adequada à situação.
Padrões Estruturais: Lidam com a composição de classes e objetos para formar estruturas maiores.
Padrões Comportamentais: Lidam com a comunicação eficiente e a atribuição de responsabilidades entre objetos.
Vamos explorar os padrões mais úteis em cada categoria, com foco especial em como eles se aplicam no desenvolvimento .NET.
Padrões Criacionais
1. Singleton
O padrão Singleton garante que uma classe tenha apenas uma instância e fornece um ponto global de acesso a ela. Em .NET, este padrão é frequentemente usado para serviços compartilhados, caches, pools de conexão e configurações.
Implementação Tradicional em C#:
public sealed class Singleton
{
private static Singleton _instance;
private static readonly object _lock = new object();
private Singleton() { }
public static Singleton Instance
{
get
{
if (_instance == null)
{
lock (_lock)
{
if (_instance == null)
{
_instance = new Singleton();
}
}
}
return _instance;
}
}
}
Esta implementação usa o padrão "double-check locking" para garantir thread safety.
Implementação Moderna em C#:
public sealed class Singleton
{
private static readonly Lazy<Singleton> _lazy =
new Lazy<Singleton>(() => new Singleton());
private Singleton() { }
public static Singleton Instance => _lazy.Value;
}
A classe Lazy<T>
do .NET cuida de toda a complexidade de lazy load e thread safety.
Quando Usar:
Quando você precisa de exatamente uma instância de uma classe acessível globalmente.
Para recursos compartilhados como caches, pools de conexão ou configurações.
Para coordenar ações em todo o sistema.
Quando Evitar:
Quando o estado compartilhado não é necessário.
Quando dificulta os testes unitários (Singletons são difíceis de simular).
Quando viola o princípio de responsabilidade única.
Exemplo Real em .NET:
O HttpClient
é frequentemente implementado como um Singleton em aplicações .NET modernas, pois sua criação é cara e ele foi projetado para ser reutilizado:
public class ApiService
{
private static readonly Lazy<HttpClient> _httpClient =
new Lazy<HttpClient>(( ) => new HttpClient());
public static HttpClient Client => _httpClient.Value;
}
2. Factory Method
O padrão Factory Method define uma interface para criar um objeto, mas permite que as subclasses decidam qual classe instanciar. Em .NET, este padrão é amplamente utilizado para criar objetos sem expor a lógica de criação.
Implementação em C#:
// Produto
public interface IDocument
{
void Open( );
void Save();
}
// Produtos concretos
public class PdfDocument : IDocument
{
public void Open() => Console.WriteLine("Abrindo documento PDF");
public void Save() => Console.WriteLine("Salvando documento PDF");
}
public class WordDocument : IDocument
{
public void Open() => Console.WriteLine("Abrindo documento Word");
public void Save() => Console.WriteLine("Salvando documento Word");
}
// Criador
public abstract class DocumentCreator
{
// Factory Method
public abstract IDocument CreateDocument();
// Operações que usam o Factory Method
public void OpenDocument()
{
var document = CreateDocument();
document.Open();
}
}
// Criadores concretos
public class PdfDocumentCreator : DocumentCreator
{
public override IDocument CreateDocument() => new PdfDocument();
}
public class WordDocumentCreator : DocumentCreator
{
public override IDocument CreateDocument() => new WordDocument();
}
Quando Usar:
Quando uma classe não pode antecipar a classe de objetos que deve criar.
Quando você quer que as subclasses especifiquem os objetos que criam.
Quando você quer delegar a responsabilidade de criar objetos para subclasses.
Quando Evitar:
Quando a hierarquia de criação se torna muito complexa.
Quando você precisa criar apenas um tipo de objeto.
Exemplo Real em .NET:
O Entity Framework Core usa o padrão Factory Method para criar instâncias de DbContext
:
public class BloggingContextFactory : IDesignTimeDbContextFactory<BloggingContext>
{
public BloggingContext CreateDbContext(string[] args)
{
var optionsBuilder = new DbContextOptionsBuilder<BloggingContext>();
optionsBuilder.UseSqlServer("connection_string");
return new BloggingContext(optionsBuilder.Options);
}
}
3. Abstract Factory
O padrão Abstract Factory fornece uma interface para criar famílias de objetos relacionados ou dependentes sem especificar suas classes concretas. É como uma fábrica de fábricas.
Implementação em C#:
// Produtos abstratos
public interface IButton
{
void Render();
void OnClick();
}
public interface ITextBox
{
void Render();
void OnInput();
}
// Produtos concretos - Família Windows
public class WindowsButton : IButton
{
public void Render() => Console.WriteLine("Renderizando botão Windows");
public void OnClick() => Console.WriteLine("Clique em botão Windows");
}
public class WindowsTextBox : ITextBox
{
public void Render() => Console.WriteLine("Renderizando caixa de texto Windows");
public void OnInput() => Console.WriteLine("Entrada em caixa de texto Windows");
}
// Produtos concretos - Família macOS
public class MacOSButton : IButton
{
public void Render() => Console.WriteLine("Renderizando botão macOS");
public void OnClick() => Console.WriteLine("Clique em botão macOS");
}
public class MacOSTextBox : ITextBox
{
public void Render() => Console.WriteLine("Renderizando caixa de texto macOS");
public void OnInput() => Console.WriteLine("Entrada em caixa de texto macOS");
}
// Abstract Factory
public interface IUIFactory
{
IButton CreateButton();
ITextBox CreateTextBox();
}
// Fábricas concretas
public class WindowsUIFactory : IUIFactory
{
public IButton CreateButton() => new WindowsButton();
public ITextBox CreateTextBox() => new WindowsTextBox();
}
public class MacOSUIFactory : IUIFactory
{
public IButton CreateButton() => new MacOSButton();
public ITextBox CreateTextBox() => new MacOSTextBox();
}
// Cliente
public class Application
{
private readonly IButton _button;
private readonly ITextBox _textBox;
public Application(IUIFactory factory)
{
_button = factory.CreateButton();
_textBox = factory.CreateTextBox();
}
public void RenderUI()
{
_button.Render();
_textBox.Render();
}
}
Quando Usar:
Quando o sistema precisa ser independente de como seus produtos são criados.
Quando o sistema deve trabalhar com múltiplas famílias de produtos.
Quando você quer fornecer uma biblioteca de classes revelando apenas suas interfaces.
Quando Evitar:
Quando adicionar novos produtos requer modificar a interface da fábrica.
Quando a hierarquia de produtos e fábricas se torna muito complexa.
Exemplo Real em .NET:
O ASP.NET Core usa o padrão Abstract Factory para criar componentes de UI em diferentes plataformas:
// Simplificação do conceito usado no ASP.NET Core
public interface IComponentFactory
{
IComponent CreateTextInput();
IComponent CreateButton();
}
public class RazorComponentFactory : IComponentFactory
{
public IComponent CreateTextInput() => new RazorTextInput();
public IComponent CreateButton() => new RazorButton();
}
public class BlazorComponentFactory : IComponentFactory
{
public IComponent CreateTextInput() => new BlazorTextInput();
public IComponent CreateButton() => new BlazorButton();
}
4. Builder
O padrão Builder separa a construção de um objeto complexo da sua representação, permitindo que o mesmo processo de construção crie diferentes representações.
Implementação em C#:
// Produto
public class Pizza
{
public string Dough { get; set; }
public string Sauce { get; set; }
public string Topping { get; set; }
public override string ToString() =>
$"Pizza com massa {Dough}, molho {Sauce} e cobertura {Topping}";
}
// Builder abstrato
public interface IPizzaBuilder
{
void BuildDough();
void BuildSauce();
void BuildTopping();
Pizza GetPizza();
}
// Builder concreto
public class HawaiianPizzaBuilder : IPizzaBuilder
{
private Pizza _pizza = new Pizza();
public void BuildDough() => _pizza.Dough = "fina";
public void BuildSauce() => _pizza.Sauce = "suave";
public void BuildTopping() => _pizza.Topping = "presunto e abacaxi";
public Pizza GetPizza() => _pizza;
}
public class SpicyPizzaBuilder : IPizzaBuilder
{
private Pizza _pizza = new Pizza();
public void BuildDough() => _pizza.Dough = "grossa";
public void BuildSauce() => _pizza.Sauce = "picante";
public void BuildTopping() => _pizza.Topping = "pepperoni e pimenta";
public Pizza GetPizza() => _pizza;
}
// Diretor
public class Waiter
{
private IPizzaBuilder _pizzaBuilder;
public void SetPizzaBuilder(IPizzaBuilder pizzaBuilder) =>
_pizzaBuilder = pizzaBuilder;
public Pizza GetPizza() => _pizzaBuilder.GetPizza();
public void ConstructPizza()
{
_pizzaBuilder.BuildDough();
_pizzaBuilder.BuildSauce();
_pizzaBuilder.BuildTopping();
}
}
Implementação Moderna com Fluent Interface:
public class Pizza
{
public string Dough { get; set; }
public string Sauce { get; set; }
public string Topping { get; set; }
public override string ToString() =>
$"Pizza com massa {Dough}, molho {Sauce} e cobertura {Topping}";
}
public class PizzaBuilder
{
private Pizza _pizza = new Pizza();
public PizzaBuilder WithDough(string dough)
{
_pizza.Dough = dough;
return this;
}
public PizzaBuilder WithSauce(string sauce)
{
_pizza.Sauce = sauce;
return this;
}
public PizzaBuilder WithTopping(string topping)
{
_pizza.Topping = topping;
return this;
}
public Pizza Build() => _pizza;
}
// Uso:
var pizza = new PizzaBuilder()
.WithDough("fina")
.WithSauce("picante")
.WithTopping("pepperoni")
.Build();
Quando Usar:
Quando o processo de construção deve permitir diferentes representações do objeto.
Quando o algoritmo para criar um objeto complexo deve ser independente das partes e como elas são montadas.
Quando a construção deve ser feita em etapas.
Quando Evitar:
Quando o objeto não tem uma estrutura complexa.
Quando a flexibilidade na construção não é necessária.
Exemplo Real em .NET:
O StringBuilder
é um exemplo clássico do padrão Builder no .NET:
var sb = new StringBuilder()
.Append("Olá")
.Append(" ")
.Append("Mundo")
.AppendLine("!")
.AppendLine("Bem-vindo ao C#.");
string result = sb.ToString();
Padrões Estruturais
1. Adapter
O padrão Adapter permite que interfaces incompatíveis trabalhem juntas. Ele atua como um tradutor entre duas interfaces diferentes.
Implementação em C#:
// Interface alvo
public interface ITarget
{
string GetRequest();
}
// Classe adaptada (incompatível com ITarget)
public class Adaptee
{
public string GetSpecificRequest() => "Solicitação específica.";
}
// Adapter
public class Adapter : ITarget
{
private readonly Adaptee _adaptee;
public Adapter(Adaptee adaptee)
{
_adaptee = adaptee;
}
public string GetRequest()
{
// Traduz a chamada para o formato que o Adaptee entende
return $"Adaptado: {_adaptee.GetSpecificRequest()}";
}
}
Quando Usar:
Quando você quer usar uma classe existente, mas sua interface não é compatível.
Quando você quer criar uma classe reutilizável que coopera com classes que não têm interfaces compatíveis.
Quando você precisa usar várias subclasses existentes, mas seria impraticável adaptar cada uma delas.
Quando Evitar:
Quando a adaptação requer mudanças significativas no código existente.
Quando uma interface simples pode ser usada diretamente.
Exemplo Real em .NET:
O .NET usa adaptadores para compatibilidade entre diferentes versões de APIs:
// Exemplo simplificado de como o .NET adapta APIs antigas para novas
public interface IModernApi
{
Task<string> GetDataAsync();
}
public class LegacyApi
{
public string GetData() => "Dados da API legada";
}
public class LegacyApiAdapter : IModernApi
{
private readonly LegacyApi _legacyApi;
public LegacyApiAdapter(LegacyApi legacyApi)
{
_legacyApi = legacyApi;
}
public async Task<string> GetDataAsync()
{
// Adapta a chamada síncrona para assíncrona
return await Task.FromResult(_legacyApi.GetData());
}
}
2. Decorator
O padrão Decorator permite adicionar comportamentos a objetos individuais dinamicamente e de forma transparente, sem afetar o comportamento de outros objetos da mesma classe.
Implementação em C#:
// Componente
public interface ICoffee
{
double GetCost();
string GetDescription();
}
// Componente concreto
public class SimpleCoffee : ICoffee
{
public double GetCost() => 5.0;
public string GetDescription() => "Café simples";
}
// Decorator base
public abstract class CoffeeDecorator : ICoffee
{
protected ICoffee _coffee;
public CoffeeDecorator(ICoffee coffee)
{
_coffee = coffee;
}
public virtual double GetCost() => _coffee.GetCost();
public virtual string GetDescription() => _coffee.GetDescription();
}
// Decoradores concretos
public class MilkDecorator : CoffeeDecorator
{
public MilkDecorator(ICoffee coffee) : base(coffee) { }
public override double GetCost() => _coffee.GetCost() + 1.5;
public override string GetDescription() => $"{_coffee.GetDescription()}, com leite";
}
public class SugarDecorator : CoffeeDecorator
{
public SugarDecorator(ICoffee coffee) : base(coffee) { }
public override double GetCost() => _coffee.GetCost() + 0.5;
public override string GetDescription() => $"{_coffee.GetDescription()}, com açúcar";
}
Quando Usar:
Quando você precisa adicionar responsabilidades a objetos individuais dinamicamente e transparentemente
Quando a extensão por subclasses não é prática
Quando você quer adicionar comportamentos que podem ser retirados
Quando você quer adicionar funcionalidades a um objeto sem afetar outros objetos
Quando Evitar:
Quando a estrutura de decoração se torna muito complexa
Quando o comportamento adicionado é fundamental para o objeto
Exemplo Real em .NET:
O .NET usa o padrão Decorator extensivamente em streams de I/O:
// Exemplo de como o .NET usa decoradores para streams
using (var fileStream = new FileStream("arquivo.txt", FileMode.Create))
using (var bufferedStream = new BufferedStream(fileStream)) // Decorator
using (var gzipStream = new GZipStream(bufferedStream, CompressionMode.Compress)) // Outro decorator
using (var writer = new StreamWriter(gzipStream)) // Outro decorator
{
writer.WriteLine("Exemplo de texto comprimido");
}
3. Facade
O padrão Facade fornece uma interface unificada para um conjunto de interfaces em um subsistema. Ele define uma interface de nível mais alto que torna o subsistema mais fácil de usar.
Implementação em C#:
// Subsistema complexo
public class CPU
{
public void Freeze() => Console.WriteLine("CPU: Congelando...");
public void Jump(long position) => Console.WriteLine($"CPU: Pulando para posição {position}");
public void Execute() => Console.WriteLine("CPU: Executando...");
}
public class Memory
{
public void Load(long position, byte[] data) =>
Console.WriteLine($"Memória: Carregando dados na posição {position}");
}
public class HardDrive
{
public byte[] Read(long lba, int size) =>
new byte[size]; // Simulando leitura
}
// Facade
public class ComputerFacade
{
private readonly CPU _cpu;
private readonly Memory _memory;
private readonly HardDrive _hardDrive;
public ComputerFacade()
{
_cpu = new CPU();
_memory = new Memory();
_hardDrive = new HardDrive();
}
public void Start()
{
_cpu.Freeze();
_memory.Load(0, _hardDrive.Read(0, 1024));
_cpu.Jump(0);
_cpu.Execute();
}
}
Quando Usar:
Quando você quer fornecer uma interface simples para um subsistema complexo.
Quando há muitas dependências entre clientes e classes de implementação.
Quando você quer estruturar um subsistema em camadas.
Quando Evitar:
Quando a simplicidade da fachada não compensa a perda de flexibilidade.
Quando o subsistema já é simples o suficiente.
Exemplo Real em .NET:
O Entity Framework Core usa o padrão Facade para simplificar operações de banco de dados:
// DbContext atua como uma fachada para operações complexas de banco de dados
public class BloggingContext : DbContext
{
public DbSet<Blog> Blogs { get; set; }
public DbSet<Post> Posts { get; set; }
// Uso simples:
// var blog = new Blog { Url = "http://example.com" };
// context.Blogs.Add(blog );
// context.SaveChanges();
}
Padrões Comportamentais
1. Strategy
O padrão Strategy define uma família de algoritmos, encapsula cada um deles e os torna intercambiáveis. Ele permite que o algoritmo varie independentemente dos clientes que o utilizam.
Implementação em C#:
// Estratégia
public interface ISortStrategy
{
List<int> Sort(List<int> list);
}
// Estratégias concretas
public class BubbleSortStrategy : ISortStrategy
{
public List<int> Sort(List<int> list)
{
Console.WriteLine("Ordenando com Bubble Sort");
// Implementação do Bubble Sort
return list.OrderBy(x => x).ToList();
}
}
public class QuickSortStrategy : ISortStrategy
{
public List<int> Sort(List<int> list)
{
Console.WriteLine("Ordenando com Quick Sort");
// Implementação do Quick Sort
return list.OrderBy(x => x).ToList();
}
}
// Contexto
public class SortedList
{
private List<int> _list = new List<int>();
private ISortStrategy _sortStrategy;
public void SetSortStrategy(ISortStrategy sortStrategy)
{
_sortStrategy = sortStrategy;
}
public void Add(int number)
{
_list.Add(number);
}
public List<int> GetSortedList()
{
return _sortStrategy.Sort(_list);
}
}
Quando Usar:
Quando você quer definir uma família de algoritmos.
Quando você precisa de diferentes variantes de um algoritmo.
Quando um algoritmo usa dados que os clientes não deveriam conhecer.
Quando uma classe define muitos comportamentos e eles aparecem como múltiplos condicionais.
Quando Evitar:
Quando o número de estratégias é fixo e raramente muda.
Quando as diferenças entre estratégias são mínimas.
Exemplo Real em .NET:
O LINQ no .NET usa o padrão Strategy para diferentes provedores de consulta:
// LINQ usa diferentes estratégias para diferentes fontes de dados
var numbersFromArray = new[] { 1, 2, 3, 4, 5 }.Where(n => n > 2); // Usa Enumerable.Where
var numbersFromDb = dbContext.Numbers.Where(n => n > 2); // Usa Queryable.Where
2. Observer
O padrão Observer define uma dependência um-para-muitos entre objetos, de modo que quando um objeto muda de estado, todos os seus dependentes são notificados e atualizados automaticamente.
Implementação em C#:
// Sujeito
public interface ISubject
{
void Attach(IObserver observer);
void Detach(IObserver observer);
void Notify();
}
// Observador
public interface IObserver
{
void Update(ISubject subject);
}
// Sujeito concreto
public class WeatherStation : ISubject
{
private List<IObserver> _observers = new List<IObserver>();
private float _temperature;
public float Temperature
{
get => _temperature;
set
{
_temperature = value;
Notify();
}
}
public void Attach(IObserver observer) => _observers.Add(observer);
public void Detach(IObserver observer) => _observers.Remove(observer);
public void Notify()
{
foreach (var observer in _observers)
{
observer.Update(this);
}
}
}
// Observadores concretos
public class TemperatureDisplay : IObserver
{
public void Update(ISubject subject)
{
if (subject is WeatherStation weatherStation)
{
Console.WriteLine($"Display: A temperatura é {weatherStation.Temperature}°C");
}
}
}
public class TemperatureLogger : IObserver
{
public void Update(ISubject subject)
{
if (subject is WeatherStation weatherStation)
{
Console.WriteLine($"Logger: Registrando temperatura: {weatherStation.Temperature}°C");
}
}
}
Implementação Moderna com Eventos:
// Usando eventos do C# para implementar o padrão Observer
public class WeatherStation
{
private float _temperature;
public event EventHandler<float> TemperatureChanged;
public float Temperature
{
get => _temperature;
set
{
_temperature = value;
OnTemperatureChanged(value);
}
}
protected virtual void OnTemperatureChanged(float temperature)
{
TemperatureChanged?.Invoke(this, temperature);
}
}
public class TemperatureDisplay
{
public TemperatureDisplay(WeatherStation weatherStation)
{
weatherStation.TemperatureChanged += HandleTemperatureChanged;
}
private void HandleTemperatureChanged(object sender, float temperature)
{
Console.WriteLine($"Display: A temperatura é {temperature}°C");
}
}
Quando Usar:
Quando uma mudança em um objeto requer mudanças em outros, e você não sabe quantos objetos precisam mudar
Quando um objeto deve notificar outros sem fazer suposições sobre quem são esses objetos
Quando uma abstração tem dois aspectos, um dependente do outro
Quando Evitar:
Quando a notificação é complexa ou específica para certos assinantes
Quando a cadeia de observadores se torna muito longa ou complexa
Exemplo Real em .NET:
O sistema de eventos do .NET é uma implementação do padrão Observer:
public class Button
{
public event EventHandler Click;
public void OnClick()
{
Click?.Invoke(this, EventArgs.Empty);
}
}
// Uso:
var button = new Button();
button.Click += (sender, e) => Console.WriteLine("Botão clicado!");
3. Command
O padrão Command encapsula uma solicitação como um objeto, permitindo parametrizar clientes com diferentes solicitações, enfileirar ou registrar solicitações e suportar operações que podem ser desfeitas.
Implementação em C#:
// Comando
public interface ICommand
{
void Execute();
void Undo();
}
// Receptor
public class Light
{
public void TurnOn() => Console.WriteLine("Luz ligada");
public void TurnOff() => Console.WriteLine("Luz desligada");
}
// Comandos concretos
public class LightOnCommand : ICommand
{
private readonly Light _light;
public LightOnCommand(Light light)
{
_light = light;
}
public void Execute() => _light.TurnOn();
public void Undo() => _light.TurnOff();
}
public class LightOffCommand : ICommand
{
private readonly Light _light;
public LightOffCommand(Light light)
{
_light = light;
}
public void Execute() => _light.TurnOff();
public void Undo() => _light.TurnOn();
}
// Invocador
public class RemoteControl
{
private ICommand _command;
public void SetCommand(ICommand command)
{
_command = command;
}
public void PressButton()
{
_command.Execute();
}
public void PressUndo()
{
_command.Undo();
}
}
Quando Usar:
Quando você quer parametrizar objetos com operações.
Quando você quer enfileirar operações, agendar sua execução ou executá-las remotamente.
Quando você quer implementar operações reversíveis.
Quando Evitar:
Quando a operação é simples e não requer parâmetros ou desfazer.
Quando a hierarquia de comandos se torna muito complexa.
Exemplo Real em .NET:
O padrão Command é usado em muitas bibliotecas de UI do .NET, como WPF com seus comandos:
// Exemplo simplificado de como o WPF usa comandos
public class RelayCommand : ICommand
{
private readonly Action<object> _execute;
private readonly Predicate<object> _canExecute;
public RelayCommand(Action<object> execute, Predicate<object> canExecute = null)
{
_execute = execute;
_canExecute = canExecute;
}
public bool CanExecute(object parameter) => _canExecute == null || _canExecute(parameter);
public void Execute(object parameter) => _execute(parameter);
public event EventHandler CanExecuteChanged;
}
// Uso em ViewModel:
public class ViewModel
{
public ICommand SaveCommand { get; }
public ViewModel()
{
SaveCommand = new RelayCommand(
param => SaveData(),
param => CanSaveData()
);
}
private void SaveData() { /* Implementação */ }
private bool CanSaveData() => true; // Lógica para determinar se pode salvar
}
Padrões Específicos para .NET
Além dos padrões de design clássicos, existem alguns padrões que são particularmente relevantes no ecossistema .NET:
1. Repository
O padrão Repository fica entre o domínio e as camadas de mapeamento de dados, atuando como uma coleção em memória de objetos de domínio.
Implementação em C#:
// Entidade
public class Customer
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
}
// Repositório
public interface ICustomerRepository
{
Customer GetById(int id);
IEnumerable<Customer> GetAll();
void Add(Customer customer);
void Update(Customer customer);
void Delete(int id);
}
// Implementação concreta com Entity Framework
public class CustomerRepository : ICustomerRepository
{
private readonly DbContext _context;
public CustomerRepository(DbContext context)
{
_context = context;
}
public Customer GetById(int id) => _context.Set<Customer>().Find(id);
public IEnumerable<Customer> GetAll() => _context.Set<Customer>().ToList();
public void Add(Customer customer)
{
_context.Set<Customer>().Add(customer);
_context.SaveChanges();
}
public void Update(Customer customer)
{
_context.Entry(customer).State = EntityState.Modified;
_context.SaveChanges();
}
public void Delete(int id)
{
var customer = GetById(id);
if (customer != null)
{
_context.Set<Customer>().Remove(customer);
_context.SaveChanges();
}
}
}
2. Unit of Work
O padrão Unit of Work mantém uma lista de objetos afetados por uma transação de negócios e coordena a escrita de mudanças.
Implementação em C#:
// Unit of Work
public interface IUnitOfWork : IDisposable
{
ICustomerRepository Customers { get; }
IOrderRepository Orders { get; }
int Complete();
}
// Implementação concreta
public class UnitOfWork : IUnitOfWork
{
private readonly DbContext _context;
public ICustomerRepository Customers { get; private set; }
public IOrderRepository Orders { get; private set; }
public UnitOfWork(DbContext context)
{
_context = context;
Customers = new CustomerRepository(_context);
Orders = new OrderRepository(_context);
}
public int Complete() => _context.SaveChanges();
public void Dispose() => _context.Dispose();
}
3. Dependency Injection
O padrão Dependency Injection é fundamental no .NET moderno, especialmente com ASP.NET Core.
Implementação em ASP.NET Core:
// Serviço
public interface IEmailService
{
void SendEmail(string to, string subject, string body);
}
public class SmtpEmailService : IEmailService
{
public void SendEmail(string to, string subject, string body)
{
// Implementação usando SMTP
}
}
// Configuração no ASP.NET Core
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
// Registrando o serviço para injeção de dependência
services.AddScoped<IEmailService, SmtpEmailService>();
}
}
// Uso em um controller
public class UserController : ControllerBase
{
private readonly IEmailService _emailService;
// Injeção de dependência via construtor
public UserController(IEmailService emailService)
{
_emailService = emailService;
}
[HttpPost]
public IActionResult Register(UserModel model)
{
// Lógica de registro
_emailService.SendEmail(model.Email, "Bem-vindo", "Obrigado por se registrar!");
return Ok();
}
}
Armadilhas Comuns ao Usar Padrões de Projeto
Embora os padrões de projeto sejam ferramentas poderosas, eles também podem ser mal utilizados. Aqui estão algumas armadilhas comuns a serem evitadas:
Over-engineering: Aplicar padrões de design onde eles não são necessários pode tornar o código mais complexo sem benefícios reais.
Padrões como fins, não meios: Os padrões são ferramentas para resolver problemas, não objetivos em si mesmos.
Ignorar o contexto: Nem todos os padrões são adequados para todas as situações. Considere o contexto específico do seu projeto.
Rigidez excessiva: Seguir padrões de forma muito rígida pode levar a soluções inflexíveis.
Nomenclatura confusa: Nomear classes com sufixos como "Factory" ou "Strategy" sem que elas realmente implementem esses padrões.
Como Escolher o Padrão Certo
Para escolher o padrão de design mais adequado para uma situação específica:
Identifique o problema: Entenda claramente qual problema você está tentando resolver.
Considere as alternativas: Avalie diferentes padrões que poderiam resolver o problema.
Avalie o contexto: Considere o contexto específico do seu projeto, incluindo requisitos não funcionais como desempenho e manutenibilidade.
Mantenha a simplicidade: Escolha a solução mais simples que resolve o problema adequadamente.
Considere a evolução futura: Pense em como o sistema pode evoluir e se o padrão escolhido suportará essa evolução.
Conclusão
Os padrões de projeto são ferramentas essenciais no arsenal de qualquer desenvolvedor .NET. Eles oferecem soluções testadas e comprovadas para problemas comuns de design de software, permitindo que você crie sistemas mais flexíveis, manuteníveis e robustos.
Neste artigo, exploramos os padrões de design mais relevantes para o ecossistema .NET, com exemplos práticos em C# e casos de uso reais. Lembre-se de que os padrões são guias, não regras rígidas, e lembre-se não existe bala de prata, não existe um padrão ou arquitetura específica que resolve todos os problemas. Use-os com sabedoria, adaptando-os às necessidades específicas do seu projeto.
À medida que você se torna mais familiarizado com esses padrões, começará a reconhecê-los não apenas em seu próprio código, mas também nas bibliotecas e frameworks que você usa diariamente. Isso aprofundará sua compreensão do ecossistema .NET e o tornará um desenvolvedor mais eficaz.
Você já implementou algum desses padrões em seus projetos .NET? Qual foi sua experiência? Compartilhe nos comentários abaixo!
Referências:
Gamma, E., Helm, R., Johnson, R., & Vlissides, J. (1994). Design Patterns: Elements of Reusable Object-Oriented Software.
Freeman, A. (2020). Pro ASP.NET Core 3: Develop Cloud-Ready Web Applications Using MVC, Blazor, and Razor Pages.
Martin, R. C. (2017). Clean Architecture: A Craftsman's Guide to Software Structure and Design.
Microsoft Docs - Design Patterns in .NET
Skeet, J. (2019). C# in Depth, 4th Edition.
Subscribe to my newsletter
Read articles from Eduardo Cintra directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
