DEV Community

Cover image for Entendendo o Design Pattern Pipeline na Prática
Wagner Negrão 👨‍🔧
Wagner Negrão 👨‍🔧

Posted on • Edited on

Entendendo o Design Pattern Pipeline na Prática

Depois de um tempo sem escrever, volto com um tema muito útil para quem trabalha com processamento em etapas: o design pattern Pipeline.

O que é o Pipeline?

O padrão Pipeline é um tipo de padrão estrutural. Ele define uma sequência de etapas (ou estágios) para processar dados de forma modular e flexível.
É ideal para cenários em que você precisa executar várias validações ou transformações em uma ordem específica.

Um exemplo simples

Imagine um processo de compra online com as seguintes etapas:

  1. Validar o pedido
  2. Processar o pagamento
  3. Enviar o produto

Cada etapa depende da anterior. Por isso, precisamos garantir que elas ocorram na ordem certa e o Pipeline ajuda exatamente nisso.
Ao separar cada etapa em blocos, conseguimos manter o código limpo, reutilizável e fácil de manter.

Implementando o Pipeline em Java

O padrão Pipeline pode ser implementado de várias formas. Vamos começar com uma abordagem usando interfaces.

1. Criando a entidade Order

public class Order {
    String orderId;
    double amount;
    String status;
// constructor
// getter
// setter
}
Enter fullscreen mode Exit fullscreen mode

2. Criando a interface de processamento

Vamos criar a interface OrderProcessingStage, que define o método process, responsável por executar a lógica de cada etapa do pipeline.

public interface OrderProcessingStage {
    Order process(Order order);
}
Enter fullscreen mode Exit fullscreen mode

3. Criando o pipeline

Em seguida, criamos a classe OrderProcessingPipeline, que possui dois métodos principais: addStage, para adicionar as etapas à lista de processamento, e execute, que percorre cada etapa executando sua lógica.

public class OrderProcessingPipeline {

    public List<OrderProcessingStage> stages = new ArrayList<>();

    public OrderProcessingPipeline addStages(OrderProcessingStage stage) {
        // Initialize the pipeline with stages
        if (stage != null) {
            stages.add(stage);
        }
        return this;
    }

    public Order execute(Order order) {
        // Execute each stage in the pipeline
        for (OrderProcessingStage stage : stages) {
            try {
                if ("invalid".equalsIgnoreCase(order.getStatus()) || "failed".equalsIgnoreCase(order.getStatus())) {
                    System.out.println("Order processing halted due to invalid status.");
                    break; // Stop processing if the order is invalid
                }

                if (stage.validate(order)) {
                    order = stage.process(order);
                }

            } catch (Exception e) {
                System.err.println("Error processing order at stage: " + stage.getClass().getSimpleName());
            }
        }
        // Return the final order after processing through all stages
        return order;
    }
}

Enter fullscreen mode Exit fullscreen mode

4. Criando os estágios

Até aqui, já criamos a entidade Order, a interface OrderProcessingStage (que funciona como um contrato para as etapas) e a classe OrderProcessingPipeline, que gerencia o fluxo. Agora, vamos implementar.

4.1 Validação do pedido

public class ValidationStage implements OrderProcessingStage {
    @Override
    public Order process(Order order) {
        // Validate the order
        if (order.getAmount() <= 0) {
            order.setStatus("Invalid");
            System.out.println("Order validation failed: Amount must be greater than zero.");
            throw new IllegalArgumentException("Order amount must be greater than zero.");
        }

        order.setStatus("Validated");
        System.out.println("Order validation passed: " + order);

        // If validation passes, return the order for the next stage
        return order;
    }
}
Enter fullscreen mode Exit fullscreen mode

4.1 Pagamento

public class PaymentStage implements OrderProcessingStage {
    @Override
    public Order process(Order order) {
        // Simulate payment processing
        if ("Invalid".equalsIgnoreCase(order.getStatus())) {
            order.setStatus("failed");
            System.out.println("Payment processing failed: Amount must be greater than zero.");
            throw new IllegalArgumentException("Order amount must be greater than zero for payment.");
        }

        // Assuming payment is successful
        order.setStatus("paid");
        System.out.println("Payment processed successfully for order: " + order);

        // If payment is successful, return the order for the next stage
        return order;
    }
}
Enter fullscreen mode Exit fullscreen mode

4.1 Envio

public class ShippingStage implements OrderProcessingStage {
    @Override
    public Order process(Order order) {
        // Simulate shipping processing
        if ("paid".equalsIgnoreCase(order.getStatus())) {
            order.setStatus("shipped");
            System.out.println("Order shipped successfully: " + order);
        } else {
            System.out.println("Shipping failed: Order must be paid before shipping.");
            throw new IllegalStateException("Order must be paid before shipping.");
        }

        // If shipping is successful, return the order for the next stage
        return order;
    }
}
Enter fullscreen mode Exit fullscreen mode

5. Executando o pipeline

Order order = new Order("12345", 100.0);

OrderProcessingPipeline pipeline = new OrderProcessingPipeline()
                .addStages(new ValidationStage())
                .addStages(new PaymentStage())
                .addStages(new ShippingStage());
pipeline.execute(order);

Enter fullscreen mode Exit fullscreen mode

Resultado no console

Order validation passed: Order{orderId='12345', amount=100.0, status='Validated'}
Payment processed successfully for order: Order{orderId='12345', amount=100.0, status='paid'}
Order shipped successfully: Order{orderId='12345', amount=100.0, status='shipped'}

Enter fullscreen mode Exit fullscreen mode

Variação com Classe Abstrata e Controle de Fluxo

Agora, vamos deixar nosso Pipeline mais robusto. Vamos:

  • Usar uma classe abstrata em vez de interface
  • Adicionar controle para etapas opcionais
  • Validar antes de processar

1. Nova classe abstrata

Agora vamos criar a classe abstrata OrderProcessingStage, que trará mais flexibilidade ao pipeline. Ela contará com três parâmetros principais:

  • nameStep: nome da etapa (usado para logs e métricas);
  • descriptionStep: descrição da etapa (também útil para observabilidade);
  • byPass: um booleano que indica se o pipeline pode continuar mesmo que essa etapa falhe.

Além disso, introduziremos o método validate, que retorna um valor booleano. Ele é responsável por verificar se os dados estão corretos antes de prosseguir para a execução do process. Essa abordagem ajuda a reduzir erros e garante que cada etapa só seja processada quando as condições necessárias forem atendidas.

public abstract class OrderProcessingStage {

    private String nameStep;
    private String descriptionStep;
    private Boolean byPass;

    protected OrderProcessingStage(String nameStep, String descriptionStep, Boolean byPass) {
        this.nameStep = nameStep;
        this.descriptionStep = descriptionStep;
        this.byPass = byPass == null ? false : byPass;
    }
    abstract Order process(Order order);
    abstract Boolean validate(Order order);
// getter setter
}

Enter fullscreen mode Exit fullscreen mode

2. Atualizando o Pipeline


public class OrderProcessingPipeline {

    public List<OrderProcessingStage> stages = new ArrayList<>();

    public OrderProcessingPipeline addStages(OrderProcessingStage stage) {
        // Initialize the pipeline with stages
        if (stage != null) {
            stages.add(stage);
        }
        return this;
    }

    public Order execute(Order order) {
        // Execute each stage in the pipeline
        for (OrderProcessingStage stage : stages) {
            try {
                System.out.println("Dados do estágio: " + stage.getNameStep() + ", Descrição: " + stage.getDescriptionStep());
                if (stage.validate(order)) {
                    order = stage.process(order);
                    continue;
                }

                if (!stage.validate(order) && !stage.getByPass()) {
                    System.out.println("Order processing halted due to invalid status.");
                    break; // Stop processing if the order is invalid
                }

            } catch (Exception e) {
                System.err.println("Error processing order at stage: " + stage.getClass().getSimpleName() + " - step: " + stage.getNameStep());
            }
        }
        // Return the final order after processing through all stages
        return order;
    }
}


Enter fullscreen mode Exit fullscreen mode

3. Etapas com validação


public class ValidationStage extends OrderProcessingStage {

    public ValidationStage() {
        super("validation_stage", "Stage to validate the order", false);
    }

    @Override
    public Order process(Order order) {
        // Validate the order
        if (order.getAmount() <= 0) {
            order.setStatus("Invalid");
            System.out.println("Order validation failed: Amount must be greater than zero.");
            throw new IllegalArgumentException("Order amount must be greater than zero.");
        }

        order.setStatus("Validated");
        System.out.println("Order validation passed: " + order);

        // If validation passes, return the order for the next stage
        return order;
    }

    @Override
    public Boolean validate(Order order) {
        return order != null;
    }
}

Enter fullscreen mode Exit fullscreen mode

Outras etapas seguem o mesmo padrão

Logs com sucesso na validação:

Dados do estágio: validation_stage, Descrição: Stage to validate the order
Order validation passed: Order{orderId='12345', amount=100.0, status='Validated'}
Dados do estágio: payment_stage, Descrição: Stage to process payment for the order
Payment processed successfully for order: Order{orderId='12345', amount=100.0, status='paid'}
Dados do estágio: shipping_stage, Descrição: Stage to ship the order
Order shipped successfully: Order{orderId='12345', amount=100.0, status='shipped'}

Enter fullscreen mode Exit fullscreen mode

Como podemos observar nos logs, o nome e a descrição de cada etapa foram exibidos corretamente, e todas as etapas foram executadas com sucesso, seguindo a ordem definida.

Agora, vamos simular uma situação em que o pedido (order) está nulo e analisar o que acontece.

Logs com falha de validação:

Dados do estágio: validation_stage, Descrição: Stage to validate the order
Order processing halted due to invalid status.

Enter fullscreen mode Exit fullscreen mode

Nesse cenário, o método validate identificou que os dados estavam inválidos e, por isso, interrompeu o fluxo. Como a primeira etapa não permite byPass, o pipeline foi encerrado imediatamente, sem continuar para os próximos estágios.

Antes de escolher um padrão de projeto, entenda profundamente o problema que você está tentando resolver. Nem sempre aplicar um design pattern é a melhor solução muitas vezes, seguir os princípios do SOLID já garante uma arquitetura bem organizada e sustentável.

No entanto, se optar por aplicar um padrão como o Pipeline, é importante considerar os seguintes pontos:

Vantagens do Pipeline

✅ Separação de responsabilidades
✅ Fácil manutenção e testes
✅ Reutilização de etapas
✅ Flexibilidade para reorganizar o fluxo

Desvantagens

❌ Mais código para manter
❌ Pode ter impacto de performance em grandes volumes
❌ Tratamento de erro entre estágios pode ser mais difícil

Top comments (0)