Aikido

Como favorecer composição em vez de herança para um código de fácil manutenção e flexível

Manutenibilidade

Regra
Favor composição em vez de herança
Profundo herança hierarquias criar um e
e tornam o código mais de compreender e manter.

Idiomas suportados: 45+

Introdução

Herança cria um acoplamento forte entre classes pai e filho, tornando o código frágil e difícil de mudar. Quando uma classe herda comportamento, ela se torna dependente dos detalhes de implementação de seu pai. Subclasses que sobrescrevem métodos mas ainda chamam super são particularmente problemáticos, misturando sua própria lógica com o comportamento herdado de maneiras que quebram quando o pai muda. A composição resolve isso permitindo que os objetos deleguem a outros objetos, criando um acoplamento fraco e uma clara separação de preocupações.

Por que isso importa

Preocupações diversas e alto acoplamento: Herança força preocupações não relacionadas na mesma hierarquia de classes. Uma classe de pagamento recorrente que herda de um processador de pagamentos mistura lógica de agendamento com processamento de pagamentos. Quando você precisa chamar super.process() e então adicionar seu próprio comportamento, você estará fortemente acoplado à implementação do pai. Se o do pai process() o método muda, a classe filha quebra de maneiras inesperadas.

Herdando comportamento indesejado: Subclasses herdam tudo de suas classes pai, incluindo métodos que não precisam ou que exigem implementações diferentes. Um pagamento recorrente herda refund() lógica projetada para pagamentos únicos, mas os reembolsos de assinatura funcionam de forma diferente. Você ou sobrescreve métodos e cria confusão, ou convive com um comportamento herdado inadequado.

Problema da classe base frágil: Alterações em classes pai se propagam por todas as subclasses. Modificando como CreditCardPayment processa pagamentos afeta Pagamento Recorrente com Cartão de Crédito mesmo que a alteração seja irrelevante para o agendamento. Isso torna o refactoring perigoso porque você não pode prever quais subclasses serão afetadas.

Complexidade de teste: Testar classes profundamente em uma hierarquia de herança requer a compreensão do comportamento da classe pai. Para testar o agendamento de pagamentos recorrentes, você também deve lidar com a lógica de processamento de cartão de crédito, chamadas de API do Stripe e validação. A composição permite testar o agendamento com um objeto de pagamento mock simples.

Exemplos de código

❌ Não-conforme:

class Payment {
    constructor(amount, currency) {
        this.amount = amount;
        this.currency = currency;
    }

    async process() {
        throw new Error('Must implement in subclass');
    }

    async refund() {
        throw new Error('Must implement in subclass');
    }

    async sendReceipt(email) {
        // All paymet types need receipts
        await emailService.send(email, this.buildReceipt());
    }
}

class CreditCardPayment extends Payment {
    constructor(amount, currency, cardToken, billingAddress) {
        super(amount, currency);
        this.cardToken = cardToken;
        this.billingAddress = billingAddress;
    }

    async process() {
        await this.validateCard();
        return await stripe.charges.create({
            amount: this.amount * 100,
            source: this.cardToken,
            currency: this.currency
        });
    }

    async refund() {
        await this.validateRefund();
        return await stripe.refunds.create({ charge: this.chargeId });
    }

    async validateCard() {
        // Card validation logic
    }
}

// Problem: RecurringCreditCardPayment's main concern is dealing with scheduling
// and not the actual payment
class RecurringCreditCardPayment extends CreditCardPayment {
    constructor(amount, currency, cardToken, billingAddress, schedule) {
        super(amount, currency, cardToken, billingAddress);
        this.schedule = schedule;
    }

    async process() {
        // Problem: Need to override parent's process() but also use it
        await super.process();
        await this.scheduleNextPayment();
    }

    async scheduleNextPayment() {
        // Subscription scheduling
    }

    // Problem: Inherits refund() from parent but refunding
    // subscriptions needs different logic
}

Por que está errado: Pagamento Recorrente com Cartão de Crédito herda a lógica de processamento de pagamentos, mas sua verdadeira preocupação é o agendamento, não os pagamentos. Ele deve chamar super.process() e envolvê-lo com comportamento de agendamento, criando um acoplamento forte. A classe herda refund() da entidade principal, mas o reembolso de assinaturas requer uma lógica diferente de pagamentos únicos. Alterações em CreditCardPayment afetar Pagamento Recorrente com Cartão de Crédito mesmo quando essas alterações são irrelevantes para o agendamento.

✅ Compatível:

class CreditCardPayment extends Payment {
    constructor(amount, currency, cardToken, billingAddress) {
        super(amount, currency);
        this.cardToken = cardToken;
        this.billingAddress = billingAddress;
    }

    async process() {
        await this.validateCard();
        return await stripe.charges.create({
            amount: this.amount * 100,
            source: this.cardToken,
            currency: this.currency
        });
    }

    async refund() {
        await this.validateRefund();
        return await stripe.refunds.create({ charge: this.chargeId });
    }

    async validateCard() {
        // Card validation logic
    }
}

class RecurringCreditCardPayment {
    constructor(creditCardPayment, schedule) {
				this.creditCardPayment = creditCardPayment;
        this.schedule = schedule;
    }

    async scheduleNextPayment() {
        this.schedule.onNextCyle(() => {
	        await this.creditCardPayment.process();
        })
    }
}

const recurringCreditCardPayment = new RecurringCreditCardPayment(
	new CreditCardPayment(),
	new Schedule(),
);

Por que isso importa: Pagamento Recorrente com Cartão de Crédito foca exclusivamente no agendamento e delega o processamento de pagamentos ao composto CreditCardPayment instância. Nenhuma herança significa nenhum acoplamento forte à implementação da classe pai. Alterações no processamento de cartão de crédito não afetam a lógica de agendamento. A instância de pagamento pode ser substituída por qualquer método de pagamento sem alterar o código de agendamento.

Conclusão

Utilize composição para separar responsabilidades em vez de misturá-las através de herança. Quando uma classe precisa da funcionalidade de outra classe, aceite-a como uma dependência e delegue a ela, em vez de herdar dela. Isso cria acoplamento fraco, facilita os testes e evita que alterações em uma classe quebrem outra.

FAQs

Dúvidas?

Quando devo usar herança vs. composição?

Utilize herança apenas para verdadeiras relações 'é-um', onde a subclasse é genuinamente uma versão especializada do pai. Um Quadrado estendendo Retângulo faz sentido se quadrados são retângulos em seu domínio. Utilize composição para relações 'tem-um' ou 'usa-um'. Um pagamento recorrente usa um processador de pagamento, não é um tipo de processador de pagamento. Em caso de dúvida, favoreça a composição.

E se eu precisar reutilizar código de múltiplas fontes?

A composição lida com isso naturalmente através de múltiplas dependências. Uma classe pode compor um processador de pagamentos, um agendador e um notificador sem lutar contra as restrições de herança múltipla. A herança força você a usar linguagens de herança única ou hierarquias complexas de herança múltipla. A composição é mais clara: cada dependência é explícita no construtor.

Como refatorar herança para composição?

Identifique o que a subclasse realmente faz versus o que ela herda. No exemplo, RecurringCreditCardPayment agenda pagamentos, mas herda a lógica de processamento. Extraia a funcionalidade pai para uma classe separada e, em seguida, passe-a como uma dependência. Substitua extends Parent por um parâmetro de construtor. Substitua as chamadas super.method() por this.dependency.method(). Teste cada etapa.

A composição não cria mais boilerplate?

A configuração inicial exige dependências explícitas, mas essa clareza é valiosa. Você vê exatamente o que cada classe precisa sem ter que procurar em hierarquias pai. Frameworks modernos de injeção de dependência reduzem o boilerplate. A explicitude previne bugs de comportamento herdado implícito. Algumas linhas extras de código de configuração valem a flexibilidade e a manutenibilidade.

E quanto a classes base abstratas e interfaces?

Interfaces são excelentes para definir contratos sem acoplar implementações. Use interfaces para especificar qual comportamento uma classe precisa, e então injete implementações concretas. Classes abstratas são apenas herança com alguns métodos não implementados, elas têm os mesmos problemas de acoplamento. Prefira interfaces com composição em vez de classes abstratas com herança.

Como lidar com métodos utilitários compartilhados?

Extraia-os para classes de utilidade ou serviços separados. Em vez de herdar lógica de validação compartilhada, injete um serviço `Validator`. No exemplo, se pagamentos únicos e recorrentes precisam da mesma validação, crie um `PaymentValidator` compartilhado que ambos possam usar através de composição. Isso torna a lógica compartilhada mais fácil de descobrir e testar do que métodos ocultos em classes pai.

Fique seguro agora

Proteja seu código, Cloud e runtime em um único sistema centralizado.
Encontre e corrija vulnerabilidades rapidamente de forma automática.

Não é necessário cartão de crédito | Resultados da varredura em 32 segundos.