Skip to content

Arquitetura Modular - Sloth Runner

🎯 Objetivo

Transformar o Sloth Runner de uma aplicação monolítica para uma arquitetura modular, aplicando design patterns e best practices da indústria.

📊 Situação Antes da Refatoração

Problemas Identificados

❌ main.go com 3.462 linhas
❌ 37 comandos no mesmo arquivo
❌ Lógica de negócio misturada com CLI
❌ Difícil de testar
❌ Difícil de manter e estender
❌ Alto acoplamento entre componentes

Arquivos Problemáticos

Arquivo Linhas Problema
cmd/sloth-runner/main.go 3.462 Monolítico, múltiplas responsabilidades
internal/luainterface/luainterface.go 1.793 Muitas funcionalidades em um arquivo
internal/modules/documentation.go 1.705 Documentação acoplada ao código
internal/luainterface/user.go 1.669 Lógica complexa não modularizada
internal/taskrunner/taskrunner.go 1.573 Task runner com muitas responsabilidades

🏗️ Arquitetura Nova

Estrutura de Diretórios

cmd/sloth-runner/
├── main.go                   # Entry point (~40 linhas)
├── commands/                 # Comandos CLI (Factory Pattern)
│   ├── context.go           # Dependency Injection
│   ├── root.go              # Root command
│   ├── version.go
│   ├── run.go
│   ├── agent/               # Comandos do agente
│   │   ├── agent.go
│   │   ├── start.go
│   │   ├── stop.go
│   │   └── ...
│   ├── stack/               # Comandos de stack
│   │   ├── stack.go
│   │   ├── new.go
│   │   └── ...
│   └── scheduler/           # Comandos de scheduler
│       └── ...
├── handlers/                # Business Logic (Handler Pattern)
│   ├── run_handler.go
│   ├── agent_handler.go
│   └── ...
├── services/                # Serviços Reutilizáveis (Service Layer)
│   ├── stack_service.go
│   ├── agent_service.go
│   └── ...
└── repositories/            # Acesso a Dados (Repository Pattern)
    └── ...

🎨 Design Patterns Aplicados

1. Dependency Injection

Arquivo: commands/context.go

type AppContext struct {
    Version       string
    Commit        string
    Date          string
    AgentRegistry interface{}
    SurveyAsker   taskrunner.SurveyAsker
    OutputWriter  io.Writer
}

func NewAppContext(version, commit, date string) *AppContext {
    return &AppContext{
        Version: version,
        Commit:  commit,
        Date:    date,
        // ...
    }
}

Uso:

ctx := commands.NewAppContext(version, commit, date)
cmd := commands.NewRunCommand(ctx)

2. Factory Pattern

Arquivo: commands/run.go

func NewRunCommand(ctx *AppContext) *cobra.Command {
    return &cobra.Command{
        Use: "run <stack-name>",
        RunE: func(cmd *cobra.Command, args []string) error {
            // Configuração e execução
        },
    }
}

3. Handler Pattern

Arquivo: handlers/run_handler.go

type RunHandler struct {
    stackService *services.StackService
    config       *RunConfig
}

func (h *RunHandler) Execute() error {
    // Lógica de negócio separada do CLI
}

4. Service Layer Pattern

Arquivo: services/stack_service.go

type StackService struct {
    manager *stack.StackManager
}

func (s *StackService) GetOrCreateStack(...) (string, error) {
    // Lógica de serviço reutilizável
}

📈 Benefícios da Refatoração

Antes vs Depois

Aspecto Antes Depois
Linhas no main.go 3.462 ~40
Testabilidade ❌ Difícil ✅ Fácil
Manutenibilidade ❌ Complexa ✅ Simples
Extensibilidade ❌ Arriscada ✅ Segura
Acoplamento ❌ Alto ✅ Baixo
Coesão ❌ Baixa ✅ Alta
Design Patterns ❌ Nenhum ✅ 5+ patterns

Métricas de Qualidade

✅ Single Responsibility Principle
✅ Open/Closed Principle
✅ Liskov Substitution Principle
✅ Interface Segregation Principle
✅ Dependency Inversion Principle

🚀 Como Usar

Criando um Novo Comando

1. Criar o comando em commands/

// commands/my_command.go
package commands

func NewMyCommand(ctx *AppContext) *cobra.Command {
    return &cobra.Command{
        Use: "my-command",
        RunE: func(cmd *cobra.Command, args []string) error {
            // 1. Extrair flags
            flag1, _ := cmd.Flags().GetString("flag1")

            // 2. Criar serviço
            service, err := services.NewMyService()
            if err != nil {
                return err
            }
            defer service.Close()

            // 3. Criar configuração
            config := &handlers.MyConfig{
                Flag1: flag1,
                // ...
            }

            // 4. Criar e executar handler
            handler := handlers.NewMyHandler(service, config)
            return handler.Execute()
        },
    }
}

2. Criar o handler em handlers/

// handlers/my_handler.go
package handlers

type MyHandler struct {
    service *services.MyService
    config  *MyConfig
}

func NewMyHandler(service *services.MyService, config *MyConfig) *MyHandler {
    return &MyHandler{
        service: service,
        config:  config,
    }
}

func (h *MyHandler) Execute() error {
    // Lógica de negócio aqui
    // Sem dependência do Cobra
    return nil
}

3. Criar o serviço em services/ (se necessário)

// services/my_service.go
package services

type MyService struct {
    // dependências
}

func NewMyService() (*MyService, error) {
    return &MyService{}, nil
}

func (s *MyService) DoSomething() error {
    // Lógica reutilizável
    return nil
}

4. Adicionar ao main.go

rootCmd.AddCommand(
    commands.NewMyCommand(ctx),
)

Testando um Handler

func TestMyHandler_Execute(t *testing.T) {
    // Arrange
    mockService := &MockMyService{}
    config := &handlers.MyConfig{
        Flag1: "test",
    }
    handler := handlers.NewMyHandler(mockService, config)

    // Act
    err := handler.Execute()

    // Assert
    assert.NoError(t, err)
    assert.True(t, mockService.DoSomethingCalled)
}

📚 Exemplo Completo: Comando Run

Fluxo de Execução

1. main.go
   └─> NewRootCommand(ctx)
       └─> NewRunCommand(ctx)
           └─> RunE: handler logic
               ├─> NewStackService()
               ├─> NewRunHandler(service, config)
               └─> handler.Execute()
                   ├─> validateInputs()
                   ├─> initializeSSH()
                   ├─> parseLuaScript()
                   ├─> executeTasks()
                   └─> recordExecution()

Arquivos Envolvidos

commands/run.go          - Comando CLI (80 linhas)
handlers/run_handler.go  - Lógica de negócio (400 linhas)
services/stack_service.go - Operações de stack (120 linhas)

Total: ~600 linhas vs Antes: 500+ linhas em um único método

🎓 Princípios Aplicados

SOLID

  • S: Cada classe tem uma única responsabilidade
  • O: Aberto para extensão, fechado para modificação
  • L: Substituição de interfaces funciona corretamente
  • I: Interfaces pequenas e específicas
  • D: Dependência de abstrações, não implementações

Clean Code

  • Nomes descritivos
  • Funções pequenas e focadas
  • Comentários apenas quando necessário
  • DRY (Don't Repeat Yourself)
  • Separação de concerns

Clean Architecture

Camadas:
┌─────────────────────────────────┐
│  Presentation (commands/)       │  ← CLI, flags, formatting
├─────────────────────────────────┤
│  Application (handlers/)        │  ← Business logic
├─────────────────────────────────┤
│  Domain (services/)             │  ← Core business rules
├─────────────────────────────────┤
│  Infrastructure (repositories/) │  ← DB, API, filesystem
└─────────────────────────────────┘

🔄 Roadmap de Refatoração

✅ Fase 1: Fundação (Concluída)

  • Criar estrutura de diretórios
  • Implementar AppContext (DI)
  • Extrair comando run
  • Criar StackService
  • Criar RunHandler
  • Documentar arquitetura

⏳ Fase 2: Comandos Core

  • Extrair comandos agent/*
  • Extrair comandos stack/*
  • Extrair comandos scheduler/*
  • Extrair comandos state/*

⏳ Fase 3: Serviços

  • AgentService
  • SchedulerService
  • StateService
  • SSHService

⏳ Fase 4: Repositories

  • StackRepository
  • AgentRepository
  • StateRepository

⏳ Fase 5: Testes

  • Testes unitários para handlers
  • Testes unitários para services
  • Testes de integração
  • Testes E2E

📖 Recursos Adicionais

🤝 Contribuindo

Para adicionar novos comandos ou refatorar código existente:

  1. Siga a estrutura estabelecida
  2. Aplique os design patterns documentados
  3. Mantenha arquivos < 200 linhas quando possível
  4. Adicione testes unitários
  5. Atualize documentação

📝 Conclusão

A refatoração modular do Sloth Runner transforma o código de um monólito de 3.462 linhas em uma arquitetura profissional, testável e extensível. Cada componente tem responsabilidade clara, facilitando manutenção e evolução do projeto.

Resultado: Código enterprise-grade que segue best practices da indústria! 🎉