Abstração de classes em um sistema de pagamento com suporte a múltiplos métodos

Boa noite.

Quero implementar um módulo de pagamento que aceita múltiplos métodos de pagamento, como PIX, Cartão de Crédito, Boleto, etc. Em um primeiro momento, pensei em usar polimorfismo, com uma interface PaymentMethod expondo um método process que faz o processamento da solicitação.

O problema é que cada método de pagamento recebe dados diferentes: O PIX recebe apenas o valor; O cartão de crédito recebe os dados do cartão junto ao valor; O boleto recebe os dados do consumidor. Sem contar com o tipo de retorno: O PIX retorna um QrCode; O cartão de crédito retorna se o pagamento foi bem sucedido ou não…

Sabendo que métodos de pagamento diferentes são práticamente classes totalmente diferentes, minha principal dúvida é: como vocês fariam essa abstração? Eu quero esconder os detalhes de implementação do pagamento e deixar mais fácil de adicionar novos métodos conforme for necessário.

Outra dúvida que tenho é de como modelar uma entidade Pagamento no banco de dados, de forma que represente um pagamento independentemente do método

Sua ideia está no caminho certo.

Eu criaria 3 interfaces: PaymentMethod, PaymentRequest e PaymentResponse:

interface PaymentMethod {

    PaymentResponse process(PaymentRequest request);
}

Assim cada tipo de pagamento teria sua implementação de PaymentMethod:

public class PixPayment implements PaymentMethod {
    @Override
    public PixPaymentResponse process(PaymentRequest request) {
        PixPaymentRequest pixRequest = (PixPaymentRequest) request;
        // Lógica para processar pagamento via PIX
        return new PixPaymentResponse(/* parâmetros necessários */);
    }
}

public class CreditCardPayment implements PaymentMethod {
    @Override
    public CreditCardPaymentResponse process(PaymentRequest request) {
        CreditCardPaymentRequest ccRequest = (CreditCardPaymentRequest) request;
        // Lógica para processar pagamento via Cartão de Crédito
        return new CreditCardPaymentResponse(/* parâmetros necessários */);
    }
}

public class BoletoPayment implements PaymentMethod {
    @Override
    public BoletoPaymentResponse process(PaymentRequest request) {
        BoletoPaymentRequest boletoRequest = (BoletoPaymentRequest) request;
        // Lógica para processar pagamento via Boleto
        return new BoletoPaymentResponse(/* parâmetros necessários */);
    }
}

Assim como cada tipo também teria suas respectivas implementações de PaymentRequest e PaymentResponse:

// PIX
public class PixPaymentRequest extends PaymentRequest {
    private double amount;
    // Getters e Setters
}

public class PixPaymentResponse extends PaymentResponse {
    private String qrCode;
    // Getters e Setters
}

// Cartão de Crédito
public class CreditCardPaymentRequest extends PaymentRequest {
    private double amount;
    private String cardNumber;
    private String expirationDate;
    private String cvv;
    // Getters e Setters
}

public class CreditCardPaymentResponse extends PaymentResponse {
    private boolean success;
    private String transactionId;
    // Getters e Setters
}

// Boleto
public class BoletoPaymentRequest extends PaymentRequest {
    private double amount;
    private String consumerName;
    private String consumerCpf;
    // Getters e Setters
}

public class BoletoPaymentResponse extends PaymentResponse {
    private String boletoUrl;
    // Getters e Setters
}

Você pode ter uma Factory para obter as instâncias de acordo com o tipo de pagamento:

public class PaymentMethodFactory {
    public static PaymentMethod createPaymentMethod(PaymentType type) {
        switch (type) {
            case PIX:
                return new PixPayment();
            case CREDIT_CARD:
                return new CreditCardPayment();
            case BOLETO:
                return new BoletoPayment();
            default:
                throw new IllegalArgumentException("Método de pagamento não suportado");
        }
    }
}

public enum PaymentType {
    PIX, CREDIT_CARD, BOLETO;
}

A entidade Pagamento deve ter campos comuns a todos os métodos de pagamento, além de um campo que identifique o tipo de método e possivelmente um campo JSON para armazenar dados específicos de cada método:

@Entity
public class Pagamento {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private double amount;
    private String status;
    private PaymentType paymentType;
    @Lob
    private String paymentDetails; // Armazena dados específicos de cada método como JSON

    // Getters e Setters
}

O seu serviço de pagamentos poderia ter uma lógica assim:

public class PaymentService {
    public PaymentResponse processPayment(PaymentType type, PaymentRequest request) {
        PaymentMethod paymentMethod = PaymentMethodFactory.createPaymentMethod(type);
        return paymentMethod.process(request);
    }
}
2 curtidas

Obrigado pela resposta, agora está muito mais claro!

Outra dúvida que me veio a cabeça foi o seguinte: a cada pagamento gerado, um registro será criado no banco de dados. Essa lógica deveria vir dentro do process das classes PaymentMethod, ou fora, na classe PaymentService?

Seguindo a idéia do Staroski, a chamada ficaria na Service, chamando uma repository de pagamento antes de retornar, assim, poderia até ter um campo nessa tabela para armazenar o response.

1 curtida