← todos os artigos

C#

POO em C#: os exemplos que finalmente fizeram sentido

Dog, Animal, e Bark() nunca grudaram. Encapsulation, abstraction, inheritance, e polymorphism finalmente fizeram sentido quando os vi numa bank account, num notification service, e num payment processor em vez disso.

POO em C#: os exemplos que finalmente fizeram sentido

Todo tutorial de POO com que comecei usava os mesmos exemplos de livro didático: um Dog que herda de Animal e chama Bark(), um Car que herda de Vehicle e chama Drive(). Nada disso grudou, porque eu não construo canis nem concessionárias de carro. Eu construo fluxos de pagamento e integrações de API. Os quatro pilares só de fato fizeram sentido quando os vi em código parecido com o que eu de fato escrevo.

Encapsulation: BankAccount

A versão que não fez sentido foi um campo público que qualquer coisa podia definir diretamente:

public class BankAccount
{
    public decimal Balance;
}

Nada impede qualquer código na solution de definir Balance como um número negativo. O que fez encapsulation fazer sentido não foi "esconda o campo", foi perceber que o object deveria ser a única coisa autorizada a mudar o próprio estado, e só através de regras que ele mesmo aplica:

public class BankAccount
{
    public decimal Balance { get; private set; }

    public void Deposit(decimal amount)
    {
        if (amount <= 0)
            throw new ArgumentException("Deposit amount must be positive.");

        Balance += amount;
    }

    public void Withdraw(decimal amount)
    {
        if (amount > Balance)
            throw new InvalidOperationException("Insufficient funds.");

        Balance -= amount;
    }
}

Abstraction: um notification service

Abstraction fez sentido quando parei de pensar nisso como "faça uma interface" e comecei a pensar nisso como esconder a bagunça com a qual quem chama não deveria precisar se importar:

public interface INotificationService
{
    Task SendSmsAsync(string phoneNumber, string message);
}

public class TwilioNotificationService : INotificationService
{
    private readonly HttpClient _httpClient;

    public async Task SendSmsAsync(string phoneNumber, string message)
    {
        // formatação do payload HTTP, assinatura do auth header, tratamento de erro
    }
}

O que quer que chame SendSmsAsync não precisa saber sobre status codes HTTP ou assinatura de headers. Essa lacuna entre o contrato simples e a implementação bagunçada por trás dele é o que abstraction realmente te dá.

Inheritance: um template de payment processor

Cadeias profundas de inheritance nunca fizeram sentido para mim até eu ver inheritance usada para um workflow compartilhado em vez de uma taxonomia compartilhada:

public abstract class PaymentProcessor
{
    public async Task ProcessTransactionAsync(decimal amount)
    {
        ValidateAmount(amount);
        await ExecutePaymentAsync(amount);
        LogReceipt();
    }

    private void ValidateAmount(decimal amount) { /* lógica compartilhada */ }
    private void LogReceipt() { /* lógica compartilhada */ }

    protected abstract Task ExecutePaymentAsync(decimal amount);
}

public class StripePaymentProcessor : PaymentProcessor
{
    protected override async Task ExecutePaymentAsync(decimal amount)
    {
        // lógica de integração com a API do Stripe
    }
}

A base class é dona do processo. Cada subclass só sobrescreve o único passo que de fato é diferente. Esse é um compromisso bem menor do que as cadeias Dog -> Mammal -> Animal que eu ficava vendo nos tutoriais.

Polymorphism: trocando payment providers

O exemplo que fez polymorphism fazer sentido foi substituir uma cadeia crescente de if/else por uma única abstraction:

public class CheckoutService
{
    private readonly PaymentProcessor _paymentProcessor;

    public CheckoutService(PaymentProcessor paymentProcessor)
    {
        _paymentProcessor = paymentProcessor;
    }

    public async Task CompleteOrderAsync(Order order)
    {
        await _paymentProcessor.ProcessTransactionAsync(order.TotalAmount);
    }
}

CheckoutService não sabe nem se importa com qual subclass concreta de PaymentProcessor recebeu, StripePaymentProcessor ou qualquer coisa adicionada depois. Adicionar um novo método de pagamento significa escrever uma nova subclass, não editar um switch statement enterrado em algum outro lugar do codebase.

O que de fato mudou

O que fez esses quatro grudarem é que todos são coisas que eu de fato escreveria. O conceito finalmente teve onde de verdade se ancorar, em vez de ficar em cima de uma classe Dog que eu nunca mais ia usar.