Projeto: Um Sistema de Formas de Pagamento

Projeto: Um Sistema de Formas de Pagamento

Um projeto integrador que reúne toda a fase de objetos num sistema de formas de pagamento: classe abstrata como base comum, herança, polimorfismo para processar qualquer pagamento uniformemente, properties e construtores garantindo objetos válidos — e o raciocínio de projeto por trás.
Linguagem C#

• • 11 min de leitura

Aprendemos, ao longo de duas fases, os pilares da orientação a objetos: classes e objetos, properties, construtores, métodos, encapsulamento, herança, polimorfismo e interfaces. São muitos conceitos, e há um risco real de que, aprendidos isoladamente, permaneçam como peças soltas na sua mente. Esta aula existe para conjurar esse risco: vamos construir, do zero e passo a passo, um pequeno sistema real que integra todos esses conceitos numa unidade coesa, vendo cada peça encontrar seu lugar. O domínio escolhido é familiar — as formas de pagamento de uma loja —, e cada decisão de projeto que tomarmos aplicará conscientemente uma ideia das aulas anteriores. Não há conceito novo aqui; há a síntese do que você já sabe. Leia com calma, reproduza o código, e observe as engrenagens girarem juntas.

O problema a modelar

Queremos representar diferentes formas de pagar uma compra — cartão de crédito, Pix, boleto —, de modo que o sistema possa processar qualquer uma delas de maneira uniforme, mas cada uma se comporte à sua própria maneira. Reflita sobre o que há de comum e de específico. Comum a todas: toda forma de pagamento tem um valor a ser pago e sabe ser processada. Específico de cada uma: a taxa cobrada varia (o cartão cobra uma porcentagem, o Pix é gratuito, o boleto tem taxa menor), e a forma de processar difere. Essa combinação de "parte comum + parte que varia" é exatamente o cenário onde a orientação a objetos brilha, e onde herança e polimorfismo mostram seu valor.

Passo 1: a base comum com uma classe abstrata

Começamos definindo o que toda forma de pagamento tem em comum. Como todas terão um valor e um comportamento de processar, mas nenhuma forma de pagamento "genérica" existe de fato (não se paga com uma "forma genérica", mas com um cartão ou um Pix específico), esta classe base não deve poder ser instanciada diretamente. Para isso, usamos uma classe abstrata — um tipo de classe base que serve apenas para ser herdada, nunca criada diretamente, e que pode exigir que as derivadas implementem certos comportamentos.

// Classe ABSTRATA: base para as formas de pagamento, não pode ser criada diretamente.
abstract class Pagamento
{
    public decimal Valor { get; }

    // Construtor que valida o valor no nascimento (Aula #25):
    public Pagamento(decimal valor)
    {
        if (valor <= 0)
        {
            Console.WriteLine("Aviso: valor inválido. Usando 1.");
            Valor = 1;
        }
        else
        {
            Valor = valor;
        }
    }

    // Método ABSTRATO: cada forma DEVE fornecer sua própria taxa.
    // 'abstract' significa "sem corpo aqui; as derivadas são obrigadas a implementar".
    public abstract decimal Taxa();

    // Método concreto, COMUM a todas: calcula o total com a taxa aplicada.
    public decimal ValorTotal()
    {
        return Valor + (Valor * Taxa());
    }

    // Método ABSTRATO: cada forma processa a seu modo.
    public abstract void Processar();
}

Há um conceito novo aqui, o método abstrato, e vale explicá-lo com cuidado, pois é o parente próximo do virtual que você já conhece. Um método marcado como abstract não tem corpo na classe base — ele apenas declara que o comportamento existe e deve ser fornecido pelas classes derivadas. É mais forte que virtual: onde virtual permite que a derivada personalize (mas não obriga), abstract obriga — toda classe derivada concreta é forçada a fornecer sua versão, ou o programa não compila. Assim, Taxa() e Processar() são abstratos porque cada forma de pagamento precisa defini-los à sua maneira, e não faria sentido uma implementação genérica. Já ValorTotal() é concreto (tem corpo) porque o cálculo do total — valor mais a taxa aplicada — é o mesmo para todas; ele é implementado uma vez na base e herdado por todas, chamando o Taxa() de cada uma. Note como a base combina o que é comum (ValorTotal, o construtor com validação) com o que varia (Taxa, Processar abstratos), delegando às derivadas apenas o que de fato difere.

Passo 2: as formas concretas por herança e polimorfismo

Agora criamos cada forma de pagamento herdando de Pagamento e fornecendo suas versões dos métodos abstratos:

class CartaoCredito : Pagamento
{
    public CartaoCredito(decimal valor) : base(valor) { }   // repassa à base (Aula #28)

    public override decimal Taxa() => 0.03m;   // 3% de taxa
    public override void Processar()
    {
        Console.WriteLine($"Cartão: cobrado R$ {ValorTotal()} (com 3% de taxa).");
    }
}

class Pix : Pagamento
{
    public Pix(decimal valor) : base(valor) { }

    public override decimal Taxa() => 0m;      // Pix sem taxa
    public override void Processar()
    {
        Console.WriteLine($"Pix: recebido R$ {ValorTotal()} instantaneamente, sem taxa.");
    }
}

class Boleto : Pagamento
{
    public Boleto(decimal valor) : base(valor) { }

    public override decimal Taxa() => 0.01m;   // 1% de taxa
    public override void Processar()
    {
        Console.WriteLine($"Boleto emitido no valor de R$ {ValorTotal()} (com 1% de taxa).");
    }
}

Cada classe usa override (Aula #29) para fornecer sua taxa e seu processamento, e : base(valor) (Aula #28) para repassar o valor ao construtor da base, que o valida. Repare que ValorTotal(), herdado da base, funciona corretamente em cada forma, pois chama o Taxa() específico de cada uma — o polimorfismo em ação, dentro da própria hierarquia. As três formas compartilham a estrutura comum e diferem apenas no essencial.

Passo 3: processando qualquer pagamento uniformemente

Agora colhemos o fruto do polimorfismo. Podemos ter uma coleção de Pagamento contendo formas diferentes, e processá-las todas uniformemente, sem verificar de qual tipo cada uma é — cada uma se processa à sua maneira automaticamente:

// Uma lista de Pagamento com formas diferentes misturadas:
List<Pagamento> pagamentos = new()
{
    new CartaoCredito(100m),
    new Pix(250m),
    new Boleto(80m)
};

decimal totalProcessado = 0m;

foreach (Pagamento p in pagamentos)
{
    p.Processar();                 // cada um processa à SUA maneira (polimorfismo)
    totalProcessado += p.ValorTotal();
}

Console.WriteLine($"\nTotal processado: R$ {totalProcessado}");

Observe o foreach: ele trata cada elemento como um Pagamento genérico, chamando p.Processar() e p.ValorTotal() de forma uniforme — e, ainda assim, cada objeto executa o comportamento correto do seu tipo real (o cartão cobra 3%, o Pix nada, o boleto 1%). Não há uma cadeia de if verificando tipos; o polimorfismo cuida disso. E aqui está a beleza da extensibilidade: se amanhã a loja aceitar uma nova forma de pagamento — digamos, uma criptomoeda —, basta criar uma classe Criptomoeda : Pagamento com sua taxa e seu processamento, e adicioná-la à lista. Nada no código que percorre e processa os pagamentos precisa mudar. Essa capacidade de estender o sistema sem modificar o que já existe é a marca de um bom projeto orientado a objetos, e você a construiu aqui com suas próprias mãos.

O que este projeto demonstra

Pare um momento e veja o todo, pois cada conceito das últimas fases tem um papel visível. A classe Pagamento e suas derivadas modelam as coisas do problema (Aula #23). As properties (Valor) encapsulam estado, com o Valor sendo somente-leitura de fora (Aula #24, #27). O construtor valida o valor no nascimento, garantindo objetos válidos (Aula #25). Os métodos (ValorTotal, Processar, Taxa) dão comportamento aos objetos (Aula #26). A herança faz cada forma reaproveitar a base comum (Aula #28). O polimorfismo, via abstract/override, faz cada forma se comportar à sua maneira sob um comando uniforme (Aula #29). E o encapsulamento protege o estado (Aula #27). Nenhuma peça está ali por acaso — e é justamente vê-las encaixadas, resolvendo um problema real e coeso, que transforma conceitos aprendidos isoladamente em compreensão integrada. Um bom exercício mental, agora, é imaginar como você adicionaria a criptomoeda mencionada: perceber que a resposta é "apenas uma classe nova, sem tocar no resto" é sinal de que você entendeu por que a orientação a objetos vale o esforço.

Este projeto reuniu, num único sistema coeso, tudo o que a fase construiu peça por peça. A classe abstrata forneceu a base comum com implementação compartilhada e exigiu das derivadas o que só elas podem definir; a herança evitou repetição; o polimorfismo permitiu processar qualquer forma de pagamento com o mesmo código; as properties e o construtor garantiram objetos válidos desde a criação.

O ponto a levar adiante não é o domínio escolhido, e sim a forma de raciocinar: identificar o que é comum e elevar à base, isolar o que varia e deixar as derivadas resolverem, e escrever o código de uso contra a abstração. É esse desenho que torna um sistema capaz de receber um caso novo sem sofrer alterações em cascata.

Fontes e leituras recomendadas

Exercícios

Exercício 1

Reproduza o projeto completo (a classe abstrata Pagamento, as três formas concretas, e o código que as processa) e execute-o. Confirme a saída e o total processado. Depois, altere os valores e observe como o ValorTotal de cada forma se ajusta às taxas.

Ver resposta

✓ Resposta: Ao executar o projeto, cada pagamento exibe sua mensagem específica com o ValorTotal correto (cartão com 3%, Pix sem taxa, boleto com 1%), e o total ao final soma os três valores totais. Alterando os valores, o ValorTotal de cada forma muda conforme Valor + Valor * Taxa, e o total acompanha, confirmando que o cálculo comum (na base) reflete corretamente a taxa específica de cada derivada.

Exercício 2

Adicione uma nova forma de pagamento, Criptomoeda, com taxa de 0,5% e uma mensagem de processamento própria, e inclua-a na lista. Confirme que você não precisou alterar o foreach que processa os pagamentos. Explique, à luz do polimorfismo, por que isso foi possível.

Ver resposta

✓ Resposta: Exemplo:

class Criptomoeda : Pagamento
{
    public Criptomoeda(decimal valor) : base(valor) { }
    public override decimal Taxa() => 0.005m;
    public override void Processar()
        => Console.WriteLine($"Cripto: transferido R$ {ValorTotal()} (com 0,5% de taxa).");
}

Adicionando-a à lista, o foreach não precisou de nenhuma alteração. Isso foi possível porque o foreach trata cada elemento como um Pagamento genérico e chama Processar() e ValorTotal() de forma uniforme; o polimorfismo faz cada objeto executar a versão correta do seu tipo real, inclusive a nova Criptomoeda. Como o código de processamento depende apenas do contrato comum (métodos que todo Pagamento tem), qualquer novo tipo que cumpra esse contrato se encaixa automaticamente.

Exercício 3

Explique, com suas palavras, a diferença entre um método abstract e um método concreto na classe Pagamento. Por que Taxa() e Processar() são abstratos, enquanto ValorTotal() é concreto?

Ver resposta

✓ Resposta: Um método abstract não tem corpo na classe base — apenas declara que o comportamento deve existir e obriga toda classe derivada concreta a fornecê-lo. Um método concreto tem corpo e é herdado pronto pelas derivadas. Taxa() e Processar() são abstratos porque variam de uma forma de pagamento para outra — cada uma tem sua própria taxa e seu próprio modo de processar —, e não existe uma versão genérica que faça sentido; por isso a base apenas exige que cada derivada os defina. ValorTotal() é concreto porque o cálculo do total (valor mais a taxa aplicada) é o mesmo para todas as formas; ele é implementado uma única vez na base e reutilizado por todas, apenas invocando o Taxa() específico de cada uma.

Exercício 4

Explique por que a classe Pagamento foi declarada como abstract. O que aconteceria se tentássemos criar um objeto com new Pagamento(100m), e por que essa proibição faz sentido para este problema?

Ver resposta

✓ Resposta: A classe Pagamento foi declarada abstract porque ela representa a ideia geral de uma forma de pagamento, mas não uma forma concreta que se possa usar diretamente — não se paga com uma "forma genérica", e sim com um cartão, um Pix ou um boleto específicos. Tentar new Pagamento(100m) geraria um erro de compilação, pois classes abstratas não podem ser instanciadas. Essa proibição faz sentido para o problema porque um Pagamento sem tipo definido não teria taxa nem forma de processar (seus métodos abstratos não têm implementação na base); só as formas concretas, que fornecem esses comportamentos, são objetos utilizáveis. A abstração garante que só se criem pagamentos completos e bem definidos.

Exercício 5

Sem código: para cada conceito da lista a seguir, aponte onde no projeto ele foi aplicado e o que ele contribuiu: (a) construtor com validação; (b) herança; (c) polimorfismo; (d) property somente-leitura de fora.

Ver resposta

✓ Resposta: (a) Construtor com validação: na classe base Pagamento, cujo construtor verifica se o valor é positivo antes de guardá-lo — contribui garantindo que todo pagamento nasça com um valor válido. (b) Herança: cada forma concreta (CartaoCredito, Pix, Boleto) herda de Pagamento com : Pagamento — contribui reaproveitando a estrutura comum (o Valor, o ValorTotal, o construtor) sem repeti-la. (c) Polimorfismo: no foreach, p.Processar() executa a versão correta de cada tipo real automaticamente — contribui permitindo tratar formas diferentes uniformemente e estender o sistema sem alterar o código de processamento. (d) Property somente-leitura de fora: o Valor { get; } da base, que pode ser lido mas não alterado depois de definido no construtor — contribui protegendo o valor do pagamento contra modificações indevidas após a criação.

Comentários

Mais em Linguagem C#

Retrospectiva Final: O que Você é Capaz de Construir
Retrospectiva Final: O que Você é Capaz de Construir

O encerramento do curso completo: um inventário do que passou a ser possível…

A Grande Virada: O que É um Objeto
A Grande Virada: O que É um Objeto

A virada conceitual mais importante do curso: por que valores soltos não…

A Instrução switch: Escolhendo Entre Muitas Opções
A Instrução switch: Escolhendo Entre Muitas Opções

Quando comparar um mesmo valor contra muitas opções, encadear condições fica…