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:
- Validar o pedido
- Processar o pagamento
- 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
}
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);
}
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;
}
}
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;
}
}
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;
}
}
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;
}
}
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);
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'}
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
}
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;
}
}
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;
}
}
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'}
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.
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)