Back-End

2 jun, 2017

Action injection – resposta certa ou sujeira no código?

Publicidade

 

As circunstâncias conspiraram para colocar discussões sobre Action Injection na minha frente várias vezes nas últimas semanas. Como já vi a abordagem várias vezes antes, eu tendo a pensar que se Action Injection é a resposta, você provavelmente está fazendo a pergunta errada. Eu vejo como uma sujeira no código, como um indicador que o sistema precisa de refatoração ou reorganização.

Action Injection

Até onde sei, o termo “Action Injection” originou-se com Alex Meyer-Gleaves em um artigo de 2010 sobre desenvolvimento ASP.NET MVC. Ele resume a Action Injection assim:

Seu constructor (classe controller) é fornecido pelas dependências (pelo container DI) que são compartilhadas por todas as ações no seu controller, cada ação individual (method) pode fazer a requisição de qualquer dependências adicionais que forem necessárias.

Para falar mais sobre isso, digamos que você tenha uma classe controller com vários action methods nela. Você se dá conta depois de um tempo que os diferentes action methods têm dependências ligeiramente diferentes. Por exemplo, alguns dos methods precisam de um logger, enquanto outros precisam de um sistema template, ou acesso ao roteador. Mas você não quer poluir o constructor da classe controller com essas dependências específicas do method, uma vez que elas serão usadas somente se esse action method em particular for invocado.

Com a Action Injection, quando você faz o pull do controller de seu contêiner de injeção de dependência e chama um action method específico, o contêiner de DI passará automaticamente as dependências corretas para os argumentos de chamada de método. Pronto: agora você pode definir todas as dependências comuns como parâmetros no constructor do controller e todas as dependências específicas de ação como parâmetros na definição do método.

Você pode ver uma descrição específica para PHP do problema, com Action Injection como a solução, neste pull request do Symfony.

Você também pode ouvir sobre isso nesta apresentação de Beau Simensen, entre os minutos 32:21 até 34:31:

Os contêineres Yii e Laravel parecem suportar esse comportamento também, usando o termo “Method Injection”. (Eu acho que é um termo equivocado; o termo aparece sobrecarregado, na melhor das hipóteses, como às vezes pode significar “métodos chamados pelo contêiner de DI em tempo de criação objeto” em vez de “argumentos de resolução de método em tempo de chamada” .) Talvez outros contêineres de DI também suportem Action Injection.

…como uma sujeira no código

A razão explícita para usar a Action Injection é “reduzir dependências ou sobrecarga” ao construir um objeto. Você não quer ter que entregar uma dúzia de dependências, quando apenas três são usadas em cada método e as outras são usadas apenas em métodos específicos.

Mas o fato de que seu controller tenha tantas dependências, usadas apenas em alguns casos e não em outros, deve ser um indicador de que a classe está fazendo muitas delas. Na verdade, está fazendo tantas que você não pode chamar seus métodos de ação diretamente; você tem que usar o contêiner de injeção de dependência não apenas para construir o objeto controller, mas também para invocar seus métodos de ação.

Reorganização para evitar Action Injection

Quais abordagens existentes para ajudá-lo a evitar Action Injection no seu código? Afirmo que a melhor solução é mudar a forma como você organiza suas estruturas do controller.

Em vez de pensar em termos de “uma classe de controller com action methods”, pense em termos de “um namespace de controller com action classes.” As Actions são os destinos de suas rotas de qualquer maneira, não classes do controller por si só. Então, faz sentido atualizar Actions para elementos de “primeira classe” do sistema. (Pense neles como controller de Actions únicas, se você quiser.)

Assim, em vez de…

<?php
namespace App;

class BlogController {
    public function browse() { ... }
    public function read() { ... }
    public function edit() { ... }
    public function add() { ... }
    public function delete() { ... }
}

…reorganize para:

<?php
namespace App\BlogController;

class Browse { ... }
class Read { ... }
class Edit { ... }
class Add { ... }
class Delete { ... }

Em seguida, o contêiner de DI pode usar a boa e velha constructor injection para criar o objeto da Action, com todas as suas dependências particulares. Para invocar a Action, chame um método bem conhecido com a entrada do usuário da rota ou da requisição. (Eu gosto do __invoke(), mas outros podem preferir exec() ou algo semelhante.)

Na verdade, eu percebi apenas depois de assistir o vídeo do Simensen uma segunda vez que seu exemplo é um controller de single-action em tudo, menos no nome. Seu código de exemplo era este…

<?php
class Home {
    /**
     * @Route("/myaction", name="my_action")
     */
    public function myAction(
        Request $request,
        Router $router,
        Twig $twig
    ) {
        if (!$request->isMethod('GET')) {
            return new RedirectResponse(
                $router->generateUrl('my_action'),
                301
            );
        }

        return new Response(
            $twig->render('mytemplate.html.twig')
        );
    }
}

…mas os parâmetros do action method do controller podem muito bem ser parâmetros do constructor da action class:

<?php
namespace Home;

class MyAction {

    public function __construct(
        Request $request,
        Router $router,
        Twig $twig
    ) {
        $this->request = $request;
        $this->router = $router;
        $this->twig = $twig;
    }

    /**
     * @Route("/myaction", name="my_action")
     */
    public function __invoke() {
        if (!$this->request->isMethod('GET')) {
            return new RedirectResponse(
                $this->router->generateUrl('my_action'),
                301
            );
        }

        return new Response(
            $this->twig->render('mytemplate.html.twig')
        );
    }
}

(ATUALIZAÇÃO: Esse código de exemplo parece ter origem em Kevin Dunglas e seu DunglasActionBundle – que por si próprio é uma implementação de controller de single-action para o Symfony.)

Action Domain Responder

Para mais informações sobre esta estrutura organizacional, leia o que ofereço no Action Domain Responder. ADR (Action Domain Responder) é um refinamento do MVC que é sintonizado especificamente para interações request/response na rede do lado do servidor.

Mas você não precisa do ADR completo para evitar a Action Injection. Basta usar controllers de single-action para fazer o truque. Em seguida, você pode ter classes de controller de single-responsibility bem fatoradas que não exigem um contêiner de DI para chamar seus action methods e aí Action Injection torna-se coisa do passado.

 

***

Paul M. Jones faz parte do time de colunistas internacionais do iMasters. A tradução do artigo é feita pela Redação iMasters, com autorização do autor, e você pode acompanhar o artigo em inglês no link: http://paul-m-jones.com/archives/6589