Orientação a objetos

Classes Abstratas vs Interfaces

No paradigma da orientação a objetos, há dois termos que são frequentemente confundidos: as classes abstratas e as interfaces. Apesar de serem utilizadas para propósitos diferentes, possuem aspectos similares, como veremos durante este artigo.

Classes Abstratas

É um tipo de classe especial que não pode ser instanciada, apenas herdada. Sendo assim, uma classe abstrata não pode ter um objeto criado a partir de sua instanciação. Essas classes são muito importantes quando não queremos criar um objeto a partir de uma classe “geral”, apenas de suas “subclasses”.

Imagine que possuímos três classes (Conta, Conta Corrente e Conta Poupança), sendo a classe Conta uma classe “geral” (comumente chamada de classe “pai”). Ao ir em um banco, nós não criamos uma nova Conta, mas sim uma Conta Corrente ou uma Conta Poupança.

Sendo assim, não faz sentido que a classe Conta possa ser instanciada, já que é um erro na regra de negócio caso isso ocorra. É aí que entra o termo “abstrato” desse tipo de classe, por não haver a necessidade de criar objetos com base em uma classe “pai”, não há porquê ela permitir a instanciação de novos objetos.

Agora, ao invés de criarmos um objeto do tipo Conta, só será permitido a criação de objetos do tipo Conta Corrente ou Conta Poupança, o que faz mais sentido.

Por não permitir a instanciação de novos objetos com base em uma classe abstrata, este tipo de classe é utilizada para implementar o conceito de Herança da OO. Ou seja, as classes abstratas irão armazenar atributos e métodos comuns às classes que a irão herdar, permitindo um maior reaproveitamento de código.

Além disso, as classes abstratas permitem que criemos métodos abstratos (mas nem todos). Basicamente, um método abstrato é um método especial que não possui implementação, apenas assinatura (tipo de retorno, nome e parâmetros), e obriga as classes que “herdarem” da classe abstrata à implementar estes métodos, algo muito parecido com o assunto do nosso próximo tópico, as interfaces.

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

Interfaces

Considerada uma Entidade, as interfaces têm papel fundamental no desenvolvimento de software orientado à objetos. Por várias vezes, nós precisamos, de alguma forma, especificar um conjunto de métodos que um grupo de classes deverá, obrigatoriamente, implementar. Para atingir este efeito, utilizamos as interfaces.

Basicamente, uma interface define um “contrato” a ser seguido por outras classes. Este contrato é composto por cláusulas, que descrevem determinados comportamentos que este grupo de classes deverá seguir.

Imagine que você irá alugar um novo imóvel, para isso, você deve assinar o contrato de locação, que impõe regras que deverão ser seguidas, caso você queira assiná-lo. Estas regras são os métodos que as classes deverão implementar para que o contrato seja válido.

Por “obrigar” que as classes que implementem uma interface, também implementem seus métodos, as interfaces apenas definem as assinaturas dos métodos (tipo de retorno, nome e parâmetros). A partir daí, é obrigação da classe que quer implementar esta interface, utilize esta assinatura e implemente seus métodos. Estes métodos, assim como nas classes abstratas, devem ser abstratos.

Além disso, por não ser considerada uma classe, as interfaces não possuem construtores e, por isso, não podem ser instanciadas, como as classes abstratas.

Classes Abstratas vs Interfaces

Vimos então que as classes abstratas e interfaces possuem algumas características semelhantes (não podem ser instanciadas, possuem métodos abstratos que obrigam as outras classes a implementá-los), porém elas não servem para o mesmo propósito.

Quando utilizamos as interfaces, estamos definindo um conjunto de assinatura de métodos que outras classes devem implementar. Com isso, apenas definimos o comportamento base de um conjunto de classes que, por ventura, implementem esta interface.

Já as classes abstratas servem para prover uma base para que as classes que “herdem” desta não precisem se preocupar com o comportamento padrão, apenas com suas características e comportamentos pessoais.

Sendo assim, sempre que precisarmos definir um conjunto de métodos que devem ser implementados por um grupo de classes, utilizamos as interfaces. Se precisarmos determinar uma classe base para outras classes, que herdarão seus atributos e métodos e esta classe não deva ser instanciada, utilizamos as classes abstratas.

O que são mixins e qual sua importância no Dart

Quando estamos desenvolvendo aplicações, é comum o uso de herança para evitar a reescrita de código. Basicamente, a herança é uma forma de uma classe obter atributos e métodos de outra classe, evitando a duplicidade e melhorando o reaproveitamento de código.

Porém, a herança por si só, não resolve todos os problemas. Imagine a seguinte situação:

Possuímos em nosso software, uma classe Pessoa , com seus atributos e métodos e três classes que herdam de Pessoa, as classes Funcionario, Cliente e Fornecedor. Estas três classes são filhas de Pessoa e, por isso, herdam seus atributos e métodos:

Diagrama exemplificando uma herança

Porém, imagine que queremos adicionar um método que é comum a diversas classes que herdam de Pessoa, mas nem todas. A primeira opção seria desenvolver estes métodos apenas nas classes que iriam utilizá-las, porém, desta forma, há a duplicidade de código, já que mais de uma classe irá utilizar o mesmo método.

A segunda opção seria utilizar um recurso presente no Dart chamado de Mixins, como veremos abaixo.

O que são Mixins e como eles funcionam?

Basicamente, os Mixins são recursos presentes no Dart que nos permitem adicionar um conjunto de “características” a uma classe sem a necessidade de utilizar uma herança.

Voltando ao exemplo anterior, imagine as classes abaixo:

abstract class Pessoa {
  String _nome;
  int _idade;
  String _sexo;
  String _email;

  Pessoa(String nome, int idade, String sexo, String email) {
    this._nome = nome;
    this._idade = idade;
    this._sexo = sexo;
    this._email = email;
  }

// getters e setters
// métodos
}
class Funcionario extends Pessoa {
  String _cargo;

  Funcionario(String nome, int idade, String sexo, String email, String cargo)
      : this._cargo = cargo,
        super(nome, idade, sexo, email);

// getters e setters
// métodos
}
class Cliente extends Pessoa {
  bool _ativo;

  Cliente(String nome, int idade, String sexo, String email, bool ativo)
      : this._ativo = ativo,
        super(nome, idade, sexo, email);

// getters e setters
// métodos
}
class Fornecedor extends Pessoa {
  String _empresa;

  Fornecedor(String nome, int idade, String sexo, String email, String empresa)
      : this._empresa = empresa,
        super(nome, idade, sexo, email);

// getters e setters
// métodos
}
Dart - Fundamentos
Curso de Dart - Fundamentos
CONHEÇA O CURSO

Agora precisamos implementar o método abastecer() que servirá para um funcionário ou fornecedor abastecer a prateleira de um supermercado. Onde seria o melhor local para isso? Se implementarmos na classe Pessoa, o Cliente também conseguirá acesso a este método, o que não é correto para o que queremos. E é aí que entram os Mixins \o

Com o uso dos Mixins, podemos desenvolver o método abastecer() de forma “isolada” e apenas permitir que determinadas classes possuam acesso a ele. Para isso, criamos o Mixin da seguinte forma:

mixin Abastecer {
  void abastecer() {
    print("Prateleira abstecida");
  }
}

Por ser uma classe especial, os mixins não precisam de construtores.

Agora, para definirmos as classes que poderão utilizar o método abastecer(), utilizamos a palavra with, como podemos ver abaixo:

class Funcionario extends Pessoa with Abastecer{
  String _cargo;

  Funcionario(String nome, int idade, String sexo, String email, String cargo)
      : this._cargo = cargo,
        super(nome, idade, sexo, email);

// getters e setters
// métodos
}
class Fornecedor extends Pessoa with Abastecer{
  String _empresa;

  Fornecedor(String nome, int idade, String sexo, String email, String empresa)
      : this._empresa = empresa,
        super(nome, idade, sexo, email);

// getters e setters
// métodos
}

Com isso, as classes Fornecedor e Funcionario poderão utilizar o método abastecer(), sem que a classe Cliente o faça:

main() {
cliente.abastecer(); //The method 'abastecer' isn't defined for the class 'Cliente'.
funcionario.abastecer(); //Prateleira abstecida
fornecedor.abastecer(); //Prateleira abstecida
}

Isso faz com que o reaproveitamento do nosso código seja ainda mais eficiente, já que o método abastecer() só será implementado uma vez, mas poderá ser utilizado quantas vezes forem necessárias. Outras linguagens possuem recursos parecidos com os mixins no Dart, como o PHP e as Traits.

Estrutura interna de valor, referências e gerenciamento de memória no PHP

A forma com o que o PHP trabalha com referências e gerenciamento de memória não é um assunto muito popular e é compreensível que não seja, se levarmos em conta que na maior parte do tempo como desenvolvedores não precisamos nos preocupar com isso, ademais, o PHP implementa um garbage collector que cuida da contagem das referências e decide o melhor momento de liberar a memória, sem impactar na experiência do desenvolvedor.

Quando falo em “experiência do desenvolvedor”, me refiro ao fato de não precisarmos a todo momento usar unset() para destruir variáveis não mais utilizadas, tampouco passar estruturas mais complexas como objetos ou arrays por referência a todo momento etc. Há raros e específicos casos em que usar referência faz sentido. Como também há raros e específicos casos em que liberar a memória manualmente (sem aguardar o trabalho do garbage collector) faz sentido como, por exemplo, a liberação de um resource para alguma operação subsequente importante que vai consumir um tanto considerável de memória.

A ideia aqui é tentar ao máximo ter uma abordagem leve, de desenvolvedor para desenvolvedor, no sentido de usuários da linguagem. Não tenho a pretenção e nem o conhecimento necessário para explicar o funcionamento interno do PHP e, além do mais, existe gente muito boa e mais preparada pra isso (compartilharei links no final do artigo).

Em suma, esse artigo se propõe a responder as seguintes questões:

  • Como um valor é representado internamente no PHP?
  • Valores em PHP possuem que semântica? De valor ou de referência?
  • Referências? O que são?
  • Objetos são passados por referência?
  • Se tenho um array com milhões de registros e preciso passá-lo para o argumento de uma função, e agora? Vou dobrar o tanto de memória que estou usando?
Desenvolvedor PHP Júnior
Formação: Desenvolvedor PHP Júnior
Nesta formação você aprenderá todos os fundamentos necessário para iniciar do modo correto com a linguagem PHP, uma das mais utilizadas no mercado. Além dos conceitos de base, você também conhecerá as características e a sintaxe da linguagem de forma prática.
CONHEÇA A FORMAÇÃO

A estrutura de dado que representa os valores do PHP

No PHP existe uma estrutura de dado elementar chamada zval (zend value), ela é uma estrutura container para qualquer valor arbitrário que se trabalha no PHP:

struct _zval_struct {
    zend_value value;
    union {
        // ...
        uint32_t type_info;
    } u1;
    // ...
};

Para a nossa noção, o mais importante é notarmos que essa estrutura armazena o valor e o tipo desse valor. E, pelo fato de o PHP ser uma linguagem de tipagem dinâmica, o tipo é conhecido apenas em tempo de execução e não em tempo de compilação. Além disso, esse tipo pode mudar durante a execução, por exemplo, um inteiro pode simplesmente virar uma string em um dado momento.

O valor propriamente dito é armazenado na union zend_value (uma union define múltiplos membros de diferentes tipos mas só um deles pode ser ativo por vez):

typedef union _zend_value {
    zend_long         lval;             /* long value */
    double            dval;             /* double value */
    zend_refcounted  *counted;
    zend_string      *str;
    zend_array       *arr;
    zend_object      *obj;
    zend_resource    *res;
    zend_reference   *ref;
    zend_ast_ref     *ast;
    zval             *zv;
    void             *ptr;
    zend_class_entry *ce;
    zend_function    *func;
    struct {
        uint32_t w1;
        uint32_t w2;
    } ww;
} zend_value;

Por essa estrutura já podemos inferir os tipos internos das variáveis que declaramos. Objetos são armazenados numa estrutura separada do tipo zend_object, arrays são do tipo zend_array numa implementação de HashTable e assim por diante. Valores simples como inteiros e float são armazenados diretamente na zval, enquanto que valores complexos como strings e objetos são armazenados em estruturas separadas e representados na zval por ponteiros (como pode ser visto em zend_string *str;).

Outra noção refere-se ao fato de que internamente o PHP faz a contagem da quantidade de vezes que um valor é usado por outras variáveis (symbols) e isso é essencial para o gerenciamento da memória por parte do garbage collector, uma vez que esse contador estiver zerado, indica pro GC que a memória desse valor pode ser liberada.

A semântica dos valores

No PHP os valores possuem a semântica de valor, desde que explicitamente não peçamos por uma referência. Ou seja, quando passamos um valor para um argumento de uma função, ou quando atribuímos uma variável a outra, estamos sempre trabalhando em cópias do mesmo valor. Vamos esclarecer esse conceito com exemplos, para que depois possamos adentrar no assunto de referências.

Exemplo:

<?php

$foo = 1;
$bar = $foo;
$foo++;

var_dump($foo, $bar); // int(2), int(1)

$bar aponta para uma zval diferente, apenas com a cópia do valor de $foo. $foo foi incrementada e isso não refletiu em $bar.

Outro exemplo:

<?php

function increment(int $value) : void {
    $value++;
}

$value = 1;

increment($value);

echo $value; // 1

O valor de $value dentro do escopo da função é um e fora dela, é outro. Por esse motivo, o que está sendo incrementado é o zval do escopo da função.

Mas, veja bem, cópias não são feitas arbitrariamente sem necessidade. Para valores comuns como inteiros, números de ponto flutuante etc, elas são imediatamente feitas. No entanto, o mesmo não acontece com valores complexos como strings e arrays, por exemplo. O PHP usa o princípio copy-on-write, onde uma deep-copy (duplicação) dos valores é feita apenas antes desses dados serem alterados.

O melhor exemplo para ilustrar isso:

<?php

// Etapa 1: Consumo atual de memória
echo sprintf("1) Mem: %s \n", memory_get_usage());

// Etapa 2: Criando um grande array dinâmicamente
$foo = [];
for ($i = 0; $i < 1000000; $i++) {
    $foo[] = random_int(1, 100);
}
echo sprintf("2) Mem: %s \n", memory_get_usage());

// Etapa 3: Atribuindo o array a duas novas variáveis
$bar = $foo;
$baz = $foo;
echo sprintf("3) Mem: %s \n", memory_get_usage());

// Etapa 4: Fizemos uma alteração de escrita no array $bar
$bar[] = 1;
echo sprintf("4) Mem: %s \n", memory_get_usage());

// Etapa 5: Zerando os arrays previamente criados
$foo = [];
$bar = [];
$baz = [];
echo sprintf("5) Mem: %s \n", memory_get_usage());

Resultado:

1) Mem: 395280 
2) Mem: 33953920 
3) Mem: 33953920 
4) Mem: 67512528 
5) Mem: 395312 

Veja que nas etapas 2 e 3 o consumo de memória permaneceu o mesmo. O senso comum seria imaginar que ele fosse triplicar, uma vez que atribuímos dois novos arrays com o valor de $foo. No entanto, o consumo de memória duplicou apenas na etapa 4, pois alteramos o array $bar, essa operação de escrita fez com que o PHP duplicasse o array para esse zval.

O mesmo comportamento pode ser visto com strings:

<?php

// Etapa 1
echo sprintf("1) Mem: %s \n", memory_get_usage());

// Etapa 2
$string = str_repeat('FOO', 100000);
echo sprintf("2) Mem: %s \n", memory_get_usage());

// Etapa 3
$string2 = $string;
$string3 = $string;
echo sprintf("3) Mem: %s \n", memory_get_usage());

// Etapa 4
$string2 .= 'BAR';
$string3 .= 'BAR';
echo sprintf("4) Mem: %s \n", memory_get_usage());

Resultado:

1) Mem: 393504 
2) Mem: 696640 
3) Mem: 696640 
4) Mem: 1302848

Apenas na etapa 4 tivemos aumento da memória, pois as strings foram modificadas, forçando deep-copy.

Outro exemplo:

<?php

// Etapa 1
echo sprintf("1) Mem: %s \n", memory_get_usage());

// Etapa 2
$string = str_repeat('FOO', 100000);
echo sprintf("2) Mem: %s \n", memory_get_usage());

// Etapa 3
function foo(string $string) {
    $string2 = $string;
}

foo($string);

echo sprintf("3) Mem: %s \n", memory_get_usage());

Resultado:

1) Mem: 393400 
2) Mem: 696536 
3) Mem: 696536
Desenvolvedor PHP Pleno
Formação: Desenvolvedor PHP Pleno
Nesta formação você aprenderá aspectos mais avançados da linguagem PHP, indo adiante nas características da linguagem, orientação a objetos, práticas de mercado, além da parte de integração com banco de dados. Ao final, estará apto a desenvolver sistemas usando banco de dados relacionais.
CONHEÇA A FORMAÇÃO

Referências no PHP

Referência é um mecanismo que permite que múltiplas variáveis (símbolos) apontem pro mesmo container interno de valor (zval). Referências não são ponteiros como vemos em C ou em Go, por exemplo.

Voltando ao exemplo anterior de $foo e $bar, como podemos fazer para $bar apontar para o mesmo zval que $foo de tal forma que o incremento em $foo reflita em $bar? Aí que entra o operador de referência & na atribuição:

<?php

$foo = 1;
$bar =& $foo;
$foo++;

var_dump($foo, $bar); // int(2), int(2)

Agora, na prática, temos dois símbolos ($foo e $bar) que apontam para o mesmo valor, alterações em um ou em outro refletirão no mesmo container interno de valor. Por esse motivo o incremento de $foo agora refletiu em $bar.

Esse é o exemplo mais elementar para distinguir a semântica de valor da de referência.

Objetos são passados por referência?

Existe a noção entre desenvolvedores e artigos na web de que objetos são passados por referência, mas isso não reflete a realidade. Eles também são passados pela semântica de valor, como qualquer outro valor. Eles apenas possuem um comportamento parecido com “referência” mas que internamente não se trata de referência.

Vejamos esse exemplo:

<?php

class Node
{
    // Todo
}

$node1 = new Node();
$node2 = $node1;

xdebug_debug_zval('node1'); // node1: (refcount=2, is_ref=0)=class Node {  }
xdebug_debug_zval('node2'); // node2: (refcount=2, is_ref=0)=class Node {  }

Nota: A função xdebug_debug_zval() retorna detalhes internos da zval e para ela funcionar, é preciso ter a extensão xdebug instalada.

Na prática, o que temos nesse exemplo, é que as duas variáveis são estruturas (zval) distintas, mas internamente apontam pro mesmo objeto.

O que é diferente de:

<?php

class Node
{
    // Todo
}

$node1 = new Node();
$node2 = &$node1;

xdebug_debug_zval('node1'); // node1: (refcount=2, is_ref=1)=class Node {  }
xdebug_debug_zval('node2'); // node2: (refcount=2, is_ref=1)=class Node {  }

Nesse exemplo, além de ambas as variáveis apontarem pro mesmo objeto, temos explicitamente uma referência, observe que o valor da flag is_ref agora é 1. Isso quer dizer que, $node1 e $node2 apontam para o mesmo zval.

A diferença, a princípio, parece sutil, mas tentemos ver isso na prática. Primeiro um exemplo onde explicitamente pedimos para receber um valor por referência:

<?php

$a = 1;

function change(&$a) {
    $a = 2;
}

change($a);

echo $a; // 2

A alteração dentro do escopo da função refletiu no mesmo container interno de valor (zval) da variável $a do escopo do script. Como era esperado.

Agora a dúvida é: se objetos são sempre passados por referência (como é o senso comum), não precisamos usar o operador de referência para receber um objeto nessa mesma função change(), certo?

Vejamos:

<?php

class Node
{
    // Todo
}

$node = new Node();

function change($node) {
    $node = 10;
}

change($node);

var_dump($node); // class Node#1 (0) { }

Pois bem, se os objetos realmente fossem passados por referência, o valor da variável $node deveria ser 10 e não uma instância da classe Node. Ou seja, quando um objeto é passado para o argumento de uma função/método ou quando é atribuído em outra variável, não temos passagem por referência, o que fica caracterizado é que essas variáveis apenas estão apontando para a mesma estrutura de dado referente ao objeto, o que dá a sensação que está acontecendo passagem por referência.

O que causa essa confusão é esse comportamento:

<?php

class Node
{
    public int $count;
}

$node = new Node();
$node->count = 1;

function change(Node $node) {
    $node->count = 2;
}

change($node);

echo $node->count; // 2

O resultado é 2, mesmo sem passagem por referência, por causa do comportamento de estarmos trabalhando com o mesmo objeto em memória.

O exemplo abaixo demonstra que estamos lidando com duas estruturas (zval) diferentes, mas que ambas referenciam a mesma estrutura de dado do objeto criado:

<?php

class Node
{
    // TODO
}

$node = new Node();

function change(Node $node) {
    $nodeFoo = &$node;

    xdebug_debug_zval('node'); // node: (refcount=2, is_ref=1)=class Node {  }
}

change($node);

xdebug_debug_zval('node'); // node: (refcount=1, is_ref=0)=class Node {  }

Veja que dentro do escopo da função já ficou caracterizado que existe referência, mas fora dela, não. Ou seja, se $nodeFoo dentro de change() fosse alterada para 10, refletiria no mesmo zval de $node. Veja:

<?php

class Node
{
    // TODO
}

$node = new Node();

function change(Node $node) {
    $nodeFoo = &$node;

    $nodeFoo = 10; // Reflete no mesmo zval de $node

    var_dump($node); // int(10)
}

change($node);

var_dump($node); // class Node#1 (0) { }

Agora, a história muda quando explicitamente recebemos a variável por referência:

<?php

class Node
{
    // Todo
}

$node = new Node();

function change(&$node) {
    $node = 10; // Reflete no mesmo zval da variável $node fora desse escopo
}

change($node);

var_dump($node); // int(10)

Por fim, um exemplo de variáveis que não se referenciam, mas que apontam para o mesmo valor interno em memória até que uma operação de escrita seja realizada, aí é feita uma deep-copy:

<?php

$string = str_repeat('FOO ', 4);

xdebug_debug_zval('string'); // string: (refcount=1, is_ref=0)='FOO FOO FOO FOO '

$string2 = $string;

xdebug_debug_zval('string'); // string: (refcount=2, is_ref=0)='FOO FOO FOO FOO '

$string3 = $string;

xdebug_debug_zval('string'); // string: (refcount=3, is_ref=0)='FOO FOO FOO FOO '

$string3 .= 'BAR';

xdebug_debug_zval('string'); // string: (refcount=2, is_ref=0)='FOO FOO FOO FOO '

Observe que depois que $string3 fez uma operação de escrita o refcount do valor de $string foi decrementado.

Outro exemplo genérico:

<?php

class Foo {
    public $index;
}

$a = new Foo();
$b = $a;
$c = &$b;

$c->index = 10;

echo "({$a->index}, {$b->index}, {$c->index})" . PHP_EOL; // (10, 10, 10)

$c = "Hello";

echo "({$a->index}, {$b}, {$c})"; // (10, Hello, Hello)

Na prática temos:

Todos os símbolos apontam pro mesmo objeto, no entanto, $c é referência de $b, alterações feitas em $c refletirão na mesma zval de $b, mantendo $a intacta.

Em que momento devo usar referências?

As funções de sorting de arrays do PHP os recebem por referência, por exemplo:

sort ( array &$array [, int $sort_flags = SORT_REGULAR ] ) : bool

Sempre que você ver na assinatura de uma função &$var, significa que está recebendo por referência e que, portanto, vai trabalhar na mesma zval da variável informada.

E usar referências não é sobre performance, dependendo do uso, pode piorá-la. Usar referências é mais sobre lidar melhor com a memória, como no caso da função acima, não faria sentido duplicar o array inteiro só para aplicar o algoritmo de sorting.

Mas, no geral, a regra é que você dificilmente vai usar com frequência operações com referências. Usa-se referências quando realmente existe um objetivo muito bem definido, portanto, não se preocupe em querer “otimizar” o seu código pra usar referências, pois é bem possível que você não precisa delas.

O que mais posso fazer com referências?

Você pode ver todas as sintaxe possíveis para se usar referências no PHP através desse link da documentação oficial.

Palavras finais

Sumarizando o que foi visto nesse artigo:

  • PHP usa a semântica de valor;
  • Objetos não são passados por referência;
  • Valores complexos como arrays e strings são apenas duplicados quando alterados (usa-se o princípio copy-on-write);
  • Referências não deixam o código mais performático, se mal utilizadas, podem é deixar mais lento, devido aos vários apontamentos que precisam ser feitos. Referências são usadas em casos bem específicos em que o desenvolvedor tem a exata noção do comportamento que ele espera ter;
  • O desenvolvedor não precisa na maior parte do tempo se preocupar com memória (a não ser que vá trabalhar com grandes data sets, mas aí pode-se estudar o uso de generators no PHP ou alguma outra estratégia);

Não é nada fácil entender a figura completa desse conjunto de pecinhas, mas caso você tenha interesse em se aprofundar, recomendo a leitura dos artigos:

Até a próxima!

Desenvolvedor PHP Sênior
Formação: Desenvolvedor PHP Sênior
Nesta formação você aprenderá aspectos avançados da linguagem PHP e de seu ecossistema, conhecimentos essenciais para se tornar um desenvolvedor diferenciado, e com isso, você terá conhecimento para desenvolver aplicações PHP usando as práticas mais modernas do mercado.
CONHEÇA A FORMAÇÃO

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?

Django - Fundamentos
Curso de Django - Fundamentos
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! 😉

Django - Tópicos de segurança
Curso de Django - Tópicos de segurança
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.
Lógica Orientada a Objetos Completo
Curso de Lógica Orientada a Objetos Completo
CONHEÇA O CURSO

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!

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

© 2004 - 2019 TreinaWeb Tecnologia LTDA - CNPJ: 06.156.637/0001-58 Av. Paulista, 1765, Conj 71 e 72 - Bela Vista - São Paulo - SP - 01311-200