Blog and Site, BETA!
Random header image... Refresh for more!

Injeção de Dependências em PHP

Apenas recentemente tive contato com um termo intrigante: Injeção de Dependências (Dependency Injection ou apenas DI). A primeira vez que eu o vi foi ao olhar alguns dos subprojetos do symfony que trata justamente disto. Após isto, vi outra notícia sobre injeção de dependências no Java 6. E isso tudo me deixou com uma pulga atrás da orelha, afinal, o que diabos é injeção de dependências? Para que isto serve? Nos parágrafos abaixo compartilho um pouco do que consegui aprender até agora.

Bem, a primeira coisa que eu descobri é que estamos falando apenas de mais um padrão de projetos. Em poucas palavras: Injeção de Dependências significa tirar de uma classe ou sistema a responsabilidade de instanciar suas próprias dependências [1]. Hmm, agora o termo já passa a fazer algum sentido. Já que o componente não vai mais instanciar, alguém terá que fazê-lo (injetar), certo?

E o que podem ser estas tais dependências e como instanciá-las? Considere que uma classe X precise de outra classe Y para funcionar. Uma maneira intuitiva de implementar X é instanciar um objeto de Y no construtor de X e utilizar os métodos desse objeto no restante do código de X. Se você quiser seguir o padrão de DI, entretanto, será preciso remover a instanciação de Y em X. Existem algumas alternativas para fazer esse desacoplamento:

  • Fazer a injeção no construtor: uma instância de objeto da classe Y é passada como parâmetro do construtor de X.
  • Fazer a injeção através de um setter: adicionamos um (ou vários) métodos à classe X especialmente para a definição de objetos das classes das quais X depende.
  • Utilizar um localizador de serviços: um componente no sistema fica responsável por realizar o mapeamento dos componentes e suas dependências e instanciá-los em tempo de execução.

Resumindo para os mais pragmáticos, encontrei  uma definição mais prática, porém menos genérica, que irá sanar qualquer tipo de dúvida:

“Injeção de Dependências é passar as dependências de um componente através de construtores, métodos ou através da atribuição direta de campos”

Vamos agora ao exemplo em PHP. Suponha que tenhamos uma classe User no nosso sistema que armazena dados do usuário em sessão. A sessão será manipulada através de uma classe SessionStorage.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class SessionStorage
{
  function __construct($cookieName = 'PHP_SESS_ID')
  {
    session_name($cookieName);
    session_start();
  }
  function set($key, $value)
  {
    $_SESSION[$key] = $value;
  }
  // ...
}
 
class User
{
  protected $storage;
  function __construct()
  {
    $this->storage = new SessionStorage();
  }
  function setLanguage($language)
  {
    $this->storage->set('language', $language);
  }
  // ...
}
 
$user = new User();

E se quisermos mudar o cookie name?

Bem, a alternativa mais óbvia é simplesmente adicionar o parâmetro à linha em que instanciamos SessionStorage na classe User (linha 20):

  $this->storage = new SessionStorage('SESSION_ID');

Outra alternativa seria configurarmos o nome através de uma constante:

1
2
3
4
5
6
7
8
9
10
11
12
 class User
{
  protected $storage;                        
 
  function __construct()
  {
    $this->storage = new   SessionStorage(STORAGE_SESSION_NAME);
  }
}
 
define('STORAGE_SESSION_NAME', 'SESSION_ID');
$user = new User();

Ou então, passar o nome como parâmetro para o construtor de User:

class User
{
  protected $storage;
  function __construct($sessionName)
  {
    $this->storage = new SessionStorage($sessionName);
  }
}
$user = new User('SESSION_ID');

Apesar de ser relativamente fácil de encontrar soluções semelhantes a estas em projetos por aí, nenhuma delas é ideal. Imagine que se queira agora trocar também o tipo de armazenamento (Sistema de Arquivos, Mysql, SQLite, etc). Como fazer?

Pode-se utilizar uma classe que realize o mapeamento, um registro:

  $this->storage = Registry::get('session_storage');

Neste caso, depende-se do registro. Por que, ao invés de instanciarmos o objeto responsável pela sessão de maneira hard coded no construtor, não injetamos a dependência (objeto manipulador de sessão) através de um parâmetro do construtor de User? Que tal?

class User
{
  protected $storage;
  function __construct($storage)
  {
    $this->storage = $storage;
  }
}
$storage = new SessionStorage('SESSION_ID');
$user = new User($storage);

Agora sim, não estamos mais instanciando a nossa dependência através do construtor, mas sim externamente. Caso queiramos trocar o tipo de armazenamento e os seus parâmetros de configuração, fazemos isto fora de User, por exemplo:

$storage = new MySQLSessionStorage('SESSION_ID');
$user = new User($storage);

Pra deixar as coisas ainda melhores, podemos padronizar um pouco mais a nossa classe, e utilizar uma interface como parâmetro do construtor:

function __construct(ISessionStorage $storage)
{
  $this->storage = $storage;
}
 
interface ISessionStorage
{
  function get($key);
  function set($key, $value);
}

E agora? E se quisermos testar a nossa classe User? Podemos usar um mock como meio de armazenamento, não?

class SessionStorageForTests implements ISessionStorage
{
  protected $data;
  function set($key, $value)
  {
    self::$data[$key] = $value;
  }
}

Prós:

  • Podemos utilizar meios de armazenamento diferentes
  • Configuração se torna natural
  • É possível utilizar classes de terceiros (encapsuladas na interface) de maneira transparente
  • Para testes, podemos usar mocks

Tudo isso de maneira simples, sem ter que alterar a classe User.

Contras

Sempre que se adiciona mais um nível de indireção, tem-se também um overhead associado. Em vista dos benefícios em relação à manutenibilidade do código e da facilitação para a realização de testes, principalmente, creio que seja um custo que valha a pena, não?

Perde-se parte do encapsulamento de uma classe ao se expor suas dependências. Além disso, dificulta-se um pouco a sua utilização, afinal, em teoria temos que instanciar as dependências sempre que quisermos utilizá-la. Em teoria, porque na prática podemos utilizar ferramentas que realizam essa tarefa para nós. Em breve espero escrever algo sobre o componente do symfony, que na realidade dá pra ser usado em qualquer projeto, não apenas naqueles baseados em symfony, legal né?

Para saber mais:

[1] The Dependency Injection Pattern – What is it and why do I care?

[2] Inversion of Control Containers and the Dependency Injection pattern do Martin Fowler (artigo mais referenciado sobre o tema)

Os exemplos foram copiados (de maneira resumida) desta apresentação:

http://fabien.potencier.org/talk/19/decouple-your-code-for-reusability-ipc-2008

Outra apresentação bem legal sobre DI em PHP:

http://www.procata.com/talks/phptek-may2007-dependency.pdf

Inversion of Control Containers and the Dependency Injection pattern

August 16, 2009   No Comments