Um tutorial prático mostrando como transformar um fluxo de coleta de dados monolítico em um design modular, testável e extensível.
Digamos que você esteja construindo um CLI que entrevista o usuário, pedindo nome, data de nascimento e número de documento. A versão inicial funciona, mas logo se torna um bloco de código difícil de manter:
- como você adiciona validações sem aumentar a complexidade?
- como você implementa lógica de confirmação e repetição para cada entrada?
1. Cenário Inicial
package main
import (
"fmt"
"log"
"strings"
"time"
"github.com/AlecAivazis/survey/v2"
)
func main() {
var name, dateStr, document string
if err := survey.AskOne(&survey.Input{Message: "Nome:"}, &name); err != nil {
log.Fatal(err)
}
if len(strings.TrimSpace(name)) < 2 {
log.Fatal("O nome deve ter pelo menos 2 caracteres")
}
if err := survey.AskOne(&survey.Input{Message: "Data de nascimento (DD/MM/AAAA):"}, &dateStr); err != nil {
log.Fatal(err)
}
birthDate, err := time.Parse("02/01/2006", dateStr)
if err != nil || birthDate.After(time.Now()) {
log.Fatal("Data de nascimento inválida")
}
if err := survey.AskOne(&survey.Input{Message: "Documento:"}, &document); err != nil {
log.Fatal(err)
}
if len(document) == 0 {
log.Fatal("O documento não pode estar vazio")
}
fmt.Printf("Coletados: %s | %s | %s\n", name, birthDate.Format("2006-01-02"), document)
}
Aqui vemos alguns problemas:
- Dificuldade em adicionar novas etapas: cada novo campo ou validação requer modificar todo o fluxo, aumentando o risco de bugs.
- Regras de validação misturadas com coleta de dados: é difícil isolar ou reutilizar validações.
- Fluxo rígido e inflexível: não é simples reordenar, pular ou repetir etapas sem reescrever código.
- Sem mecanismo de repetição organizado: qualquer erro termina o programa, sem controle refinado de repetição.
- Baixa clareza sobre o fluxo: à medida que o código cresce, torna-se mais difícil entender a ordem e lógica de cada etapa.
2. Raciocínio para Desacoplamento
Queremos dividir o processo em etapas independentes, cada uma responsável por: • Perguntar; • Validar; • Decidir se repete ou avança.
O Padrão Pipeline resolve isso encadeando handlers que compartilham um contexto mutável.
2.1 Por que este padrão?
Quando você precisa executar todas as etapas em uma ordem fixa – mas quer manter cada uma pequena, testável e com suas próprias regras de negócio – o Padrão Pipeline é uma escolha natural. Ele modela o fluxo como uma cadeia de handlers independentes que recebem um contexto compartilhado, realizam seu trabalho e então acionam a próxima etapa.
Principais razões para adotá-lo aqui:
- Sequência garantida – o pipeline garante que
Nome → DataNascimento → Documentosempre aconteça nessa ordem. - Baixo acoplamento – inserir, remover ou reordenar etapas requer apenas ajustar como elas são encadeadas na montagem do pipeline.
- Repetição localizada – cada handler controla sua própria política de repetição sem espalhar loops
for/ifpor todo o código principal. - Foco em responsabilidade única – validação, análise e feedback ao usuário vivem juntos na etapa correta.
- Testabilidade – pequenos handlers podem ser testados com mocks sem executar todo o CLI.
2.2 Componentes Essenciais
- Contexto Compartilhado – estrutura com os campos coletados.
- Interface
Handler– cada etapa implementaExecuteeSetNext. BaseHandler– forneceCallNextpara avançar no pipeline.
3. Ok, mas como fazemos isso?
A chave é tratar cada etapa como uma ação autônoma. Nome, data e documento têm critérios de validação completamente diferentes e às vezes contagens de repetição diferentes. Portanto, cada etapa recebe seu próprio handler, responsável por:
- Perguntar ao usuário pela entrada.
- Aplicar todas as validações específicas para esse campo.
- Gerenciar política de repetição localmente (quantas tentativas, quais mensagens mostrar).
- Se bem-sucedido, preencher o contexto e chamar a próxima etapa.
Esta separação garante regras claras, código pequeno e a capacidade de testar cada peça isoladamente.
3.1 Implementação Base
Primeiro, precisamos de uma estrutura que permita que cada peça se conecte como um “pipeline”. À primeira vista, isso se assemelha ao padrão Chain of Responsibility, mas não estamos aplicando CoR aqui. No CoR, cada etapa escolhe se trata ou delega; no nosso caso, todas as etapas são obrigatórias e executadas sequencialmente – uma característica típica do Padrão Pipeline.
// UserContext mantém o estado entre etapas
type UserContext struct {
Nome string
DataNascimento time.Time
Documento string
}
// Interface comum
type Handler interface {
Execute(ctx *UserContext) error
SetNext(next Handler) Handler
}
// Base para vincular etapas
type BaseHandler struct { next Handler }
func (b *BaseHandler) SetNext(next Handler) Handler { b.next = next; return next }
func (b *BaseHandler) CallNext(ctx *UserContext) error {
if b.next != nil { return b.next.Execute(ctx) }
return nil
}
3.2 Exemplo de NomeHandler
type NomeHandler struct {
BaseHandler
maxRetries int
}
func (h *NomeHandler) Execute(ctx *UserContext) error {
for i := 0; i < h.maxRetries; i++ {
nome, _ := askForInput("Digite seu nome: ")
fmt.Printf("Tentativa %d\n", i+1)
if len(strings.TrimSpace(nome)) < 2 {
fmt.Println("O nome deve ter pelo menos 2 caracteres")
continue
}
ctx.Nome = nome
break
}
if ctx.Nome == "" {
return errors.New("número máximo de tentativas alcançado para entrada de nome")
}
return h.CallNext(ctx)
}
3.3 DataNascimentoHandler
type DataNascimentoHandler struct {
BaseHandler
}
func (h *DataNascimentoHandler) Execute(ctx *UserContext) error {
dataStr, _ := askForInput("Digite sua data de nascimento (DD/MM/AAAA): ")
dataNascimento, err := time.Parse("02/01/2006", dataStr)
if err != nil || dataNascimento.After(time.Now()) {
return fmt.Errorf("data inválida")
}
ctx.DataNascimento = dataNascimento
return h.CallNext(ctx)
}
3.4 DocumentoHandler
type DocumentoHandler struct {
BaseHandler
}
func (h *DocumentoHandler) Execute(ctx *UserContext) error {
doc, _ := askForInput("Digite seu CPF: ")
if len(doc) == 0 {
return errors.New("CPF inválido")
}
ctx.Documento = doc
return h.CallNext(ctx)
}
3.5 Montagem do Pipeline
func main() {
ctx := &UserContext{}
nome := &NomeHandler{maxRetries: 3}
data := &DataNascimentoHandler{}
doc := &DocumentoHandler{}
nome.SetNext(data).SetNext(doc)
if err := nome.Execute(ctx); err != nil {
log.Fatal(err)
}
fmt.Printf("Dados coletados: %+v\n", ctx)
}
4. Conclusão
Refatorar para o Padrão Pipeline transformou um script frágil em um fluxo robusto e modular pronto para crescer.
Código completo
package main
import (
"errors"
"fmt"
"log"
"strings"
"time"
"github.com/AlecAivazis/survey/v2"
)
type UserContext struct {
Name string
BirthDate time.Time
Document string
}
type Handler interface {
Execute(ctx *UserContext) error
SetNext(next Handler) Handler
}
type BaseHandler struct {
next Handler
}
func (h *BaseHandler) SetNext(next Handler) Handler {
h.next = next
return next
}
func (h *BaseHandler) CallNext(ctx *UserContext) error {
if h.next != nil {
return h.next.Execute(ctx)
}
return nil
}
type NameHandler struct {
BaseHandler
maxRetries int
}
func (h *NameHandler) Execute(ctx *UserContext) error {
for i := 0; i < h.maxRetries; i++ {
name, _ := askForInput("Enter your name: ")
fmt.Printf("Attempt %d\n", i+1)
if len(strings.TrimSpace(name)) < 2 {
fmt.Println("Name must have at least 2 characters")
continue
}
ctx.Name = name
break
}
if ctx.Name == "" {
return errors.New("maximum retries reached for name input")
}
return h.CallNext(ctx)
}
type BirthDateHandler struct {
BaseHandler
}
func (h *BirthDateHandler) Execute(ctx *UserContext) error {
dateStr, _ := askForInput("Enter your birth date (MM/DD/YYYY): ")
birthDate, err := time.Parse("01/02/2006", dateStr)
if err != nil || birthDate.After(time.Now()) {
return fmt.Errorf("invalid date")
}
ctx.BirthDate = birthDate
return h.CallNext(ctx)
}
type DocumentHandler struct {
BaseHandler
}
func (h *DocumentHandler) Execute(ctx *UserContext) error {
doc, _ := askForInput("Enter your SSN: ")
if len(doc) == 0 {
return errors.New("invalid SSN")
}
ctx.Document = doc
return h.CallNext(ctx)
}
func main() {
ctx := &UserContext{}
nameHandler := &NameHandler{maxRetries: 3}
birthHandler := &BirthDateHandler{}
docHandler := &DocumentHandler{}
nameHandler.SetNext(birthHandler).SetNext(docHandler)
if err := nameHandler.Execute(ctx); err != nil {
log.Fatal("Pipeline error:", err)
}
fmt.Printf("Collected data: %+v\n", ctx)
}
func askForInput(prompt string) (string, error) {
var result string
q := &survey.Input{Message: prompt}
if err := survey.AskOne(q, &result); err != nil {
return "", err
}
return result, nil
}