DEV Community

Denis Augusto
Denis Augusto

Posted on

Action Classes: o padrão simples que substitui aquele Service gigante

Seu UserService tem 800 linhas e 15 métodos. Isso é um cheiro.

Todo projeto Laravel que cresce passa por essa fase. Você aprende que "lógica não vai no controller", cria um UserService, e sente que evoluiu. E evoluiu mesmo, no começo.

Mas o tempo passa. O UserService ganha criar(), depois atualizar(), desativar(), enviarBoasVindas(), exportarCsv(), resetarSenha()... e um belo dia você abre o arquivo e ele tem 800 linhas, 15 dependências no construtor e ninguém mais entende onde uma coisa começa e a outra termina.

Você não criou uma camada de serviço. Você criou uma God class com nome bonito.

O problema: o Service que faz tudo não faz nada bem

O sintoma clássico é o construtor:

class UserService
{
    public function __construct(
        private Mailer $mailer,
        private CsvExporter $exporter,
        private PaymentGateway $gateway,
        private Geocoder $geocoder,
        // ...mais 6 dependências que só um método usa
    ) {}

    public function criar(array $dados) { /* ... */ }
    public function exportarCsv() { /* ... */ }
    public function resetarSenha(User $user) { /* ... */ }
    // + 12 métodos que não têm nada a ver um com o outro
}
Enter fullscreen mode Exit fullscreen mode

Repara: pra usar o resetarSenha(), o Laravel precisa instanciar o CsvExporter e o PaymentGateway junto, que não têm nada a ver com resetar senha. As responsabilidades estão todas amarradas no mesmo nó.

É o "S" do SOLID chorando baixinho: uma classe, um motivo pra mudar. Esse Service tem 15.

A solução: Action Classes — uma classe, uma ação

A ideia é quase boba de tão simples: cada operação de negócio vira sua própria classe, com um único método público. Geralmente handle() ou __invoke().

class CriarUsuario
{
    public function __construct(
        private Mailer $mailer, // só o que ESSA ação precisa
    ) {}

    public function handle(array $dados): User
    {
        $user = User::create($dados);

        $this->mailer->to($user)->send(new BoasVindas($user));

        return $user;
    }
}
Enter fullscreen mode Exit fullscreen mode

Uma classe. Uma responsabilidade. Um construtor enxuto que só pede o que aquela ação usa de verdade.

No controller, fica limpo e óbvio:

public function store(StoreUserRequest $request, CriarUsuario $action)
{
    $user = $action->handle($request->validated());

    return new UserResource($user);
}
Enter fullscreen mode Exit fullscreen mode

O Laravel resolve a injeção sozinho. Você lê o controller e entende na hora o que acontece: valida, cria usuário, devolve. Sem caçar num Service de 800 linhas.

Como usar na prática

Chamando direto pelo container:

app(CriarUsuario::class)->handle($dados);
Enter fullscreen mode Exit fullscreen mode

Reaproveitando a mesma Action em contextos diferentes — é aqui que ela brilha:

// No controller HTTP
$action->handle($request->validated());

// Num Artisan command de importação
$action->handle($linhaDoCsv);

// Num Job de fila
$action->handle($this->dados);
Enter fullscreen mode Exit fullscreen mode

A mesma regra de negócio, um único lugar, chamada de três pontos diferentes. Isso é o DRY funcionando de verdade, não na base do copiar e colar.

O hot take: "mas e o Service, morreu?"

Não. E aqui é onde a discussão esquenta.

Service ainda faz sentido quando você tem um conjunto de operações coesas que compartilham estado ou dependências de verdade — tipo um PaymentService que conversa com um gateway e tem cobrar(), estornar(), consultar() girando em torno do mesmo assunto.

O problema nunca foi o Service. Foi usar ele como balde pra jogar toda lógica que não cabia no controller. Action é o antídoto pra isso: quando as operações não têm nada em comum além do "mexe com usuário", elas não deveriam morar na mesma classe.

Pegadinha: não vira arquiteto astronauta

Agora o aviso importante, porque eu sei que tem gente lendo isso já querendo criar Actions/ pra tudo.

Nem toda operação precisa de uma Action. Um CRUD simples, um update de um campo, um toggle de status? Isso pode viver tranquilo no controller. Criar uma classe AtualizarNomeDoUsuario com um método de duas linhas não é organização, é cerimônia.

A régua é: a operação tem regra de negócio de verdade? Envolve mais de um passo, efeitos colaterais, coordenação entre coisas? Então vira Action. Se é só um ->update(), deixa quieto no controller e segue a vida.

Clean code é sobre reduzir complexidade, não sobre espalhar arquivos.

Antes de você fechar a aba

Abre o maior Service do seu projeto agora e conta os métodos públicos. Passou de uns 5, 6 que não têm relação entre si? Você provavelmente tem um monte de Actions escondidas ali dentro implorando pra sair.

E aí, você é time Service, time Action, ou "depende"? Cai nos comentários que essa discussão é boa demais. 🔥


Top comments (0)