Orientação a objetos

Autowiring em Container de Injeção de Dependência

No artigo Entendendo Injeção de Dependência vimos sobre o que é injeção de dependência, seu funcionamento e como se dá a sua aplicação. No artigo Container De Injeção De Dependência (DI Container) vimos como funciona um container para gerenciar o mapa de dependências.

Nesse artigo veremos uma funcionalidade relativamente comum nos containers de dependência de frameworks que é a habilidade de resolver as dependências de um construtor (principalmente) automaticamente, chamada de autowiring.

No artigo Container De Injeção De Dependência (DI Container) implementamos um protótipo (simples, porém funcional) de um container e, em determinado momento, chegamos nesse tipo de caso de uso:

$indexController = new IndexController(
    $container->get('user.repository')
);

$userController = new UserController(
    $container->get('user.repository')
);

$registerController = new RegisterController(
    $container->get('user.repository')
);

Alguns controladores precisavam receber a instância de UserRepository e tivemos que manualmente passá-la para cada um deles. Agora, imagine se cada um desses controladores tivesse que receber outras dependências? Nenhum problema, só que ficaria meio chato e improdutivo ter que ficar repetindo essas construções na instanciação deles, concorda?

E se desenvolvermos uma forma do nosso container automaticamente resolver essas dependências? Desse modo, tudo o que faríamos seria pedir para o Container uma instância desses controladores, sem a necessidade de nos preocuparmos em passar manualmente para o construtor de cada um deles as suas dependências. Teríamos como resultado um código assim:

$indexController = $container->get(IndexController::class);

$userController = $container->get(UserController::class);

$registerController = $container->get(RegisterController::class);

Estamos pedindo as instâncias desses controladores sem nos preocuparmos em alimentá-los de suas dependências, deixamos esse trabalho para o Container.

Symfony - Fundamentos
Curso de Symfony - Fundamentos
CONHEÇA O CURSO

Estrutura inicial do projeto

Vamos criar o protótipo de uma aplicação para que possamos testar a nossa implementação de Container. A nossa aplicação terá essa estrutura:

- [blog-artigo-di]
- - - [app]
- - - - [Http]
- - - - - - [Controller]
- - - - - - - UserController.php
- - - - [Repositories]
- - - - - LeadRepository.php
- - - - - TagRepository.php
- - - - - UserRepository.php
- - - Container.php
- index.php

O repositório dessa estrutura no GitHub você encontra clicando aqui. Se desejar, você pode fazer download direto dela clicando nesse link: sem-autowiring.zip

Depois de baixar e colocar no local onde normalmente você executa seus projetos, basta que você execute composer install pois o projeto faz uso da PSR-4 para autoloading:

$ ~/D/w/blog-artigo-di> composer install

O nosso Container atualmente possui uma implementação simples e que ainda não suporta a resolução automática de dependências. Abra app/Container.php:

<?php
declare(strict_types=1);

namespace App;

use Closure;

final class Container
{
    private $instances = [];

    public function set(string $id, Closure $closure) : void
    {
        $this->instances[$id] = $closure;
    }

    public function get(string $id) : object
    {
        return $this->instances[$id]($this);
    }

    public function singleton(string $id, Closure $closure) : void
    {
        $this->instances[$id] = function () use ($closure) {
            static $resolvedInstance;

            if (null !== $resolvedInstance) {
                $resolvedInstance = $closure($this);
            }

            return $resolvedInstance;
        };
    }
}

Esse container já foi explicado na indicação de leitura no início desse artigo. Portanto, não vamos entrar novamente em seus pormenores.

O arquivo index.php na raiz do projeto é o responsável pelo bootstrap da nossa aplicação protótipo:

<?php

// Carrega o autoload do Composer
require './vendor/autoload.php';

use App\Repositories\TagRepository;
use App\Repositories\LeadRepository;
use App\Repositories\UserRepository;
use App\Http\Controller\UserController;

// Instancia o container
$container = new App\Container();

// Adiciona referências ao container
$container->set(TagRepository::class, function() {
    return new TagRepository();
});

$container->set(LeadRepository::class, function() {
    return new LeadRepository();
});

$container->set(UserRepository::class, function() {
    return new UserRepository();
});

// Instancia o UserController passando para ele as dependências necessárias
$userController = new UserController(
    $container->get(TagRepository::class),
    $container->get(UserRepository::class),
    $container->get(LeadRepository::class)
);

$userController->index();

Para testar o exemplo, na raiz do projeto execute:

$ ~/D/w/blog-artigo-di> php -S localhost:8000

Isso executará o servidor embutido do PHP em cima da raiz do nosso projeto, permitindo que o acessemos pelo navegador através da URL: http://localhost:8000/

O resultado será:

/Users/kennedytedesco/Documents/www/blog-artigo-di/app/Http/Controller/UserController.php:31:
array (size=2)
  'id' => int 1
  'name' => string 'tag' (length=3)

/Users/kennedytedesco/Documents/www/blog-artigo-di/app/Http/Controller/UserController.php:31:
array (size=2)
  'id' => int 2
  'name' => string 'user' (length=4)

/Users/kennedytedesco/Documents/www/blog-artigo-di/app/Http/Controller/UserController.php:31:
array (size=2)
  'id' => int 3
  'name' => string 'lead' (length=4)

Indica que o método index() do UserController foi executado com sucesso. Ou seja, as dependências, os repositórios necessários para o funcionamento dessa classe foram injetados com sucesso. Até aqui tudo bem, ademais, injetamos tais dependências manualmente:

$userController = new UserController(
    $container->get(TagRepository::class),
    $container->get(UserRepository::class),
    $container->get(LeadRepository::class)
);

Implementando a resolução automática de dependências

Para que possamos implementar a resolução automática das dependências utilizaremos a API de reflexão do PHP. Essa API nos fornece meios para que façamos engenharia reversa nas classes, extraindo muitas de suas informações internas como quantos métodos possui, quais são públicos, protegidos ou privados, se implementa um construtor, quais são os parâmetros do construtor, se são opcionais ou exigidos, entre outras informações.

Dessa forma, se o usuário pedir para o Container a instanciação do UserController:

$userController = $container->get(UserController::class);

Que tal a gente usar reflexão para obter quais dependências (classes) ele necessita para ser instanciado e então resolver essas dependências automaticamente usando o próprio Container e retornar a instância dele?

É exatamente isso que a nossa implementação de autowiring fará. Para tanto, começaremos alterando o código do nosso container app\Container.php para:

<?php
declare(strict_types=1);

namespace App;

use Closure;
use ReflectionClass;
use ReflectionParameter;

final class Container
{
    private $instances = [];

    public function set(string $id, Closure $closure) : void
    {
        $this->instances[$id] = $closure;
    }

    public function get(string $id) : object
    {
        // Se essa referência existe no mapa do container, então a retorna diretamente.
        if ($this->has($id)) {
            return $this->instances[$id]($this);
        }

        // Se a referência não existe no container, então foi passado uma classe para ser instanciada
        // Façamos então a reflexão dela para obter os parâmetros do método construtor
        $reflector = new ReflectionClass($id);
        $constructor = $reflector->getConstructor();

        // Se a classe não implementa um método construtor, então vamos apenas retornar uma instância dela.
        if (null === $constructor) {
            return new $id();
        }

        // Itera sobre os parâmetros do construtor para realizar a resolução das dependências que ele exige.
        // O método "newInstanceArgs()" cria uma nova instância da classe usando os novos argumentos passados.
        // Usamos "array_map()" para iterar os parâmetros atuais, resolvê-los junto ao container e retornar um array das instâncias já resolvidas pelo container.
        return $reflector->newInstanceArgs(array_map(
            function (ReflectionParameter $dependency) {
                // Busca no container a referência da classe desse parâmetro
                return $this->get(
                    $dependency->getClass()->getName()
                );
            },
            $constructor->getParameters()
        ));
    }

    public function singleton(string $id, Closure $closure) : void
    {
        $this->instances[$id] = function () use ($closure) {
            static $resolvedInstance;

            if (null !== $resolvedInstance) {
                $resolvedInstance = $closure($this);
            }

            return $resolvedInstance;
        };
    }

    public function has(string $id) : bool
    {
        return isset($this->instances[$id]);
    }
}

(Através do GitHub você pode visualizar o que foi adicionado/removido. Basta visualizar esse commit aqui).

A partir do momento que o nosso Container consegue resolver as dependências automaticamente, podemos alterar no index.php a forma de instanciar, que era uma instanciação direta da classe usando new UserController:

$userController = new UserController(
    $container->get(TagRepository::class),
    $container->get(UserRepository::class),
    $container->get(LeadRepository::class)
);

Para uma instanciação que usa o Container (para que ele possa resolver as dependências para nós):

$userController = $container->get(UserController::class);
$userController->index();

(O código completo do exemplo com autowiring encontra-se no branch master do repositório desse exemplo).

Você pode testar o exemplo e terá o mesmo resultado obtido anteriormente quando não usávamos autowiring. O UserController continuará sendo instanciado com sucesso.

Laravel - Framework PHP (Parte 3/3)
Curso de Laravel - Framework PHP (Parte 3/3)
CONHEÇA O CURSO

Concluindo

Vimos nesse artigo a fundação sobre como a resolução automática de dependências é feita nos containers de injeção de dependência. É por esse caminho que Symfony, Laravel entre outros frameworks (inclusive de outras linguagens) fazem. Obviamente o nosso Container é simples, didático e bem direto ao ponto, não estando 100% pronto para uso em projetos reais. Algumas verificações de segurança (se a classe existe, senão lançar uma exceção etc) precisariam ser implementadas. Na realidade, existem boas implementações de containers por aí e, se você usa um Web Framework, não vai precisar criar a sua própria. No entanto, saber como funciona, é essencial. Essa foi a intenção desse artigo.

Até a próxima!

Os pilares da orientação a objetos

O paradigma orientado a objetos é um dos paradigmas mais utilizados no mercado de trabalho. Além de ser um dos primeiros paradigmas com o qual nós temos contato quando começamos a estudar desenvolvimento de software, a maioria das linguagens utilzadas pela indústrial em geral possui uma forte base orientada a objetos, o que faz com que seja essencial o domínio deste paradigma. Neste artigo, vamos verificar quais são os pontos principais do paradigma orientado a objetos.

Lógica Orientada a Objetos Completo
Curso de Lógica Orientada a Objetos Completo
CONHEÇA O CURSO

Classes e objetos

Em linguagens orientadas a objeto, nós organizamos a maior parte do nosso código em estruturas chamadas classes.

Você pode entender uma classe como sendo inicialmente um molde, molde este que geralmente representa alguma estrutura do mundo real com a qual nosso código terá que lidar. Vamos imaginar que estejamos trabalhando com uma aplicação que lida com carros…

Provavelmente, nossa aplicação irá lidar com carros e, por causa disso, precisaremos de um molde para definirmos o que é um carro dentro do nosso código. Esse molde seria responsável por estabelecer o que é um carro e o que pode fazer um carro.

Se pararmos para pensar, nós podemos falar que um carro pode ser caracterizado pelos seguintes itens:

  • Marca;
  • Modelo;
  • Cor;
  • Placa;
  • E várias outras características…

Também podemos dizer que um carro pode ter as seguintes ações:

  • Ligar;
  • Acelerar;
  • Desligar;
  • E várias outras ações…

Veja que, quando falamos do que um carro é, estamos falando de suas características, ou seja: falamos do que caracteriza e define um determinado carro. Já quando falamos do que um carro pode fazer, estamos falando das ações que um carro pode desempenhar.

Trazendo para termos técnicos, nós podemos chamar as características do carro de atributos, euquanto nós chamamos as ações de métodos. No final, os métodos e atributos ficam agrupados em uma classe.

Nós podemos representar estes atributos e métodos através de uma linguagem de modelagem chamada UML (Unified Modeling Language). A UML prevê alguns diagramas que visam auxiliar o processo de modelagem de um software. Entre estes diagramas, nós temos justamente o diagrama de classes.

A classe Carro, se fosse representada pelo diagrama de classes UML, poderia ficar da seguinte maneira:

Perceba que a partir deste molde, nós podemos especificar vários carros… Poderíamos ter um Fiat Línea prata com a placa ABC-1234, um Volswagen Gol preto com a placa DEF-4567 ou mesmo um Hyundai HB20 branco com a placa GHI-8901… Todos eles são carros, já que derivam do mesmo molde: todos eles têm marca, modelo, cor e placa, além de poderem ser ligados, desligados, freados e acelerados, atributos e métodos todos estabelecidos pela classe Carro. Nesse caso, o Fiat Línea, o Volkswagen Gol e o Hyundai HB20 são objetos, pois foram “fabricados” a partir do molde que definimos, que é a classe Carro. Objetos são como variáveis que criamos para utilizar as nossas classes, seja para definir seus atributos, como também para invocar seus métodos. É o encadeamento coordenado entre escritas e leituras de atributos com a invocação de métodos que dá a tônica de uma aplicação escrita com uma linguagem orientada a objetos.

Se fôssemos utilizar o Java para representar nossa classe e nossos objetos de uma maneira primitiva, teríamos o código abaixo.

// Pacotes e demais estruturas omitidas para clareza...
public class Carro {
    public String modelo;
    public String marca;
    public String cor;
    public String placa;

    public void ligar() {
        System.out.println("O veículo ligou!");
    }

    public void desligar() {
        System.out.println("O veículo desligou!");
    }
}

// ...
Carro gol = new Carro();
gol.modelo = "Gol";
gol.marca = "Volkswagen";

Carro linea = new Carro();
linea.modelo = "Línea";
linea.marca = "Volkswagen";

gol.ligar();
gol.desligar();
linea.ligar();
linea.desligar();

No exemplo acima, temos a classe Carro fazendo o papel de nosso molde. As variáveis gol e linea são objetos que são do tipo Carro ou, utilizando os termos técnicos corretos, gol e linea são instâncias da classe Carro. Por derivarem da classe Carro, estes objetos têm todos os atributos e métodos previstos pela classe Carro.

Encapsulamento

Ainda levando em consideração o exemplo com a classe Carro, poderíamos imaginar um indicador para verificarmos se o carro está ligado ou não. Isso até daria mais “qualidade” à nossa classe Carro: nós poderíamos, por exemplo, garantir que o carro só pudesse ser acelerado ou freado caso estivesse ligado. Esse indicador ainda poderia ser modificado pelos métodos ligar()e desligar()

Partindo dessa idéia, nossa classe Carro ficaria da seguinte maneira:

public class Carro {

    public String modelo;
    public String marca;
    // ...
    public boolean ligado;  

    public Carro() {
        ligado = false;
    }

    public void ligar() {
        ligado = true;
        System.out.println("O veículo ligou!");
    }

    public void desligar() {
        ligado = false;
        System.out.println("O veículo desligou!");
    }
}

Veja que, além de adicionarmos um atributo chamado ligado do tipo booleano (truepara ligado ou false para desligado), nós temos ainda o trecho de código abaixo:

public Carro() {
    ligado = false;
}

Esse trecho de código é um construtor. O construtor é invocado quando inicializamos um objeto a partir de uma classe. Nós, de maneira geral, invocamos o construtor quando chamamos a criação da instância com a palavra-chave new. Sendo assim, quando temos o código abaixo…

Carro gol = new Carro();

… nós estamos justamente chamando este método construtor. Ao reescrevermos este método, estamos impondo uma “personalização” na inicialização dos objetos a partir da classe Carro: todo carro será criado já possuindo o indicador ligado como false, ou seja, o carro já começa como desligado por padrão.

Agora, poderíamos definir, por exemplo, um método chamado acelerar() em nosso Carro. Nós podemos verificar o atributo ligado para “controlar” melhor este método: um carro, obviamente, só pode ser acelerado caso esteja ligado.

Nosso código de exemplo ficaria da seguinte maneira:

public class Carro {

    public String modelo;
    public String marca;
    // ...
    public boolean ligado;  

    public Carro() {
        ligado = false;
    }

    public void ligar() {
        ligado = true;
        System.out.println("O veículo ligou!");
    }

    public void desligar() {
        ligado = false;
        System.out.println("O veículo desligou!");
    }

    public void acelerar() {
        if (!ligado){
            throw new Exception("O carro não pode ser acelerado, pois ele está desligado."); // Erro: o carro está desligado!
        }
        System.out.println("O carro foi acelerado");
    }
}

Assim, poderíamos ter a nossa classe Carro sendo utilizada da seguinte maneira:

Carro gol = new Carro();
System.out.println(gol.ligado); // Vai imprimir "false" por causa do construtor personalizado
gol.modelo = "Gol";
gol.marca = "Volkswagen";
// gol.acelerar(); Se essa linha for descomentada, o código gerará um erro, pois estamos tentando acelerar um carro desligado
gol.ligar();
System.out.println(gol.ligado); // Vai imprimir "true", pois o método ligar() foi chamado
gol.acelerar();
gol.desligar();
System.out.println(gol.ligado); // Vai imprimir "false", pois o método desligar() foi chamado

Aparentemente, nossa classe Carro está funcionando corretamente. Mas, temos um problema: o atributo ligado é acessível para todo mundo, da mesma maneira que os atributos modelo e marca por exemplo. Isso quer dizer que nós podemos “deturpar” o comportamento da classe Carro… Nós poderíamos, por exemplo, alterar manualmente o conteúdo do atributo ligado antes de chamarmos o método ligar(), permitindo acelerar um carro que estivesse em tese desligado:

Carro gol = new Carro();
System.out.println(gol.ligado); // Vai imprimir "false" por causa do construtor personalizado
gol.ligado = true; // Atributo que define se o carro está ligado ou não alterado "na mão"
gol.acelerar(); // Agora a linha não causará erro, mesmo que o método ligar() não tenha sido chamado

Isso certamente é uma situação problemática, pois agora nosso carro dá uma brecha para funcionar de maneira diferente de como ele foi planejado.

Para corrigirmos isso, precisamos recorrer a um pilar da orientação a objetos: o encapsulamento. O encapsulamento visa esconder atributos e métodos de nossas classes que não deveriam ser acessados por outras estruturas. É exatamente o que precisamos: o atributo ligado deveria ser acessível em tese só pela própria classe Carro, o que permitiria somente aos métodos ligar() e desligar() alterarem o indicador de funcionamento do carro da maneira correta. Isso evitaria que nós acessássemos o atributo do lado de fora, causando a falha no código que estamos discutindo aqui.

O encapsulamento nas linguagens orientadas a objetos é definido através de algo que chamamos de atributo de visibilidade. Estes atributos de visibilidade estabelecem justamente o quão acessível nossos atributos e métodos são com relação às demais estruturas do nosos código. De maneira geral, temos três atributos de visibilidade básicos e comuns às linguagens orientadas a objeto em geral:

  • public: a estrutura é visível a partir de qualquer lugar no código, inclusive em outras classes fora a classe que define o atributo ou método em si;
  • private: a estrutura é visível somente pela classe que define a estrutura em si. Estruturas externas, como outras classes, não conseguem acessar o método ou atributo que esteja marcado com este atributo de visibilidade;
  • protected: a estrutura é visível somente na classe-mãe e nas classes-filhas.

Se considerarmos a nossa classe Carro, podemos ver que o problema relatado acontece porque o atributo ligado está definido como public, o tornando acessível em qualquer lugar. Vimos que esse é o problema, pois o atributo ligado não poderia ser acessível a partir de qualquer lugar: ele deveria ser acessível somente dentro dos métodos ligar() e desligar(), ambos dentro da classe Carro. O nosso atributo ligado não está encapsulado.

Poderíamos o encapsular se o tornássemos private, fazendo com que ele fosse acessível somente dentro da classe Carro.

public class Carro {

    public String modelo;
    public String marca;
    // ...
    private boolean ligado; 

    public Carro() {
        ligado = false;
    }

    public void ligar() {
        ligado = true;
        System.out.println("O veículo ligou!");
    }

    public void desligar() {
        ligado = false;
        System.out.println("O veículo desligou!");
    }

    public void acelerar() {
        if (!ligado){
            throw new Exception("O carro não pode ser acelerado, pois ele está desligado."); // Erro: o carro está desligado!
        }
        System.out.println("O carro foi acelerado");
    }
}

Assim, o erro que víamos antes não acontecerá mais, pois o atributo ligado agora só é acessível dentro da própria classe Carro.

Carro gol = new Carro();
gol.ligado = true; // Essa linha causará um erro de compilação, pois o atributo "ligado" não é mais acessível externamente

Agora, podemos dizer que o atributo ligado da classe Carro está encapsulado.

Se quiséssemos pelo menos ler o valor do atributo ligado externamente (já que alterá-lo externamente estaria completamente errado), poderíamos criar um método que devolvesse o valor do atributo ligado.

public class Carro {

    public String modelo;
    public String marca;
    // ...
    public boolean ligado;  

    public Carro() {
        ligado = false;
    }

    public void ligar() {
        ligado = true;
        System.out.println("O veículo ligou!");
    }

    public void desligar() {
        ligado = false;
        System.out.println("O veículo desligou!");
    }

    public void acelerar() {
        if (!ligado){
            throw new Exception("O carro não pode ser acelerado, pois ele está desligado."); // Erro: o carro está desligado!
        }
        System.out.println("O carro foi acelerado");
    }

    public boolean estaLigado() {
        return ligado;
    }
}

Com o método acima, poderíamos pelo menos verificar externamente se o carro está ligado ou desligado.

Carro gol = new Carro();
System.out.println(gol.estaLigado()); // Vai imprimir "false" por causa do construtor personalizado
gol.modelo = "Gol";
gol.marca = "Volkswagen";
gol.ligar();
System.out.println(gol.estaLigado()); // Vai imprimir "true", pois o método ligar() foi chamado
gol.acelerar();
gol.desligar();
System.out.println(gol.estaLigado()); // Vai imprimir "false", pois o método desligar() foi chamado

Este tipo de método é geralmente chamado de método de acesso, já que ele provê um tipo de acesso indireto a um atributo encapsulado. Com relação ao encapsulamento, nós temos dois tipos de métodos de acesso basicamente:

  • get: métodos que permitem ver o valor de um atributo;
  • set: métodos que permitem alterar o valor de um atributo.

Nós poderíamos falar que o método estaLigado() é um método get, pois ele permite a nós lermos algo que está encapsulado dentro da classe Carro.

É uma prática recorrente em linguagens orientadas a objeto (principalmente no Java) envolver todos os atributos com métodos de acesso do tipo get e set, evitando o acesso direto aos atributos. Apesar de ser uma prática comum, é importante dizer que só o fato de utilizarmos métodos de acesso não garante o encapsulamento de nenhuma estrutura. O que garante este encapsulamento é a definição de visibilidade correta de cada um dos atributos e métodos e o estabelecimento dos métodos de acesso de maneira correta. Por exemplo: não criamos um método set para o atributo ligado, pois ele só pode ser alterado pela própria classe.

Se fôssemos aplicar esta regra a nossa classe Carro, nós teríamos o seguinte código:

public class Carro {

    private String modelo;
    private String marca;
    // ...
    public boolean ligado;  

    public void setModelo(String modelo) {
        this.modelo = modelo;
    }

    public String getModelo() {
        return modelo;
    }

    public void setMarca(String marca) {
        this.marca = marca;
    }

    public String getMarca() {
        return marca;
    }

    public Carro() {
        ligado = false;
    }

    public void ligar() {
        ligado = true;
        System.out.println("O veículo ligou!");
    }

    public void desligar() {
        ligado = false;
        System.out.println("O veículo desligou!");
    }

    public void acelerar() {
        if (!ligado){
            throw new Exception("O carro não pode ser acelerado, pois ele está desligado."); // Erro: o carro está desligado!
        }
        System.out.println("O carro foi acelerado");
    }

    public boolean estaLigado() {
        return ligado;
    }
}

Herança

O reaproveitamento de código e a possibilidade de se evitar código duplicado são objetivos das linguagens orientadas a objetos. Vamos imaginar que agora nós precisamos criar uma classe para definir um outro tipo de veículo, como uma bicicleta. Uma bicicleta possui atributos em comum com um carro: ambos possuem marca e modelo, por exemplo. Além disso, ambos não deixam de ser um tipo de veículo.

Se fôssemos escrever o código para definição da classe Bicicleta sem considerar a classe Carro, nós teríamos o seguinte código:

public class Bicicleta {

    private String modelo;
    private String marca;

    public void setModelo(String modelo) {
        this.modelo = modelo;
    }

    public String getModelo() {
        return modelo;
    }

    public void setMarca(String marca) {
        this.marca = marca;
    }

    public String getMarca() {
        return marca;
    }

    public void acelerar() {
        System.out.println("A bicicleta foi acelerada.");
    }
}

Veja que acabamos duplicando todo o código relativo aos atributos marca e modelo nas classes Carro e Bicicleta, o que pode ser bem ruim em termos de manutenibilidade do código a longo prazo. Mas, nós temos uma maneira de evitar essa duplicidade: nós podemos utilizar o conceito de herança.

Nesse exemplo, se fôssemos aplicar o conceito de herança, nós teríamos três classes:

  • A classe Carro, com tudo que um carro possui de características e ações;
  • A classe Bicicleta, com tudo que uma bicicleta possui de características e ações;
  • Uma nova classe chamada Veiculo, com tudo que existe de comum entre carros e bicicletas.

No exemplo acima, para que as classes Carro e Bicicleta conseguissem usufruir das estruturas comuns estabelecidas na classe Veiculo, elas precisariam herdar a classe Veiculo.

Poderíamos representar esta relação entre as classes Veiculo, Carro e Bicicleta com a UML da seguinte maneira:

Diagrama de classes: Veiculo, Carro e Bicicleta

Também poderíamos definir as classes Veiculo, Carro e Bicicleta da seguinte maneira:

public class Veiculo {

    private String modelo;
    private String marca;

    public void setModelo(String modelo) {
        this.modelo = modelo;
    }

    public String getModelo() {
        return modelo;
    }

    public void setMarca(String marca) {
        this.marca = marca;
    }

    public String getMarca() {
        return marca;
    }

    public void acelerar() {
        // ???
    }

}

public class Carro extends Veiculo {

    public boolean ligado;  

    public Carro() {
        ligado = false;
    }

    public void ligar() {
        ligado = true;
        System.out.println("O veículo ligou!");
    }

    public void desligar() {
        ligado = false;
        System.out.println("O veículo desligou!");
    }

    public void acelerar() {
        if (!ligado){
            throw new Exception("O carro não pode ser acelerado, pois ele está desligado."); // Erro: o carro está desligado!
        }
        System.out.println("O carro foi acelerado");
    }

    public boolean estaLigado() {
        return ligado;
    }
}

public class Bicicleta extends Veiculo {

    public void acelerar() {
        System.out.println("A bicicleta acelerou!");
    }

}

Veja que tudo que é comum entre as classes Carro e Bicicleta foi para a classe Veiculo. As classes Carro e Bicicleta estão agora herdando a classe Veiculo para reaproveitar estas estruturas comuns, além de que um carro e uma bicicleta são tipos de veículos. É importante que, quando formos empregar a herança, exista essa relação de “ser” ou “estar” entre as classes.

Nesse caso, nós podemos fazer com que Carro herde Veiculo porque um carro é um veículo; assim como também podemos fazer com que Bicicleta herde Veiculo, pois uma bicicleta é um tipo de veículo. Com a herança, nós evitamos a duplicidade de código e facilitamos a manutenção, além de manter a coerência, desde que a regra do “ser/estar” seja devidamente implementada.

Quando temos a herança sendo utilizada, as classes podem assumir um dos dois papéis:

  • Classe-mãe, classe-base ou super-classe: é a classe que serve de base para as demais classes. Em nosso exemplo, a classe Veiculo é uma super-classe;
  • Classe-filha ou sub-classe: é a classe que herda outra determinada classe. No nosso exemplo, as classes Carro e Bicicleta são sub-classes.

Abstração

Quando estamos lidando com a orientação a objetos, é muito comum que nós sempre tentemos escrever código baseado em abstrações, pois isso traz flexibilidade ao código.

No exemplo anterior, nós esbarramos em um problema de abstração: todo veículo é capaz de acelerar, independente de ser um carro ou bicicleta. Sendo assim, nós não podemos remover o método acelerar() da nossa classe Veiculo, já que todo veículo tem esse comportamento. Mas, a própria classe Veiculo não “sabe” como acelerar.

Quem sabe como acelerar é a classe Carro(que sabe como um carro acelera) e a classe Bicicleta (que sabe como uma bicicleta acelera). Para resolver esta situação, poderíamos tornar o método acelerar() abstrato: isso vai desobrigar a classe Veiculo a definir uma implementação para este método (já que a classe Veiculo não sabe como acelerar), mas obriga as classes-filha (no caso, as classes Carro e Bicicleta) a definirem seus comportamentos de aceleração.

Além disso, se pararmos para analisar, não faz sentido nós instanciarmos objetos a partir da classe Veiculo, já que ela serve somente como uma super-classe em nosso cenário. Nós poderíamos instanciar objetos a partir das classes Carro e Bicicleta, mas não a partir da classe Veiculo. Como queremos evitar que objetos sejam instanciados a partir da classe Veiculo, já que ela deve ser somente uma classe-base, também podemos definir a classe Veiculo como sendo uma classe abstrata.

O código ficaria da seguinte maneira:

// Classe abstrata (não pode ser instanciada)
public abstract class Veiculo {

    private String modelo;
    private String marca;

    public void setModelo(String modelo) {
        this.modelo = modelo;
    }

    public String getModelo() {
        return modelo;
    }

    public void setMarca(String marca) {
        this.marca = marca;
    }

    public String getMarca() {
        return marca;
    }

    // Método abstrato: a classe Veiculo sabe que ela tem que acelerar, mas não sabe como fazer isso.
    // A responsabilidade passa a ser das classes-filha
    public abstract void acelerar();

}

public class Carro extends Veiculo {

    public boolean ligado;  

    public Carro() {
        ligado = false;
    }

    public void ligar() {
        ligado = true;
        System.out.println("O veículo ligou!");
    }

    public void desligar() {
        ligado = false;
        System.out.println("O veículo desligou!");
    }

    // Aqui, a classe Carro define como ela deve exercer o ato de acelerar
    @Override
    public void acelerar() {
        if (!ligado){
            throw new Exception("O carro não pode ser acelerado, pois ele está desligado."); // Erro: o carro está desligado!
        }
        System.out.println("O carro foi acelerado");
    }

    public boolean estaLigado() {
        return ligado;
    }
}

public class Bicicleta extends Veiculo {

    // Aqui, a classe Bicicleta define como ela deve exercer o ato de acelerar
    @Override
    public void acelerar() {
        System.out.println("A bicicleta acelerou!");
    }

}

Com a utilização da abstração nesse caso, sabemos que qualquer novo tipo de veículo que for criado a partir da classe Veiculo terá que obrigatoriamente implementar o comportamento de aceleração. Isso faz muito sentido, já que todo veículo tem que ser capaz de acelerar.

Polimorfismo

Linguagens orientadas a objetos ainda prevêem o suporte para criação de estruturas polimórficas. Estruturas polimórficas são estruturas que conseguem mudar seu comportamento interno em determinadas circunstâncias. Essa variação comportamental pode acontecer por algumas formas, como através da sobescrita de métodos e através do LSP (Liskov Substitution Principle).

No exemplo anterior, nós temos um exemplo de polimorfismo através da sobrescrita de métodos: nós temos a classe Veiculo, que possui o método abstrato acelerar(). O método é abstrato porque a classe Veiculo só sabe que tem que conter este comportamento, mas não sabe como esse comportamento deve ocorrer.

Mas, as classes Carro e Bicicleta foram obrigadas a implementar o método acelerar() por herdarem a classe Veiculo. Cada uma dessas classes implementou o método acelerar() da maneira mais adequada para cada um dos tipos de veículos. Aqui, já temos um exemplo de polimorfismo: as classes Carro e Bicicleta são polimórficas, pois possuem um ancestral comum (a classe Veiculo) que as obriga a implementar o método acelerar(), mas cada uma delas implementa o mesmo método da maneira mais correta para cada tipo de veículo.

Essa mudança de implementação não exigiu a mudança do código da classe Veiculo, além de que a implementação dos métodos acelerar() em cada uma das classes é isolada: a implementação do método acelerar() na classe Carro não afeta a implementação do mesmo método na classe Bicicleta, e vice-versa.

Outro exemplo de polimorfismo seria através da aplicação do Princípio da Substituição de Liskov (também conhecido como LSP – Liskov Substitution Principle). O LSP é parte de um conjunto de cinco práticas de codificação conhecido como SOLID. Estes princípios visam a produção de código com alta qualidade e alinhado com os princípios das linguagens orientadas a objeto.

Para entender o LSP, considere o código abaixo:

Carro veiculo = new Carro();
// ...
veiculo.acelerar(); // Vai escrever "O carro foi acelerado"
// ...

O código não aparenta nada de diferente: temos um objeto chamado veiculo do tipo Carro. Mas, pelo fato de Carro herdar a classe Veiculo, nós podemos deduzir que um objeto do tipo Carro também pode ser considerado como sendo do tipo Veiculo; afinal, todo Carro agora é também um Veiculo por causa da herança. Sendo assim, podemos escrever o código acima da seguinte maneira:

Veiculo veiculo = new Carro();
// ...
veiculo.acelerar(); // Vai escrever "O carro foi acelerado"
// ...

Nós podemos definir o objeto veiculo como sendo do tipo Veiculo, mas o criar com base em um Carro. Nesse momento, o objeto veiculo vai se comportar como um Carro. No caso acima, dizemos que Veiculo é a abstração, enquanto Carro é a concretização.
Se quiséssemos trocar o tipo do nosso veículo para uma bicicleta, bastaria trocar a concretização.

Todo o código abaixo continuaria funcionando normalmente, já que uma Bicicleta também é um Veiculo. Isso torna nosso código muito mais flexível (se quisermos alterar o comportamento do nosso código, basta trocar as concretizaçãoes) e a prova de falhas de escrita de código (já que a abstração vai garantir que o código que vier logo abaixo da definição da concretização vai continuar funcionando de maneira transparente).

Veiculo veiculo = new Bicicleta();
// ...
veiculo.acelerar(); // Vai escrever "A bicicleta foi acelerada"
// ...

Neste exemplo, além de todas as vantagens que podemos notar no que diz respeito à qualidade e manutenibilidade do código, podemos dizer que o objeto veiculo é um objeto polimórfico, pois a concretização está sendo capaz de alterar seu comportamento. Porém, todo o código subsequente não é afetado por essa troca.

Conclusão

As definições de classes, objetos, encapsulamento, herança, abstração e polimorfismo constituem os principais pilares do paradigma orientado a objetos. Estes são considerados temas pilares pois, através dele, podemos começar a obter as vantagens que o paradigma orientado a objetos visa oferecer da maneira mais correta possível. É imprescindível que nós, como desenvolvedores, conheçamos estes pilares para que consigamos escrever código orientado a objetos flexível, conciso e com um grau de qualidade superior.

Aqui, vale uma ressalva: cada linguagem oferece estes pilares de uma maneira própria, a depender da filosofia da linguagem. Existem linguagens orientadas a objeto que talvez não ofereçam o conceito de visibilidade de maneira tão explítica; algumas linguagens permitem herança múltipla, outras linguagens oferecem apenas herança simples… De fato, para o total aproveitamento dos conceitos da orientação a objetos, é imprescindível que, além de conhecer estes pilares, o desenvolvedor também conheça a filosofia e a sintaxe da linguagem que está sendo utilizada.

Lógica Orientada a Objetos Completo
Curso de Lógica Orientada a Objetos Completo
CONHEÇA O CURSO

Utilizando herança no Python

Desenvolvida originalmente sob o paradigma funcional, o Python precisou evoluir com o tempo para se adequar ao mercado competitivo e, assim, se tornar uma das linguagens mais utilizadas do mundo. Para isso, um dos principais recursos que foram implementados foi o suporte ao paradigma orientado ao objetos, porém, sem perder a essencia da linguagem, fazendo com que muita coisa seja implementada de forma diferente quando comparado com o Java ou o C#, por exemplo. Uma dessas mudanças foi a adição do recurso de herança na linguagem e que veremos como utilizá-la neste artigo.

Python - Orientação a objetos
Curso de Python - Orientação a objetos
CONHEÇA O CURSO

O que é a Herança?

A Herança é um conceito do paradigma da orientação à objetos que determina que uma classe (filha) pode herdar atributos e métodos de uma outra classe (pai) e, assim, evitar que haja muita repetição de código.

Um exemplo do conceito de herança pode ser visto no diagrama a seguir:

Nele temos que as classes filhas Gato, Coelho e Cachorro herdam os atributos e métodos da classe pai Animal e, assim caracterizando a herança.

Não vamos entrar em muitos detalhes sobre o conceito da herança, mas se você quiser estudar um pouco mais a fundo esse conceito, recomendo um outro artigo aqui do blog: Devo usar herança ou composição?

Como usar a herança no Python?

Para utilizar a herança no Python é bem simples. Assim com vimos no diagrama acima, vamos criar quatro classes para representar as entidades Animal, Gato, Cachorro e Coelho.

Obs: não vamos nos preocupar com a implementação de métodos e atributos, vamos criar apenas o básico para demonstrar o conceito da herança.

class Animal():
    def __init__(self, nome, cor):
        self.__nome = nome
        self.__cor = cor

    def comer(self):
        print(f"O {self.__nome} está comendo")

No código acima definimos a classe pai que irá possuir todos os atributos e métodos comuns às classes filhas (Gato, Cachorro e Coelho). Nela, criamos apenas o construtor que irá receber o nome e a cor do animal, além do método comer que vai exibir a mensagem com o nome do animal que está comendo.

Após isso, criamos as três classes “filhas” da classe Animal. Para definir que estas classes são herdeiras da classe Animal, declaramos o nome da classe pai nos parenteses logo após definir o nome da classe, como podemos ver abaixo:

import animal

class Gato(animal.Animal):
    def __init__(self, nome, cor):
        super().__init__(nome, cor)
import animal

class Cachorro(animal.Animal):
    def __init__(self, nome, cor):
        super().__init__(nome, cor)
import animal

class Coelho(animal.Animal):
    def __init__(self, nome, cor):
        super().__init__(nome, cor)

Note que as classes filhas só estão repassando seus dados de nome e cor para a classe Pai através do super() e que nenhum método foi implementado dentro dessas classes.

Agora, por herdar da classe Animal, as classes Gato, Cachorro e Coelho podem, sem nenhuma alteração, utilizar o método comer(), definido na classe Animal pois elas herdam dessa classe, logo elas possuem a permissão de invocar este método:

import gato, cachorro, coelho

gato = gato.Gato("Bichano", "Branco")
cachorro = cachorro.Cachorro("Totó", "Preto")
coelho = coelho.Coelho("Pernalonga", "Cinza")

gato.comer()
cachorro.comer()
coelho.comer()

Ao executar o código acima, obtemos o seguinte retorno no terminal:

O Bichano está comendo
O Totó está comendo
O Pernalonga está comendo

E é dessa forma que é implementado o conceito de herança no Python. Há muitos outros conceitos, dentro da herança, a serem abordados, como a sobrescrita de métodos, herança múltipla… Mas não se preocupem, estes tópicos serão abordados em breve. 🙂

Ah, claro, muito te recomendo o nosso completo curso sobre Orientacão a Objetos com Python:

Python - Orientação a objetos
Curso de Python - Orientação a objetos
CONHEÇA O CURSO

Espero vocês lá. Abraço!

Object Calisthenics em PHP – Parte 2

Este artigo é uma continuação do anterior. Para relembrarmos, abaixo a lista dos assuntos e em negrito os que serão abordados nesse artigo:

  1. Um nível de indentação por método
  2. Não use ELSE
  3. Envolva seus tipos primitivos
  4. Envolva suas collections em classes
  5. Uma chamada de método por linha
  6. Não abrevie
  7. Mantenha as classes pequenas
  8. Não tenha classes com mais de duas variáveis de instância
PHP Básico
Curso de PHP Básico
CONHEÇA O CURSO

3. Envolva seus tipos primitivos

Podemos definir esse exercício para os tipos escalares em PHP que são: int, float, bool e string. “Envolver” vem de um significado da programação orientada a objetos, que quer dizer, colocar o tipo “envolta” de uma classe, a fim de trazer mais resultados e funcionalidades do que um tipo comum/escalar.

Essa técnica vêm de uma aplicação do DDD (Domain-Driven Design) chamada de Value Object, onde temos um objeto-valor pequeno, que irá cuidar de um tipo de dado específico. Como o PHP é fracamente tipado, a melhor aplicação será em passagens de parâmetros de métodos ou funções. Veja o código abaixo:

class Customer
{
    protected $name;
    protected $birthday;

    public function __construct(string $name, string $birthday)
    {
        // Validar aqui???
        $this->name = $name;
        $this->birthday = $birthday;
    }
}

Ambos parâmetros são validáveis e não é legal validarmos no construtor da classe, pelo fato de não podermos reaproveitar as validações. Já o fato de forçarmos os atributos como string na entrada, algo errado pode acontecer se o desenvolvedor que está usando a classe não souber, por exemplo, qual padrão de data utilizado para a entrada $birthday, o que pode gerar um problema lá na frente, possivelmente no banco de dados.

Abaixo um exemplo de bom e outro de mau uso da classe:

// Programador que conhece a classe
$customer = new Customer('John Doe', '1983-02-10');

// Programador que não conhece a classe
// pode gerar um 0000-00-00 no Database
$customer = new Customer('John Doe', '10/02/1983'); 

Poderíamos então usar duas classes que farão envolvimento no tipo string, por exemplo:

  • CustomerName: que cuidará de validação de nome de cliente, pode verificar tamanho, fazer trim, e até limpeza.
  • CustomerBithday: Mais importante que o nome, ela vai validar o formato da data ou até mesmo formatar a entrada como, por exemplo, converter 10/02/1983 para 1983-02-10, evitando assim um problema de inconsistência, lembrando que não precisa ser necessariamente uma classe e, seguindo o princípio de inversão de dependência, podemos facilmente trabalhar com interfaces seguindo estratégias.

Como ficaria após a implementação dessas classes:

class Customer
{
    protected $name;
    protected $birthday;

    public function __construct(CustomerName $name, CustomerBirthday $birthday)
    {
        $this->name = $name;
        $this->birthday = $birthday;
    }
}

Usando:

// Programador que conhece a classe
$customer = new Customer(
     new CustomerName('John Doe'), 
     new CustomerBirthday('1983-02-10')
);

// A data será formatada internamente
$customer = new Customer(
    new CustomerName('John Doe'), 
    new CustomerBirthday('10/02/1983')
);

Um possível problema dessa abordagem é que ela adiciona complexidade à base de código. Na tradução dos Object Calisthenics, é colocado que todos os tipos primitivos devem ser envolvidos em classes, porém, sabemos que em PHP isso pode se tornar improdutivo e desnecessário, portanto, analise o quanto aquela entrada ou tipo pode sofrer mudança, se precisa de validação, normalização etc, só aplique-o se tiver uma real justificativa.

4. Envolva suas collections em classes

Semelhante ao exercício anterior, devemos envolver nossas coleções. Isso significa que trabalhar com um CustomerList é melhor do que com um array, neste caso, o uso dará uma melhor flexibilidade para o tratamento da coleção.

Abaixo uma usabilidade com e outra sem coleção em classe:

// A lógica fica fora, o que pode trazer problemas futuros
foreach ($customers as $customer) {
    if ($customer->isGoldAccount()) {
        $customer->addBonus(new Money('R$ 50,00'));
    }
}

Nesse exemplo queremos adicionar um bônus aos clientes do tipo gold. $customers é um array e por isso para modificar a coleção, precisamos iterá-la com um foreach e ainda internamente verificar se o tipo do cliente é gold.

Se usarmos uma classe que envolve a coleção, ou seja, uma CustomerCollection ela poderá ter 2 métodos:

  • Para filtragem de tipos de clientes: filterGoldAccounts.
  • Para adicionar aos clientes o bônus: addBonus.

A usabilidade ficaria assim:

$customersCollection = new CustomersCollection; // Classe com Lazy Loading

// Filtramos os clientes de conta Gold
$goldCustomers = $customersCollection->filterGoldAccounts();

// Adicionamos pela collection o bonus de R$ 50,00
// filtrado pela classe de coleção
$goldCustomers->addBonus(new Money('R$ 50,00'));

// Por fim persistimos
$goldCustomers->persists();

Assim, temos coleções que são específicas e inteligentes o suficiente para melhorar a usabilidade e evitar erros de programação. No PHP temos um conjunto de classes padrão para lidar com listas, a SPL(Standard PHP Library) que tem uma sessão dedicada a iteradores.

5. Uma chamada de método por linha

Devemos sempre fazer uma chamada de método por linha, não se aplicando à bibliotecas que usam do padrão Method Chaining ou DSL(Domain Specific Language).

Para seguir esse exercício não devemos, por exemplo, ao desenvolver um conjunto de Models, relacioná-los com métodos em cadeia, isso pode ser uma péssima ideia, segue um exemplo:

$customer->getById(55988)
         ->getPurchase(18376)
         ->getProducts()
         ->filterById(234);

Queremos resgatar um produto do pedido de um cliente, pode-se parecer muito prático, porém, alguns problemas poderão ocorrer e é muito difícil testar um bloco desses. Como saber se getPurchase encontrou o pedido? E se não encontrou, o que acontece? Nesse caso, vem outra problemática: e se o pedido não contém itens ainda? E temos um retorno null, certamente teremos um erro de método não encontrado.

Por isso, para garantir que tudo ocorreu certo, podemos seguir a Lei de Demeter, ela diz que devemos somente conversar com classes próximas, então, criamos um método para conversar e filtrar o que precisamos, ao invés de percorrer pelos Models que estão distantes. Não focaremos na implementação, porém, o conceito de uso abaixo pode ilustrar essa aproximação:

// Resgatando o model isoladamente
$customer = $customerModel->getById(55988);

// Aproximação: método que pertence a Customer
// sua implementação cuidará de retornos nulls
$product = $customer->getPuchasedProduct(18376, 234);

O principal objetivo desse exercício é não sair percorrendo por objetos retornados em chamadas de métodos, usar chamadas de vários métodos em linha pode gerar muitos problemas de manutenção, dificuldade de entendimento e testes mal escritos. Costuma-se dizer que gera um código que “cheira mal”.

Conclusão

Esses exercícios são mais aprofundados e devem ser estudados com calma, não devemos seguí-los somente por que parece ser o certo, devemos entender a motivação por trás deles, lembrando que nenhuma dessas técnicas são balas de prata e vão servir para toda modelagem, o bom senso é a melhor direção.

Até o próximo artigo da série!

PHP Intermediário
Curso de PHP Intermediário
CONHEÇA O CURSO

Object Calisthenics em PHP – Parte 1

O conceito de object calisthenic foi criado pelo desenvolvedor Jeff Bay, que teve a visão de que praticar código reutilizável, limpo e de boa leitura é uma prática que necessita de muito exercício, ou seja, muita prática e constante melhoria e evolução através de ações e práticas.

Ele é composto por nove exercícios que constituem a aplicação dos mesmos ao se escrever código em orientação a objetos.

Object vem da programação orientada a objetos, e calisthenics do termo grego, Kales, simplificando, seria a forma de se obter um físico ou, no caso, um resultado a partir da prática de exercícios que deixarão seu código em “forma”.

Ao todo são nove regras básicas, veja abaixo:

  1. Um nível de indentação por método;
  2. Não use ELSE;
  3. Envolva seus tipos primitivos;
  4. Envolva suas collections em classes;
  5. Uma chamada de método por linha;
  6. Não abrevie;
  7. Mantenha as classes pequenas;
  8. Não tenha classes com mais de duas variáveis de instancia;
  9. Sem getters e setters;

Veremos neste artigo as duas primeiras que são exercícios fortíssimos que fazem já muita diferença quando praticados.

PHP Intermediário
Curso de PHP Intermediário
CONHEÇA O CURSO

1. Um nível de indentação por método

Como já especificado devemos internamente dentro de uma função usar somente um nível de indentação, quanto mais níveis de comandos de decisão ou estruturas de decisão, mais complexo o método fica, minando com a simplicidade do projeto.

Abaixo podemos ver um exemplo disso:

Código sem a regra de indentação:

class Customer
{
    public function getPromoCode(string $promoName)
    {
        // 1.
        if ($this->promoCode) {
            // 2.
            if (false === $this->promoCodeExpired()) {
                // 3.
                if ($this->promoName == $promoname) {
                    return $this->promoCode;
                } else {
                    throw new Exception('Promoção não existe mais');
                }
            } else {
                throw new Exception('Promoção Expirada');
            }      
        } else {
            throw new Exception('Cliente sem código de promoção');
        }
    }
}

Uma forma de “treinar” esse exercício é criando métodos protegidos auxiliares que tenham um motivo e reaproveitamento, atuando na facilitação da escrita e colocando os blocos dentro das funções de apoio.

Veja como resolvemos o problema acima:

class Customer
{
    public function getPromoCode(string $promoName)
    {
        if ($this->promoCode) {
            return $this->getValidPromoCode($promoName);
        } else {
            throw new Exception('Cliente sem código de promoção');
        }
    }

    protected function getValidPromoCode(string $promoName)
    {
        if (false === $this->promoCodeExpired()) {
            return $this->getPromoExists($promoName);
        } else {
            throw new Exception('Promoção Expirada');
        }    
    }

    protected function getPromoExists(string $promoName)
    {
        if ($this->promoName == $promoName) {
            return $this->promoCode;
        } else {
            throw new Exception('Promoção não existe mais');
        }
    }
}

2. Não use ELSE

Essa regra parece ser muito estranha, mas ela funciona muito bem com o conceito early return, que emprega o uso do “retorne seu valor o quanto antes”, ação que só é facilmente implementada dentro de funções, métodos ou loops.

A base deste exercício é sempre trabalhar com o return (ou continue), sabemos que ao cair em um return/continue o código abaixo não será executado o que ajuda na remoção dos “elses” ao inverter ou até modificar a validação antes usada.

Abaixo o código anterior com early return:

class Customer
{
    public function getPromoCode(string $promoName)
    {
        if ($this->promoCode) {
            return $this->getValidPromoCode($promoName);
        } 
        throw new Exception('Cliente sem código de promoção');
    }

    public function getValidPromoCode(string $promoName)
    {
        if (false === $this->promoCodeExpired()) {
            return $this->getPromoExists($promoName);
        }
        throw new Exception('Promoção Expirada'); 
    }

    public function getPromoExists(string $promoName)
    {
        if ($this->promoName == $promoName) {
            return $this->promoCode;
        }
        throw new Exception('Promoção não existe mais');
    }
}

Como trocamos os comandos de decisões para que fique um código limpo removemos os “else’s”, e simplificamos a lógica que passa a ser melhor compreendida e limpa, esse conceito também pode ser aplicador para loops, onde o intuito é usar o continue no lugar do “else”.

Vejamos abaixo um exemplo:

Antes:

// Exibir a lista de membros
// que pagaram a mensalidade
foreach ($members as $member) {
    if ($member->paid()) {
        $report[] = [$member->name => 'Paid'];
    } else {
        $report[] = [$member->name => 'Not Paid'];
    }
}

Depois:

// Sem Else
foreach ($members as $member) {
    if ($member->paid()) {
        $report[] = [$member->name => 'Paid'];
        continue;
    }
    $report[] = [$member->name => 'Not Paid'];
}

Ou até com um pouco mais de limpeza:

// Um pouco mais de limpeza
foreach ($members as $member) {
    $report[] = $member->paid() ? 
                    [$member->name => 'Paid'] : 
                    [$member->name => 'Not Paid'];
}

Conclusão

Veremos em breve as outras regras numa série de posts. O uso do object calisthenics trás muitos benefícios que em grande escala fazem uma grande diferença, comece esses exercícios e terá seu código elogiado e muito mais elegante.

Obviamente nem sempre é possível de imediato “atacar” seu código com os calisthenics, mas com refatorações isso começa a ficar natural e quanto mais prática mais natural fica a aplicação desses exercícios.

Espero que tenham gostado e nos vemos nos próximos posts. Bons estudos!

PHP Avançado
Curso de PHP Avançado
CONHEÇA O CURSO

Introdução aos princípios SOLID

Muito provavelmente você já ouviu falar dos tais princípios SOLID enquanto você estudava orientação a objetos. Mas, afinal de contas, para quê servem e onde podemos utilizá-los? Qual é a real importância destes princípios na programação orientada a objetos? O que posso obter de vantagens se eu utilizar os tais princípios SOLID?

Webpack - Empacotando aplicações
Curso de Webpack - Empacotando aplicações
CONHEÇA O CURSO

O que são os princípios SOLID?

É importante frisarmos que os princípios SOLID não constituem uma linguagem, nem uma tecnologia, nem um framework, nem uma biblioteca, nem uma arquitetura, nem um paradigma. Os princípios SOLID são apenas… princípios, haha. Eles constituem algumas constatações que podem fazer você programar mais próximo da orientação a objetos.

Estes princípios surgiram no início dos anos 2000 e foram definidos pelo engenheiro de software Robert Martin (também conhecido como Uncle Bob), sendo que a sigla que tornou estes princípios tão populares foi criada por Michael Feathers.

Os princípios SOLID tentam definir de maneira efetiva as bases para que a orientação a objetos seja efetivamente aplicada no mundo real, trazendo a possibilidade da construção de aplicações e sistemas que sejam fáceis de se manter e evoluir com o passar do tempo – este é um dos grandes desafios da arquitetura e engenharia de software até hoje!

Quais são estes princípios?

SOLID na verdade é um acrônimo que denomina cinco princípios:

  • Single Responsability Principle ou Princípio da Responsabilidade Única;
  • Open/closed Principle* ou Princípio do aberto/fechado;
  • Liskov Substitution Principle ou Princípio da Substituição de Liskov;
  • Interface Segregation Principle ou Princípio da Segregação de Interfaces;
  • Dependency Inversion Principle ou Princípio da Inversão de Dependências.

Não se preocupe no momento com cada um deles em especial… Nos próximos artigos, abordaremos cada um deles, inclusive com exemplos práticos. 😉

Mas, para quê eu deveria me atentar aos princípios SOLID?

Essa é uma pergunta que pode ser interessante. De fato, você não é obrigado a seguir os princípios SOLID necessariamente para programar com uma linguagem orientada a objetos (embora, sem querer, você vá acabar usando pelo menos um dos princípios em algum momento).

Mas, não tem sentido também não nos atentarmos a estes princípios se eles visam nos guiar para aplicarmos a Orientação a Objetos corretamente, obtendo manutenibilidade e extensibilidade do nosso código da maneira correta. Se estamos nos propondo a utilizar uma linguagem orientada a objetos, eu particularmente acho importante que nós tenhamos a capacidade de absorver o que é proposto pelo paradigma para aplicá-lo da maneira mais correta possível. Sendo assim, por que não entender os princípios SOLID?

Eu gosto de ilustrar o porquê de nos atentarmos a eles com a ilustração abaixo:

As ferramentas acima estão, nitidamente, sendo utilizadas de forma incorreta, não? Pois é… Mas quantas vezes nós não escrevemos código, mesmo que sem querer, que no final tínhamos uma situação parecida com essa? O código funcionava? Até funcionava… Da melhor maneira possível? Não necessariamente. Era fácil dar manutenção no código? Aí já era outra história, hahaha.
Os princípios SOLID nos auxiliam a justamente evitar situações como as da imagem acima dentro do nosso código.

Os princípios SOLID são a salvação do universo?

Não, não são. Por várias vezes, você vai se encontrar no dilema de ou aplicar corretamente algum dos princípios ou tornar o seu código mais claro. Você até mesmo provavelmente irá chegar na situação de ou aplicar os princípios SOLID ou entregar o projeto no prazo determinado! Mas não, eles não constituem a salvação do seu software.

Existe também o fato de que você provavelmente não vá utilizar somente orientação a objetos em seu projeto. Assim como os princípios SOLID, a orientação a objetos não resolve todos os problemas de arquitetura e engenharia de software do mundo.

É por isso que temos outros paradigmas de desenvolvimento que podem ser muito mais úteis em determinadas situações, como o paradigma orientado a eventos, o paradigma funcional (este que vem ganhando muito mais popularidade a cada ano que passa) e o paradigma orientado a aspectos. Cada um deles é bom em resolver determinada situação e ruim para resolver outras. O legal é quando chegamos a um nível de maturidade onde conseguimos enxergar o melhor de cada um destes paradigmas e conseguimos aplicá-los em conjunto.

Mas, voltando aos princípios SOLID, se você provavelmente não vai utilizar somente orientação a objetos, isso quer dizer que você também não utilizará somente os princípios SOLID.

Nos próximos posts, iremos analisar e entender cada um dos cinco princípios SOLID. Verificaremos para que cada um deles serve, que problemas eles resolvem e, inclusive, teremos exemplos de implementações com “código de verdade”.

Até o próximo post da série! 😉

C Intermediário
Curso de C Intermediário
CONHEÇA O CURSO

Devo usar herança ou composição?

Sempre que você vê class {CLASS_NAME} extends {PARENT_CLASS_NAME}, está diante de uma herança direta de classe, também conhecida como “herança clássica”, uma herança vertical onde as implementações e as especificações descem na “árvore”.

Um diagrama de um caso didático de herança:

Se levado para um pseudo-código que usa o paradigma de orientação a objetos, poderíamos ter:

class Animal
{
   // TODO
}

class Gato extends Animal
{
   // TODO
}

class Coelho extends Animal
{
   // TODO
}

class Cachorro extends Animal
{
   // TODO
}

A herança clássica nos leva as dois benefícios:

  • Reuso de contrato (especificação);
  • Reuso de código (implementação);

Se você já estudou orientação a objetos, em qualquer linguagem que seja, já deve ter “saturado” na mente a ideia de que a herança evita duplicação de código, uma vez que ela herda da classe pai suas partes concretas (implementações) e seus contratos/interfaces (especificações). No entanto, reuso de código por si só não é motivo para se usar herança. Reuso é consequência do uso de herança. Esse conceito é muito importante de se ter.

A herança faz sentido quando se tem a necessidade de representar um tipo, de tal forma que, quando essa classe é estendida, o seu tipo também é, podendo criar, inclusive, um novo sub-tipo.

No caso do nosso diagrama, temos a seguinte relação:

  • Animal é a classe pai, o topo da hierarquia.
  • Gato estende Animal, logo, Gato é do tipo Animal.
  • Cachorro estende Animal, logo Cachorro é um Animal (só que mais específico);

Lidamos com uma inferência lógica: Gato estende Animal, logo, Gato também é um Animal.

Outra relação importante que temos na herança refere-se à generalização e à especificação. Quanto mais “alto” na árvore, mais genérico é, quanto mais baixo, mais específico. Generalizar é o ato de reunir características comuns numa classe de tal forma que ela possa servir de extensão para outras. No lado oposto, especificar é tornar específico, ou seja, com características mais singulares e que não devem estar na classe pai, pois, normalmente, são essas características que dão o valor de sub-tipo à classe em questão. A relação que temos é que, classes pai são classes de generalização e subclasses normalmente são classes de especialização.

Ainda dentro do nosso exemplo, a classe Animal deve ser, em teoria, a parte mais genérica e as subclasses Gato, Coelho e Cachorro devem ser mais específicas. Por exemplo, um método latir() não poderia estar na classe Animal para ser estendido para as outras, uma vez que essa é uma característica específica dos cachorros.

A herança clássica é um mecanismo útil e não deve ser demonizada. O grande problema é o seu mau uso, e se dá, normalmente, quando precisamos trazer características de outra classe para a nossa e, nesse caso, a primeira coisa que costuma vir à mente é a opção de “herdá-la” ao invés de pensarmos em objetos que comunicam entre sí.

Se a nossa classe não é do tipo da outra, herança não deve ser utilizada. Quando estendemos uma classe, não herdamos apenas a sua implementação e sua especificação, herdamos também o seu tipo. Logo, se uma classe não é do tipo da outra, a herança não é o mecanismo adequado.

É comum esse tipo de construção:

class MySqlConnection
{
   // TODO
}

class Usuario extends MySqlConnection
{
  // TODO
}

A classe Usuario estende a MySqlConnection. Mas, Usuario não é uma MySqlConnection, não é do tipo MySqlConnection. Esse tipo de erro é muito comum pois, como desenvolvedores, tendemos a achar que apenas a herança pode resolver os nossos problemas de reuso de código.

Esse exemplo, por sinal, é um caso para composição, de tal forma que saímos do contexto de “ser” para o contexto de “usar”. Usuario não deve ser MySqlConnection, ele precisa usar MySqlConnection.

Problemas que são evidenciados na super utilização de herança (em excesso):

  • Mudar a classe pai pode afetar todas as classes filhas, mesmo quando isso não for intencional. O projeto fica um pouco menos previsível.
  • O encapsulamento costuma ficar fraco (o acoplamento é forte, com uma tendo muito conhecimento sobre a outra, às vezes conhecimento até específico demais).
  • A herança é um relacionamento estático, não é possível mudar em tempo de execução.

Composição

Antes, um disclaimer: Nesse artigo, quando eu falo em composição, eu quero dizer: um objeto que usa outro objeto independentemente da relação associativa entre eles. Aqui é pouco importante separar a relação (se é uma agregação simples ou uma agregação de composição etc).

A composição também é uma forma de extensão, só que pela delegação de trabalho para outro objeto. Diferente da herança clássica onde tipo, atributos e métodos são estendidos. Quando uma classe precisa usar o comportamento de outra, usualmente é melhor usar composição.

Em termos gerais, se o seu problema não é caso para herança clássica, ele será um caso para composição (sem entrar aqui na possibilidade de se usar traits, um mecanismo de herança horizontal disponível em algumas linguagens).

Alguns dos pontos positivos do uso de composição:

  • Facilita a mudança da associação entre classes em tempo de execução;
  • Permite que um objeto assuma múltiplos comportamentos com decisões em tempo de execução;

Voltando ao nosso exemplo, a melhor relação que podemos ter:

E, traduzindo para código, poderíamos ter essa implementação:

class MySqlConnection
{
   // TODO
}

class Usuario
{
    public function findById(int $id) : array
    {
        $connection = new MySqlConnection();

        // TODO
    }
}

Usuario não mais estende MySqlConnection, ao invés disso, ele instancia essa classe e utiliza o objeto resultante.

Mas, priorizando injeção de dependência e o princípio de inversão de dependência, a melhor alternativa seria construimos esse exemplo assim:

class MySqlConnection implements Connection
{
   // TODO
}

class Usuario
{
    protected $connection;

    public function __construct(Connection $connection)
    {
        $this->connection = $connection;
    }

    public function findById(int $id)
    {
        $this->connection->select('...');

        // TODO
    }
}

$connection = new MySqlConnection();
$myUser = new Usuario($connection);

Assim como no primeiro exemplo, nesse também temos composição (no sentido de um objeto A que usa um objeto B). Só mudamos a forma deles se relacionarem. No primeiro exemplo temos uma associação de agregação de composição e nesse, injetamos MySqlConnection como dependência de Usuario, numa associação de agregação (onde a parte – MySqlConnection – não depende do todo – Usuario – para existir).

Concluindo

Não faz sentido polarizar e afirmar que você deve usar uma em detrimento a outra. Composição e herança podem coexistir cada qual com seu caso de uso. Por outro lado, o uso de composição é sempre encorajado. Não exatamente no sentido de fazer o desenvolvedor “parar” de usar herança, mas no sentido de que, tendo mais objetos inter-relacionados (não no sentido de quantidade, mas no de qualidade de comportamento), as responsabilidades são melhores delegadas, favorece o reuso, a refatoração, a testabilidade e até mesmo a mudança de associação em tempo de execução.

Muitos objetos dificultam o relacionamento e até mesmo o gerenciamento deles como dependências e o código fica bem menos estático (menos previsível). No entanto, há mecanismos que nos ajudam bastante na resolução desses problemas, recomendo a leitura dos seguintes artigos:

Um abraço!

Precisamos falar sobre o TypeScript

Ultimamente muita gente tem torcido o nariz para um novo superset do JavaScript. Sim, estamos falando do TypeScript.

O TypeScript é um caso interessante. Percebo ultimamente que, ou você o adora (o que é meu caso) ou você simplesmente o odeia. Parece não haver um consenso intermediário de maneira geral na comunidade. Por que será que isso acontece? Será que existem conceitos errôneos difundidos na comunidade sobre o TypeScript? Será que ainda há pontos cruciais a serem melhorados nele? Vamos discorrer sobre isso! 🙂

TypeScript - Primeiros Passos
Curso de TypeScript - Primeiros Passos
CONHEÇA O CURSO

É da Microsoft…

Acho que esse ponto talvez seja o mais complicado de se discutir. Muita gente simplesmente tem ódio do TypeScript simplesmente por ele ser da Microsoft. Em minha opinião, acho que é simplesmente perda de tempo deixar de utilizar alguma tecnologia por ela ser de alguma empresa específica. Acho que a qualidade técnica e a maneira como a companhia conduz a plataforma, além dos fatores externos (infraestrutura, contratos comerciais, etc.), deveriam ser os fatores cruciais para se decidir se determinado framework/linguagem/plataforma será adotada ou não. Porém, sabemos que por muitas vezes a paixão toma lugar da razão nesse tipo de tomada de decisão (eu comentei um pouco sobre esse tipo de cenário no artigo Não seja um profissional “intolerante”.

Eu concordo que a conduta que a Microsoft tinha há algum tempo não era a mais adequada com relação aos desenvolvedores. Mas também precisamos reconhecer que essa postura nos últimos anos mudou da água para o vinho! Em pouco mais de cinco anos, a Microsoft se tornou a maior contribuidora em software open source no GitHub. Duvida? Você pode comprovar através do próprio GitHub.

A Microsoft também se tornou uma das principais contribuidoras do kernel Linux. Ela ainda tem disponibilizado uma série de ferramentas que antes eram licenciadas (caso do Visual Studio e do Xamarin, por exemplo) de maneira completamente gratuita. As principais plataformas da Microsoft (o que inclui toda a plataforma do .NET Core e o próprio TypeScript) estão abertos no GitHub. Não há mais motivos para achar que a Microsoft “tem um plano maquiavélico/diabólico/capitalista de dominar a indústria de software” (embora esse acabe sendo um objetivo indireto de qualquer empresa no fim das contas).

A Microsoft mostra atualmente que já entendeu que ela precisa se tornar parte do mundo open source, além de já ter provado que sim, ela tem muita coisa interessante para a comunidade. Para mim, isso tudo reforça que essa mania de falar que o que é da Microsoft é ruim deveria já ter ficado no passado.

O TypeScript não quer substituir o JavaScript

Algumas pessoas que eu conheço têm pavor do TypeScript porque acham que ele vem para substituir o JavaScript, o que é um equívoco. O TypeScript nunca teve a intenção de substituir completamente o JavaScript. A própria documentação diz que ele é na verdade um superset do JavaScript. E o que isso quer dizer? Isso quer dizer que ele visa fornecer alguns recursos que o JavaScript não oferece por padrão. Entre esses recursos, temos por exemplo:

  • A oportunidade de termos tipos de dados mais definidos (tornando o JavaScript um pouco mais tipado);
  • Uma sintaxe e estrutura fortemente orientados à objetos (o TypeScript é fortemente influenciado pelo C# e F#), aproximando a experiência de desenvolvimento de linguagens tradicionalmente orientada a objetos como o C# e o Java;

Essas vantagens, por decorrência, trazem outras vantagens:

Por causa da existência de tipagem e também de uma sintaxe mais clara, as IDEs ganham a possibilidade de aprimorarem seus mecanismos de detecção de erros e de sintaxe em tempo de desenvolvimento. Isso quer dizer que, usando TypeScript, sua IDE poderá lhe avisar de alguma passagem de tipo incorreta ou mesmo sobre alguma sintaxe incorreta enquanto você ainda escreve o código. Ferramentas como o IntelliSense (conhecido por muitos como AutoComplete) também podem ser bem mais efetivas nesse cenário. Não quero dizer que, quando utilizamos JavaScript “convencional”, as IDEs não ofereçam esse tipo de suporte, mas o suporte das IDEs para o TypeScript é ou pelo menos tem a grande tendência de ser bem mais aprimorado;

Ainda temos, como decorrência do ponto anterior, a possibilidade de que o código escrito com TypeScript seja bem mais robusto, seguro e fácil para ser testado. Isso tudo, mais uma vez, graças aos conceitos de linguagens estáticas e tipadas que o TypeScript trouxe para o mundo das linguagens funcionais, na minha opinião, no nível certo. TypeScript não é JavaScript simplesmente tipado e estático, ele torna possível utilizar o melhor do mundo de linguagens de tipagem fraca e funcionais e do mundo de linguagens estáticas e de tipagem forte;

Aqui já é uma opinião bem mais pessoal, mas eu prefiro a sintaxe do TypeScript que a sintaxe do JavaScript, principalmente no que tange à orientação a objetos. Eu gosto das “regras de sintaxe” e a pseudo-tipagem que o TypeScript traz para o JavaScript. Ou você realmente acha que isso:

var __extends = (this && this.__extends) || function (d, b) {
    for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p];
    function __() { this.constructor = d; }
    d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
var Animal = (function () {
    function Animal(name) {
        this.name = name;
    }
    Animal.prototype.move = function (distanceInMeters) {
        if (distanceInMeters === void 0) { distanceInMeters = 0; }
        console.log(this.name + " moved " + distanceInMeters + "m.");
    };
    return Animal;
}());
var Snake = (function (_super) {
    __extends(Snake, _super);
    function Snake(name) {
        return _super.call(this, name) || this;
    }
    Snake.prototype.move = function (distanceInMeters) {
        if (distanceInMeters === void 0) { distanceInMeters = 5; }
        console.log("Slithering...");
        _super.prototype.move.call(this, distanceInMeters);
    };
    return Snake;
}(Animal));

… é mais claro que isso?

class Animal {
    constructor(public name: string) { }
    move(distanceInMeters: number = 0) {
        console.log(`${this.name} moved ${distanceInMeters}m.`);
    }
}

class Snake extends Animal {
    constructor(name: string) { super(name); }
    move(distanceInMeters = 5) {
        console.log("Slithering...");
        super.move(distanceInMeters);
    }
}

Agora, nada impede de você utilizar JavaScript “tradicional” em conjunto com o TypeScript, mesmo porque no final o TypeScript é transpilado para JavaScript! No final, tudo vira JavaScript… Veja que no exemplo acima mesmo isso ocorre quando utilizamos o tradicional console no meio do código TypeScript. Isso reforça que a idéia da Microsoft não é substituir o JavaScript, nem nunca vai ser! A idéia é podermos ter a possibilidade de utilizar um ferramental mais poderoso para produzir um código mais robusto e legível.

Agora terei um código muito verboso

Não há como negar isso. As características técnicas (tipagem, vocação para orientação à objetos, etc.) fazem naturalmente que o código TypeScript torne-se um pouco mais verboso e com menos vocação para o lado funcional, principalmente para quem vem de linguagens fracamente tipadas e/ou funcionais. Na verdade, no final, o TypeScript faz uma mistura entre os conceitos dos dois paradigmas (funcional e orientado a objeto) e dos dois sistemas de tipagem (fracamente tipado e fortemente tipado).

E aí, já viu: isso pode provocar tanto admiração como também raiva. Algumas pessoas podem gostar bastante dessa mistura (meu caso) ou outras podem simplesmente odiar (já que o TypeScript corre o sério risco de desagradar tanto os adeptos do paradigma funcional como também do paradigma orientado à objetos). Temos verbosidade no TypeScript sim, mas ainda sim temos aspectos funcionais também presentes.

O TypeScript ainda não consegue lidar bem com todas as bibliotecas JavaScript

Para que o TypeScript consiga “conversar” com as APIs de outras bibliotecas (por exemplo, o jQuery), ele precisa de um arquivo com a extensão **.d.ts*. Este arquivo, conhecido como TypeScript Type Definition, traz os mapeamentos necessários para que através do TypeScript você possa acessar e utilizar os métodos expostos por uma biblioteca externa, mesmo que esta não tenha sido escrita com o TypeScript. Esse arquivo acaba funcionando como uma ponte entre uma biblioteca externa e o TypeScript.

Aí reside o problema: nem todas as bibliotecas possuem esse arquivo que as prepara para serem consumidas por código TypeScript. As mais famosas bibliotecas já possuem este arquivo preparado, mas se você utiliza uma biblioteca um pouco menos conhecida ou possui uma biblioteca proprietária, ela pode não ser de cara compatível com um código TypeScript de maneira completa. Nesse caso, não há outro cenário: será necessário que você mesmo escreva este arquivo de definições ou abra mão do TypeScript. E isso, de fato, pode vir a ser um empecilho dependendo do projeto…

O TypeScript não é o salvador da pátria

alt

O TypeScript pode trazer algumas confusões com relação a sua utilização… Já que o código TypeScript no final é sempre transpilado para um código JavaScript “normal”, podemos dizer que todo código JavaScript também é código TypeScript! Isso é legal porque justamente conseguimos ainda ter as características legais do JavaScript com o poder do TypeScript.

Por isso mesmo que o TypeScript não visa substituir o JavaScript, e sim o complementar. Algumas pessoas têm comentado ultimamente, chegando ao (absurdo, na minha opinião) de dizer que hoje TypeScript é a linguagem de alto nível enquanto o JavaScript é o assembly.

É cedo demais para dizer isso; assim como, em minha opinião, nem sempre o TypeScript pode ser a melhor saída. Para se escolher se você deve usar TypeScript ou não, não se esqueça de levar em consideração a quebra de paradigmas que ele pode causar na equipe de desenvolvimento, bem como o suporte às bibliotecas que você pretende utilizar. E esses são somente dois pontos que devemos considerar… TypeScript não é uma bala de prata..

Afinal, devo utilizar TypeScript ou não?

Mais uma vez, essa é uma questão muito subjetiva, onde não temos o certo e o errado. Tudo vai depender da situação como um todo. Eu gosto da idéia do TypeScript e o utilizo sem problema nenhum: o sistema de tipagem, a sintaxe mais clara e a aliança com o JavaScript (afinal, ainda temos JavaScript como produto final) me agradam bastante. Mas você pode ter uma opinião diferente… No final, nenhum de nós vai estar 100% certo ou 100% errado, hehe.

Agora, eu não acho perda de tempo estudar TypeScript. Para mim, está claro que ele é algo que veio para ficar. Além de tudo, ele ainda está muito alinhado com o ECMA 6, tendo muitos recursos que o ECMA 6 propõe. E tem mais um tempo: alguns frameworks, como Angular e Ionic, estão adotando o TypeScript, assim como outros estão pelo menos oferecendo suporte a ele. Por essas e outras, o TypeScript veio para ficar.

Melhor para nós! Mais uma opção de ferramenta legal que podemos utilizar em nossos projetos. 🙂 Que a guerra dos frameworks/bibliotecas/supersets do JavaScript continue! o/

Você quer aprender TypeScript? Veja esse nosso incrível curso:

TypeScript - Primeiros Passos
Curso de TypeScript - Primeiros Passos
CONHEÇA O CURSO

Para que servem os métodos ToString(), Equals() e GetHashCode()?

Muitas pessoas costumam se confundir com três métodos que qualquer classe possui no .NET e no Java: os métodos ToString(), Equals() e GetHashCode(). Os dois primeiros são mais ou menos claros até certo ponto, mas o último vive causando confusão e pânico entre desenvolvedores Java e C#, ainda mais na hora em que necessitamos sobrescrevê-los… Que tal entendermos de uma vez por todas para que estes três métodos servem?

Importante: os conceitos que veremos aqui servem tanto para a plataforma Java quanto para a plataforma .NET! 😀

C# (C Sharp) - Introdução ao ASP.NET Core
Curso de C# (C Sharp) - Introdução ao ASP.NET Core
CONHEÇA O CURSO
Por que toda classe tem estes métodos?

No Java e no .NET, toda classe tem um ancestral por “padrão”: a classe Object. Dessa maneira, qualquer objeto de qualquer classe que criarmos em Java ou .NET, pelo Princípio da Substituição de Liskov, também será uma instância de Object. E a classe Object já expõe por padrão pelo menos os três métodos que estão em discussão.

Java Básico
Curso de Java Básico
CONHEÇA O CURSO
O método ToString()

Talvez este seja o método que é mais claro com relação ao seu propósito. Seu objetivo é trazer uma representação textual de uma instância de um objeto.
Essa representação textual de um objeto vem a ser muito útil principalmente em situações de debugging e de logging. Isso ocorre porque os métodos de saída para o streamming padrão (os famosos System.out.print[ln]() ou Console.Write[Line]()), assim como os principais métodos de praticamente todas as APIs de log (métodos como o debug() e info()) sempre chamam por padrão o método ToString() de instâncias de objetos que tenham sido passadas para eles.

O exemplo abaixo está escrito em C#, mas o princípio é exatamente o mesmo para o Java também, rs.

Imagine o código abaixo:

namespace TreinaWeb.Exemplo
{
    public class Pessoa
    {
        public string Nome { get; set; }
    }
}
// ...
Pessoa p = new Pessoa { Nome = "TreinaWeb" };
Console.WriteLine(p);
// ou
Console.WriteLine(p.ToString());

Perceba que não foi realizada nenhuma sobrecarga do método ToString(), o que faz com que a chamada deste método para a classe Pessoa encaminhe a chamada para a classe ancestral, ou seja: na verdade será chamado o método Object.ToString().

Não se esqueça: ==todo e qualquer objeto/classe sempre vai ter o método ToString() por causa da herança da classe Object==. Se a própria classe não implementa este método, a chamada será encaminhada para o ToString() “padrão”, ou seja, o método ToString() da classe Object.

No caso do C#, a saída do código abaixo seria a representada abaixo, independente da chamada explícita ao método ToString() ou não:

TreinaWeb.Exemplo.Pessoa

A saída tanto para a chamada explícita ao método ToString() como para a chamada ocultando-se a chamada é a mesma. Isso ocorre porque os método Console.Write[Line]() chama o método ToString() da instância repassada como parâmetro por padrão. 😉

O que o .NET emite como saída nesses casos é um elemento conhecido como Full Qualified Name, ou simplesmente FQN. Apesar de o nome assustar, ele é muito simples: trata-se somente do “nome completo” e único da classe, que é composto por namespace + nome da classe.

Se tivéssemos o código equivalente em Java, teríamos uma saída similar à abaixo:

TreinaWeb.Exemplo.Pessoa@1033203

A saída é muito similar ao FQN do .NET, com o acréscimo deste número precedido por @. Costumam dizer por aí que este número logo após o @ é a posição de memória do objeto. Daqui a pouco vamos ver que não é “beeem” assim, hehe.

O grande ponto é que essa saída fornecida pelo método ToString() não tem nenhuma utilidade prática para nós, tanto no streamming padrão de saída, quanto em um arquivo de log. Daí vem a necessidade da sobrescrita deste método.

A idéia é que o método ToString() forneça uma representação simplificada e direta do estado do objeto em questão. Uma maneira de atingir este objetivo é fazer com que a saída forneça o valor atual dos atributos do objeto.

Poderíamos sobrescrever o método ToString() da nossa classe Pessoa da seguinte maneira:

namespace TreinaWeb.Exemplo
{
    public class Pessoa
    {
        public string Nome { get; set; }

        public override string ToString()
        {
            return string.Format("Pessoa [Nome = {0}]", this.Nome);
        }
    }
}

Nossa saída agora, tanto em Java quanto em .NET, seria a mesma abaixo:

Pessoa [Nome = TreinaWeb]

Agora temos uma representação “palpável” e que faça sentido para nós. Essa é com certeza uma representação muito melhor do que as representações padrão da classe Object.

O método Equals()

Antes de falarmos do método Equals(), precisamos relembrar um pouco sobre manipulação de memória, principalmente sobre stack e heap.

Se você quer relembrar como funciona a manipulação de memória, tanto em Java quanto em .NET, você pode ver este nosso artigo, onde tratamos sobre manipulação de memória, stack, heap, value-types e reference-types.

Quando criamos um objeto da classe Pessoa, este objeto será armazenado na memória heap:

Pessoa minhaPessoa = new Pessoa();

Alocação de memória: reference-type

Não se esqueça de que o compilador não acessa os objetos na heap de maneira direta por questões de performance. Sendo assim, o acesso a esse objeto armazenado na heap é feito através de uma referência dentro da stack para o objeto minhaPessoa, apontando onde na memória heap que este objeto está de fato guardado! Sim: estamos falando de ponteiros.

Acesso à memória: reference-types

É importante entendermos estes conceitos para entendermos melhor como o método Equals() funciona.

Quando comparamos objetos, é considerada uma boa prática utilizarmos o método Equals() para fazer a comparação de igualdade. E, quando a classe a qual os objetos em questão pertencem não sobrescreve o método Equals(), o método Object.Equals() será chamado.

Vamos verificar o código abaixo, considerando ainda a classe Pessoa:

namespace TreinaWeb.Exemplo
{
    public class Pessoa
    {
        public string Nome { get; set; }
    }
}
// ...
Pessoa p = new Pessoa { Nome = "TreinaWeb" };
Pessoa p2 = new Pessoa { Nome = "TreinaWeb" }; 

Console.WriteLine(p.Equals(p2));

Faria muito sentido se esse código fornecesse como saída true, afinal, os objetos aparentemente são iguais, certo? Mas esse código irá produzir false, apesar de o nome nos dois objetos serem iguais… Por que isso acontece?

Aí entra em cena a relação entre o método Object.Equals() e a maneira como a memória é manipulada no Java e no .NET.

A cada vez que utilizamos a keyword new, nós estamos instruindo o compilador a reservar um espaço na memória heap e criar um ponteiro na stack para que seja possível acessar esta área da heap. Sendo assim, no código acima, são criadas duas áreas de memória na heap distintas. Estas áreas são gerenciadas, respectivamente, pelos ponteiros p e p2.

O que ocorre quando utilizamos o Equals() baseado na implementação de Object.Equals() é que este por padrão verifica se os ponteiros ==apontam para a mesma área de memória na heap!== Como temos dois objetos instanciados de maneira distintas (chamamos o new para cada um dos ponteiros), nós temos também duas posições de memória distintas para cada um dos objetos p e p2. Por isso, por padrão, temos como resposta false para o código acima.

Agora, vamos imaginar o código abaixo:

namespace TreinaWeb.Exemplo
{
    public class Pessoa
    {
        public string Nome { get; set; }
    }
}
// ...
Pessoa p = new Pessoa { Nome = "TreinaWeb" };
Pessoa p2 = p; 

Console.WriteLine(p.Equals(p2));

Agora sim! Perceba que para p2 nós não instruímos o compilador a criar uma nova área de memória na heap, e sim o instruímos a fazer com que p2 aponte para a mesma posição de memória na heap para a qual p aponta. Desta maneira, neste último código, obteremos true como resposta quando utilizamos a implementação do método Equals() baseada em Object.Equals().

Agora, não faz muito sentido para nós esta implementação do método Equals() baseado em Object.Equals(). Para nós, faria muito mais sentido que o método Equals() na primeira situação deste tópico retornasse true, afinal, ambos os objetos Pessoa possuem o mesmo nome. Por isso, é importantíssimo para nós sobrescrevermos de maneira adequada o método Equals().

Antes de partirmos para a sobrecarga, é importante entendermos algumas premissas para o método Equals():

  • Se ambos os objetos que estão sendo comparados apontam para a mesma posição de memória, é mandatório que Equals() retorne true, baseado na implementação de Object.Equals();
  • x.Equals(x) sempre tem que retornar true;
  • x.Equals(y) sempre tem que retornar o mesmo que y.Equals(x). Este princípio é conhecido como princípio de simetria;
  • Se x.Equals(y) e y.Equals(z), z.Equals(x) tem que retornar true também. Isso ocorre por decorrência do princípio da simetria;
  • x.Equals(null) sempre será falso. O método Equals() por definição não pode retornar exceções do tipo NullReferenceException ou NullPointerException. Isso faz sentido: se um ponteiro é nulo, na verdade ele não aponta para nenhuma área da heap e, portanto, é impossível fazer uma comparação coerente entre os objetos.

Tendo todos estes princípios em vista, temos 3 pontos importantes a serem observados quando sobrescrevermos o método Equals() para que este atenda a todos estes requisitos:

  • Precisamos ver se um dos participantes da chamada do método Equals() é nulo;
  • Precisamos ver se os dois objetos apontam para a mesma área de memória;
  • Precisamos comparar o estado interno dos participantes da chamada do Equals().

Poderíamos sobrescrever o método Equals() da nossa classe Pessoa() da seguinte maneira, afim de que a nossa sobrecarga atenda aos requisitos essenciais:

namespace TreinaWeb.Exemplo
{
    public class Pessoa
    {
        public string Nome { get; set; }

        public override bool Equals(Object obj)
        {
            // Verificando se o segundo participante está nulo
            if (obj == null)
            {
                return false;
            }
            Pessoa p2 = obj as Pessoa;
            // Verificando se o cast foi realizado com sucesso. 
            // Caso não foi, obj nem é um objeto do tipo Pessoa 
            // e automaticamente o método tem que retornar false
            // NOTA: o operador de cast "as" retorna
            // null caso o cast não seja possível
            if (p2 == null)
            {
                return false;
            }
            // Vamos agora verificar se ambos apontam para a mesma posição 
            // de memória utilizando Object.Equals()
            if (base.Equals(obj))
            {
                return true;
            }
            // Agora comparamos o estado interno dos objetos!
            return this.Nome == p2.Nome;
        }
    }
}

Agora nós temos o método Equals() devidamente sobrescrito e respeitando todas as condições necessárias. Considerando esta sobrecarga, se chamarmos o código abaixo…

Pessoa p = new Pessoa { Nome = "TreinaWeb" };
Pessoa p2 = new Pessoa { Nome = "TreinaWeb" }; 
Pessoa p3 = p2;

Console.WriteLine(p.Equals(p2));
Console.WriteLine(p2.Equals(p3));
Console.WriteLine(p.Equals(p3));

… obteremos true em todas as saídas, o que faz muito sentido!

No caso específico do C#, nós ainda poderíamos utilizar p == p2, o que causaria também a chamada de Object.Equals(). Por isso, no C#, temos a possibilidade de fazermos a sobrescrita também de operadores. Nesta situação, precisaríamos sobrescrever o operador == para a classe Pessoa para que tenhamos tudo “nos trilhos” e não tenhamos duas implementações distintas de igualdade. Da mesma maneira, acaba sendo prudente sobrescrever também o operador !=.
Nosso código poderia ficar como está abaixo:

namespace TreinaWeb.Exemplo
{
    public class Pessoa
    {
        public string Nome { get; set; }

        public static bool operator ==(Pessoa a, Pessoa b)
        {
            // Fazemos o cast para Object
            // para evitar StackOverFlowException:
            // o código cairia em loop infinito
            // chamando de maneira encadeada
            // o método Equals() sobrescrito
            // e o operador == sobrescrito! 😉
            if ((((object)a) == null) || (((object)b) == null))
            {
                return false;
            }
            return object.Equals(a, b) || a.Nome == b.Nome;
        }

        public static bool operator !=(Pessoa a, Pessoa b)
        {
            return !(a == b);
        }

        public override bool Equals(Object obj)
        {
            // Verificando se o segundo participante está nulo
            if (obj == null)
            {
                return false;
            }
            Pessoa p2 = obj as Pessoa;
            // Verificando se o cast foi realizado com sucesso. 
            // Caso não foi, obj nem é um objeto do tipo Pessoa 
            // e automaticamente o método tem que retornar false
            if (p2 == null)
            {
                return false;
            }
            // Vamos agora verificar se ambos apontam 
            // para a mesma posição de memória utilizando Object.Equals()
            if (base.Equals(obj))
            {
                return true;
            }
            // Agora comparamos o estado interno dos objetos!
            return this.Nome == p2.Nome;
        }
    }
}
O método GetHashCode() ou hashCode()

Por fim, temos o famigerado método GetHashCode() ou hashCode(). Este é um dos métodos que causam mais confusão nos desenvolvedores.

O hash code é um número inteiro que é gerado de maneira única para cada objeto que esteja alocado em memória. É como se ele fosse um ID único para cada objeto que esteja sob domínio da CLR ou da JVM.

E onde este código único é utilizado? Aí é a grande sacada! Ele é utilizado principalmente dentro de coleções com a finalidade de melhoria da performance. A JVM e a CLR utilizam o hash code internamente para localizar objetos em coleções de maneira mais rápida. E daí também vem sua relação direta com o método Equals().

Uma das maneiras que os ambientes de execução do .NET e do Java utilizam para ver se um determinado objeto existe dentro de uma coleção é comparando os hash codes dos objetos pertencentes à coleção com o hash code do objeto a ser localizado. E, se um objeto é localizado dentro de uma coleção, é porque ele é igual ao elemento dentro de alguma posição da coleção em questão. Percebe a relação entre GetHashCode() e Equals()?

Por isso, vale a máxima abaixo para o método GetHashCode():

Se x.Equals(y) e x e y são objetos da mesma classe, x.GetHashCode() == y.GetHashCode() tem que obrigatoriamente retornar true.

A sobrescrita do Equals() impacta diretamente na implementação do GetHashCode() e vice-versa por causa dessa relação entre os dois. E é por isso que as IDEs emitem warnings quando você, por exemplo, sobrescreve o método Equals() e não sobrescreve o método GetHashCode() e vice-versa.

Agora, um ponto interessante: nem a CLR e nem a JVM garantem a situação inversa! Isso quer dizer que dois objetos, deste que de tipos (ou classes) diferentes, podem por coincidência retornar o mesmo hash code!

Se x é do tipo A e y é do tipo B (ou seja: x e y são objetos de classes diferentes), x.Equals(y) irá retornar por definição false; porém, pode ser que x.GetHashCode() == y.GetHashCode() retorne true.

Essa situação, apesar de ser incomum, pode acontecer. Ela é chamada de colisão. A colisão, quando ocorre, geralmente acontece por causa de sobrescrita equivocada do método GetHashCode().

O método padrão Object.GetHashCode() tem uma implementação um pouco complexa. Se estivermos falando de .NET, a chamada a Object.GetHashCode() irá ser convertida para uma chamada de baixo nível para ObjectNative::GetHashCode. Se estivermos falando de Java, a chamada de Object.hashCode() irá converter a posição de memória onde o objeto está alocado para uma representação numérica, adotando esta representação como sendo o hash code. Inclusive, lembra-se da implementação padrão do método toString() no Java? Pois então… Aquele número estranho que vem depois do @ é o hash code do objeto! =)

Existe um outro ponto muito importante com relação ao método GetHashCode():

Se GetHashCode() tem relação direta com Equals(), um mesmo objeto sempre deverá retornar o mesmo hash code, da mesma maneira que seu método Equals() sempre retornará o mesmo resultado quando haver uma comparação entre dois objetos.

Aí entra um problema grave: a implementação padrão vinda de Object.GetHashCode() não retorna o mesmo hash code para o mesmo objeto. Na verdade, a cada chamada ao método padrão Object.GetHashCode(), um novo hash code será invocado. Isso faz cair por terra o ganho de performance que a utilização de GetHashCode() poderia trazer… Daí vem a necessidade de sobrescrevermos corretamente este método em nossas classes.

Uma técnica geralmente utilizada para conseguirmos sobrescrever corretamente o método GetHashCode() é somar os hash codes de todos os atributos da classe e multiplicar por um número primo. Isso reduz bastante as chances de haver algum tipo de colisão.

Sendo assim, poderíamos sobrescrever o método GetHashCode() da classe Pessoa da seguinte maneira:

namespace TreinaWeb.Exemplo
{
    public class Pessoa
    {
        public string Nome { get; set; }

        public override int GetHashCode()
        {
            // 17 é um número primo! 😉
            return this.Nome.GetHashCode() * 17;
        }
    }
}

O método GetHashCode() auxilia na performance dentro de coleções porque é muito mais simples para os compiladores comparar dois números inteiros do que chamar o método Equals() para cada elemento que faça parte da coleção, sendo que a implementação do Equals() pode ser um pouco complexa e lenta.

Sendo assim, quando o compilador precisa localizar um objeto dentro de uma coleção, ele faz uma iteração em cada elemento que faça parte e faz a comparação com o objeto a ser localizado da seguinte maneira:

  • A primeira comparação é feita através do hash code dos objetos envolvidos. Se eles forem diferentes, o compilador para o trabalho por aqui, já que se dois objetos são iguais (ou seja, o método Equals() com os dois objetos deveria retornar true), o hash code de ambos também deveria ser igual;
  • Caso os hash codes sejam iguais, o compilador levanta a hipótese de estar havendo uma colisão. Então, ele chama o método Equals() para confirmar se os objetos são iguais ou não. Se eles forem iguais, o compilador considera que encontrou o objeto dentro da coleção. Caso não, o compilador avança para o próximo item da coleção e reinicia o processo de comparação.

Consegue perceber como o hash code pode acelerar o processo de manipulação de coleções? Ele ajuda o compilador a evitar a chamada ao método Equals(), que pode ser lento, de maneira desnecessária! o/

O método GetHashCode() é importantíssmo para coleções. Desde coleções mais básicas, como ArrayList; até coleções mais complexas, como Dictionary<TKey, TValue> ou Map<K, U>, se utilizam deste método. O Dictionary<TKey, TValue>, inclusive, utiliza este método para localizar se existem chaves duplicadas ou não.

Os métodos ToString(), Equals() e GetHashCode() são importantíssimos!

Esperamos que você tenha percebido melhor a importância desses métodos para os desenvolvedores. Muitos costumam não dar a devida importância para a sobrescrita correta destes métodos, o que pode ocasionar problemas bem críticos no código em determinadas situações (principalmente quando falamos de serialização de objetos e ambientes de alta concorrência). Você, inclusive, pode acompanhar um problema decorrente da sobrescrita e utilização incorretas destes métodos em um ambiente real neste post do StackOverflow.

Tem alguma dúvida? Quer discutir sobre algum determinado ponto? Quer expor uma situação pela qual você já passou no seu dia-a-dia como desenvolvedor que envolvia a utilização destes métodos? Compartilha com a gente nos comentários! Vamos discutir sobre este assunto! o/

Até o próximo post! =)

C# (C Sharp) - Introdução ao ASP.NET Core
Curso de C# (C Sharp) - Introdução ao ASP.NET Core
CONHEÇA O CURSO

Container de injeção de dependência (DI Container)

No artigo Entendendo Injeção de Dependência vimos sobre o que é injeção de dependência, seu funcionamento e como se dá a sua aplicação.

Injetar dependências pode se tornar uma tarefa tediosa quando se têm muitas classes envolvidas. Antes de injetar uma dependência ela precisa ser instanciada. Portanto, não cuidamos apenas da “injeção”, precisamos também ter o conhecimento de quais objetos ela precisa para funcionar.

Um container de injeção de dependência (DI Container) gerencia e automatiza as instanciações. Dizemos pra ele como um objeto deve ser criado (essa é a parte que nos toca, o nosso conhecimento sobre ele) e então sempre que o precisarmos, basta que usemos o container para obtê-lo.

Esse artigo utilizará PHP como linguagem base para os exemplos, no entanto, há de se destacar, o conceito é agnóstico à linguagem. Se PHP não é a sua “praia”, não tem problema, você pode pesquisar por “dependency injection container C#” ou por qualquer outra linguagem que encontrará importantes referências e implementações.

PHP Básico
Curso de PHP Básico
CONHEÇA O CURSO

No cenário dos frameworks PHP, os mais utilizados pelo mercado (Symfony, Laravel etc) implementam, cada um, o seu próprio container e, assim o fazem, pois seria impraticável manter os objetos “conversando” pelo detrimento da enorme quantidade de instanciações repetidas que precisariam ser feitas no ciclo de uma simples requisição. Esses frameworks possuem centenas de classes e não ter por onde resolver as dependências e reutilizá-las sob demanda, é impensável.

Um container nada mais é do que um “mapa” das dependências que o projeto usa, em termos práticos, é uma classe que armazena o conhecimento sobre seus objetos e suas dependências.

Uma implementação genérica de um container (para que possamos assimilar melhor):

<?php
declare(strict_types=1);

use Closure;

final class Container
{
    private $instances = [];

    public function set(string $id, Closure $closure) : void
    {
        $this->instances[$id] = $closure;
    }

    public function get($id) : object
    {
        return $this->instances[$id]($this);
    }
}

O método set() armazena no array $instances a identificação/nome de uma dependência e a lógica por trás da sua instanciação.

Por exemplo:

$container = new Container();

$container->set('db', function() {
    return new DatabaseAdapter('mysql:dbname=test;host=127.0.0.1', 'root', '');
});

O método get() é utilizado para resolver e retornar a instância do objeto. Observe que a instanciação não se dá no set() e sim sob demanda, na hora que precisamos daquele objeto, ou seja, na hora que usamos get().

Observe essa linha:

return $this->instances[$id]($this);

Está executando a função anônima que definimos (a que resolve a dependência) e está passando para ela como único parâmetro a instância da classe Container ($this dentro daquele contexto refere-se à instância da classe em operação). Quando a função anônima é executada temos como retorno um novo objeto.

Lembra o que a nossa função anônima retorna?

$container->set('db', function() {
    return new DatabaseAdapter('mysql:dbname=test;host=127.0.0.1', 'root', '');
});

Pois bem, saindo um pouco dessas nuances relacionadas à implementação, na prática temos:

<?php

$container = new Container();

$container->set('db', function() {
    return new DatabaseAdapter('mysql:dbname=test;host=127.0.0.1', 'root', '');
});

// Imprime a instância de um objeto do tipo 'DatabaseAdapter'
var_dump($container->get('db'));

Vamos aumentar o nosso leque de objetos e suas dependências?

<?php

// Container
$container = new Container();

// Objeto que recupera configurações salvas em algum tipo de arquivo.
$container->set('config', function() {
    return new Config();
});

// Veja que agora *db* têm como dependência um objeto da classe Config
// e o utiliza para obter os dados de acesso ao BD.
$container->set('db', function($container) {
    $config = $container->get('config')->getConfig('db');

    return new DatabaseAdapter($config['dsn'], $config['user'], $config['password']);
});

// A classe UserRepository precisa de uma instância de *db*
// para fazer consultas ao banco de dados.
$container->set('user.repository', function($container) {
    return new UserRepository($container->get('db'));
});

Veja que temos uma cadeia de objetos interdependentes. Imagine agora a situação de termos três diferentes controladores, sendo instanciados em momentos diferentes no ciclo de execução da aplicação e todos eles necessitando da instância de UserRepository para recuperar informações sobre um usuário?

Com o container definido tudo o que teríamos que fazer:

// ... 

$indexController = new IndexController(
    $container->get('user.repository')
);

$userController = new UserController(
    $container->get('user.repository')
);

$registerController = new RegisterController(
    $container->get('user.repository')
);

Observe que estamos passando para o construtor dos controladores uma instância de user.repository. O container lidará de nos retornar o objeto que queremos injetar a partir dessa identificação.

Um container pode implementar ainda mais comportamentos. Por exemplo, você deve ter percebido que a execução de $container->get('user.repository') vai sempre instanciar um novo objeto. Se executarmos 100 vezes, serão 100 novos objetos criados.

No entanto, algumas dependências são definitivas o suficiente para que não haja a necessidade de sempre instanciarmos um novo objeto delas. Nesses casos podemos ter um novo método no container para definir uma dependência compartilhada (singleton), onde uma única instância é gerada e retornada durante todo o ciclo de execução da aplicação.

O objetivo primário desse artigo não é se preocupar tanto com a implementação, mas com o conceito. No entanto, é importante que desenvolvamos alguns “protótipos” para uma melhor assimilação.

Vejamos então a implementação do nosso container com o novo método singleton():

<?php
declare(strict_types=1);

use Closure;

final class Container
{
    private $instances = [];

    public function set(string $id, Closure $closure) : void
    {
        $this->instances[$id] = $closure;
    }

    public function get($id) : object
    {
        return $this->instances[$id]($this);
    }

    public function singleton(string $id, Closure $closure) : void
    {
        $this->instances[$id] = function() use($closure) {
            static $resolvedInstance;

            if(null !== $resolvedInstance) {
                $resolvedInstance = $closure($this);
            }

            return $resolvedInstance;
        };
    }
}

Nesse método a lógica de resolução tem uma camada a mais, nela verificamos:

  1. Essa dependência já foi resolvida (devidamente instanciada) anteriormente?
    1.1. Não? Então assim o faremos.
    1.2. Já foi? Então vamos retorná-la do “cache” da variável estática (tipo de variável que não perde o valor mesmo quando o nível de execução do programa deixa o escopo).

Voltando ao contexto do nosso exemplo das classes controladoras que recebem a injeção do objeto UserRepository, podemos agora otimizar a resolução dessa dependência usando o método singleton() ao invés do set():

$container->singleton('user.repository', function($container) {
    return new UserRepository($container->get('db'));
});

Agora, na injeção dessa dependência nas classes dos controladores teremos sempre o mesmo objeto sendo compartilhado entre as diferentes instâncias:

// ... 

$indexController = new IndexController(
    $container->get('user.repository')
);

$userController = new UserController(
    $container->get('user.repository')
);

$registerController = new RegisterController(
    $container->get('user.repository')
);

É comum referências a DI Container (Dependency Injection Container) ou a IoC Container (Inversion of Control Container). São a mesma coisa: containers de injeção de dependência. A diferença é que um IoC Container precisa conseguir (inclusive) resolver as dependências a partir do mapeamento de interfaces (ele as resolve a partir de abstrações em detrimento às implementações concretas). É possível programar um IoC Container para resolver determinado objeto se uma determinada interface for requerida.

O container do Laravel Framework é um bom caso de uso. Ele é bastante encorpado e consegue resolver de diferentes formas. Ele é considerado um IoC Container, mas nada de errado se o referirmos como sendo um DI Container.

Veja esse trecho do core do Laravel Framework:

// The database manager is used to resolve various connections, since multiple
// connections might be managed. It also implements the connection resolver
// interface which may be used by other components requiring connections.
$this->app->singleton('db', function ($app) {
    return new DatabaseManager($app, $app['db.factory']);
});

Ele possui diversos services providers que configuram dezenas de dependências. E o container é compartilhado e utilizado por quase todas as classes do Framework.

(Para visualizar o Código-fonte do IoC Container do Laravel, clique aqui).

Laravel 5.1 - Framework PHP
Curso de Laravel 5.1 - Framework PHP
CONHEÇA O CURSO

Outras importantes implementações de containers para PHP:

Recomendação de leitura:

Novo artigo da série, sobre resolução automática de dependências. Nele , vamos incrementar o container criado aqui nesse artigo. Portanto, recomendo a leitura:

Até a próxima!

PHP Avançado
Curso de PHP Avançado
CONHEÇA O CURSO