PHP para Iniciantes - Classes e Objetos - Herança

Olá, devs incríveis e maravilhoses. Tudo certinho com vocês?! Eu consegui um tempinho para escrever esse artigo, então acho que estou bem, haha.

O assunto de hoje vai ser mais um pequeno salto no fluxo atual da documentação oficial do PHP, pois ao invés de falarmos somente sobre Visibilidade, falaremos de Herança.

Ué, Kiko... Por que pularemos esse assunto?

Pelo simples fato de que eu já falei sobre ele. Lembra das palavras public, private e protected? Eu expliquei nos artigos onde esse tópico estava relevante:

  1. Visibilidade de Propriedades (procura por 1.1.1);
  2. Visibilidade de Métodos (procura por 3. O que é e como declarar métodos).

Ainda assim, há um pequeno detalhe que não falei a respeito: visibilidade em outros objetos. Vou dedicar essa introdução para isso, ok? Afinal, está relacionado com herança.

Visibilidade entre objetos

A visibilidade que damos às propriedades e métodos pertence à assinatura da classe que estamos desenvolvendo. Isso significa que mesmo que você escreva uma propriedade privada, instâncias diferentes da mesma classe poderão acessar esse valor. Basta injetar a outra instância em algum método:

<?php

class Exemplo {
    public function __construct(
        private int $codigo
    ) { }

    public static function imprime($ex Exemplo) {
        echo $ex->codigo;
        // $ex->codigo é uma propriedade privada da instância $ex
        // mas eu consigo acessar seu valor, pois estamos dentro da classe que conhece toda a assinatura dele
    }
}

$ex = new Exemplo(123);
Exemplo::imprime($ex);

Notou que o método estático imprime está recebendo um objeto da classe Exemplo? E que esse método também pertence à classe Exemplo? Dessa forma, é possível trabalhar com todas as propriedades e métodos privados de $ex.

E o que isso tem a ver com herança, Kiko?

Tudo. Nós podemos acessar propriedades protegidas de outra classe, se estiver herdada na classe atual. É por isso que digo que a visibilidade entre objetos faz parte de herança.

<?php

class Person {
    public function __construct(
        protected string $name
    ) { }
}

class Pilot extends Person {
    public function criticize(Person $person): void {
        echo $person->name . " ainda tem muito o que aprender..." . PHP_EOL;
    }
}

$john = new Person("John Doe");


$pilot = new Pilot("José");
$pilot->criticize($john);

Veja o código funcionando: https://onlinephp.io/c/b7bb0

  1. $person->name é uma propriedade protegida de Person;
  2. Pilot extende Person, logo ele herda todos os métodos e propriedades protegidos e públicos de Person, incluindo $person->name;
  3. Quando Pilot recebe um objeto Person, ele pode acessar tudo aquilo que herdou dele.

É por isso que vemos por aí o pessoal divulgando herança sempre como uma relação Pai e Filho. Hoje vou quebrar um pouco esse ciclo vicioso:

<?php

class Mae {
    protected array $imoveis;
    protected array $contasBancarias;
}

class Filho extends Mae { }

Esse exemplo faz sentido e, ao mesmo tempo, não faz.

MEME: é o que, véi?

Como é, Kiko?

Olhando somente pela herança entre as classes, nós entendemos que a classe Filho herda as propriedades imoveis e contasBancarias da classe Mae. Porém, da forma como está, Filho herda tudo, não somente bens e dinheiro. Se a gente cria uma propriedade que define o emprego da mãe, o filho estaria herdando isso também... E na vida real, a gente sabe que isso não faz sentido algum, não é mesmo?

Se fossemos replicar a herança da vida real, esse relacionamento seria limitado aos objetos e não às classes. O que poderíamos fazer é criar uma abstração, definindo o que é uma pessoa e criando relacionamento entre pessoas, sem herança:

<?php

class Pessoa {
    protected array $imoveis;
    protected array $contasBancarias;
    protected Pessoa|null $pai; // sutilidade triste aqui, mas é realidade. Se você se identificou, força! Eu te respeito muito.
    protected Pessoa $mae;

    public function herancaMaternal() {
        array_push($this->imoveis, $this->mae->imoveis...);
        // na real, aqui a gente precisa dividir com os irmãos ainda, né? mas vamos deixar assim só pra explicar o que eu tava falando
        array_push($this->contasBancarias, $this->mae->contasBancarias...);
    }
}

Com isso, a pessoa que acionar $pessoa->herancaMaternal() só vai receber o que de fato herdou, quando for a hora. Percebeu que eu acessei os atributos protegidos imoveis e contasBancarias de $this->mae? Só foi possível pois a visibilidade de qualquer instância de Pessoa nos permite "ver" esses dados naquele bloco.

E com isso, temos uma boa introdução para o artigo...

Herança

Oloko, Kiko, isso foi só uma introdução?!

Eu queria que fosse, mas eu já escrevi bastante, né? Hahaha. Pelo menos agora eu vou poder ser um pouco mais direto...

Resusmo: herança, em orientação a objeto, é o compartilhamento de recursos entre classes. Se a classe A extende a classe B, ela herda todos os métodos e propriedades públicos e privados dela. Esses métodos e propriedades passam a coexistir tanto em B quanto em A.

A forma de escrita é na declaração da classe. Até aqui, você me viu declarando classes como class AlgumaCoisa, certo? Em herança, a declaração inclui a palavra extends que indica de qual classe você está herdando:

<?php

class A { }
class B extends A { } 
class C extends B { }

Sim, é possível fazer heranças contínuas.

Um tópico interessante sobre herança é o override, ou seja, sobreescrever alguma coisa. Uma amostra bem simples disso seria uma classe A implementar o método vibrar() de um jeito e a classe que a herda reimplementar de outra forma:

<?php

class A {
    public static function vibrar(): void {
        echo "zzz" . PHP_EOL;
    }
}

class B extends A {
    public static function vibrar(): void {
        echo "BZZZZ" . PHP_EOL;
    }
}

class C extends A { }

Se você chamar:

  • A::vibrar(): irá imprimir zzz;
  • B::vibrar(): irá imprimir BZZZZ;
  • C::vibrar(): irá imprimir zzz.

Veja o código rodando aqui: https://onlinephp.io/c/98c0e

Ué, por que só B::vibrar() está diferente?

Porque ele sobreescreveu o comportamento de A ao reimplementar o método vibrar().

Override é um recurso comumente utilizado quando encontramos alguma particularidade no meio das heranças, por isso podemos fazer isso. Mas é preciso tomar cuidado com um pequeno detalhe: antes do PHP 8, nós não tínhamos a capacidade de informar o tipo de dado que cada função e/ou método poderia retornar, por isso sempre foi possível sobreescrever os métodos herdados retornando um tipo de dado completamente diferente.

A partir do PHP 8.1, esse tipo de comportamento gera um aviso de depreciação, o que significa que, muito em breve, essa prática não irá mais funcionar nas novas versões.

Ou seja: precisou reimplementar um método e mudou a assinatura dele? É melhor refatorar enquanto ainda pode... Veja o alerta de depreciação aqui: https://onlinephp.io/c/c6e6d.

Na documentação, consta a possibilidade de omitir esse alerta ao aplicar o atributo #[ReturnTypeWillChange] acima da assinatura do método ou função, porém não recomendo e nem vou explorar isso... Até porque, se há esse risco nas suas heranças, isso só mostra que você está quebrando o seu código.

O que nos leva à seguinte polêmica...

Herança é anti-pattern

A herança entre classes, apesar de ser bem aceito na programação com abstrações, também pode ser considerado um anti-pattern ou anti-padrão. O exemplo que dei ali de pessoas, mãe, filho e tal evidencia isso: muitas vezes aplicamos uma lógica de herança porque queremos reduzir código, mas quase sempre estamos enxergando a abstração da forma errada.

Como esse artigo é pra quem nunca viu a parada, eu preciso mostrar o erro acontecendo, não é mesmo? Então vamos lá...

Digamos que nós queremos criar uma classe que represente um fusca e que tenha uma função de buzinar, sendo o som da buzina "bibii".

<?php

class Fusca {
    public function buzinar(): void {
        echo "bibii";
    }
}

$fusca = new Fusca();
$fusca->buzinar();

E agora, além do fusca, queremos uma lamborghini, sendo a buzina "pompom".

<?php

class Lamborghini {
    public function buzinar(): void {
        echo "pompom";
    }
}

$lamborghini = new Lamborghini();
$lamborghini->buzinar();

Ué, Kiko... Repetiu tudo?!

Sim, criamos uma repetição de código, onde a única diferença é o som da buzina. Se eu quiser criar uma função que chama essa buzina independente do modelo de carro, nós teremos um problema, pois as classes não tem relação entre si.

Geralmente, a primeira coisa que as pessoas pensam quando querem resolver esse tipo de problema é em herança, pois é a saída mais fácil: ah, pega o que tem em comum, cria uma classe abstrata com isso e pronto.

<?php

abstract class Carro {
    public function buzinar(): void {
        echo static::BUZINA . PHP_EOL;
    }
}

class Fusca extends Carro {
    protected const BUZINA = "bibii";
}

class Lamborghini extends Carro {
    protected const BUZINA = "pompom";
}

// Função genérica para todos os carros
function apertarBuzina(Carro $carro) {
    $carro->buzinar();
}

// Aplicando a função
$fusca = new Fusca();
$lamborghini = new Lamborghini();

apertarBuzina($fusca);
apertarBuzina($lamborghini);

Veja o código em ação: https://onlinephp.io/c/cb978

Pô, Kiko, para mim ficou muito legível... Por que isso seria considerado um anti-pattern??

Sim, herança pode até deixar tudo mais organizado, porém a manutenabilidade disso é péssima pois criamos perigos invisíveis. Por exemplo, para indicar o som da buzina de cada carro, eu preciso declarar a constante BUZINA. Se eu escrever um carro sem declarar isso, nós teremos um erro:

<?php

class CarroQuebrado extends Carro { }

$carro = new CarroQuebrado();
$carro->buzinar();

Veja o que acontece: https://onlinephp.io/c/2e033

Toda vez que precisarmos abstrair alguma coisa no nosso código, o ideal é criarmos uma estrutura rígida que nos force a criar tudo o que for obrigatório. Essa estrutura rígida é chamada de interface e é amplamente utilizada em várias linguagens de programação.

Quando você define uma interface Carro, você está dizendo ao interpretador: olha aqui, cara... Toda vez que alguma classe implementar essa estrutura, obrigue o desenvolvedor a escrever todos esses métodos do mesmo jeitinho que eu estou assinando aqui, ok?!

E o dev que se vire pra respeitar isso:

<?php

interface ICarro { // colocamos o I na frente pra deixar claro que é uma interface
    public function obterBuzina(): string
}

Diferente de herança, a declaração de uma interface é precedido pela palavra-chave interface. Em herança, nós declaramos classes mesmo... Da mesma forma, para declarar que uma classe está seguindo as regras de uma interface, nós usamos a palavra-chave implements, diferente do extends da herança.

Toda classe que implementar a interface ICarro precisará ter um método obterBuzina() que retorna uma string. Com isso, a gente pode recriar aquelas classes Fusca e Lamborghini para parar de usar herança e passar a assinar a nova interface:

class Fusca implements ICarro {
    public function obterBuzina(): string {
        return "bibii";
    }
}

class Lamborghini implements ICarro {
    public function obterBuzina(): string {
        return "pompom";
    }
}

Ficou um pouco mais verboso? Ficou, duas linhas... Ainda assim, as próximas classes de carro que forem criadas não terão o risco de alguém esquecer de escrever o que é obrigatório.

Mas Kiko, cadê o método buzinar?

É aqui que fica o pulo do gato: apesar de fazer total sentido colocar um método buzinar nos carros, nós podemos criar uma função que faça a buzina em si... Afinal, na outra implementação, nós teríamos de criar essa função do mesmo jeito, lembra? Tínhamos carro->buzinar dentro de apertarBuzina($carro).

Agora só precisamos do apertarBuzina($carro):

function apertarBuzina(ICarro $carro): void {
    echo $carro->obterBuzina() . PHP_EOL;
}

$fusca = new Fusca();
$lamborghini = new Lamborghini();

apertarBuzina($fusca);
apertarBuzina($lamborghini);

Veja o código funcionando aqui: https://onlinephp.io/c/cd215

Então mesmo herança sendo muito legal, tente pensar um pouco além, tudo bem? Você precisa fazer um código com o menor risco possível de erros acidentais. Pense sempre que outro desenvolvedor vai precisar mexer: é melhor força-lo a seguir um padrão do que criar padrões invisíveis, concorda?

E por hoje é só!! Curtiu? Comenta e compartilha! Acho que, com esse artigo, consegui deixar bem claro o motivo de herança ser considerado um anti-pattern... Ou você ainda discorda? Você teria feito esse exercício dos carros diferente? Pode mandar aí nos comentários, hehe.

No próximo artigo, falaremos sobre Operador de Resolução de Escopo (::), que é o que usamos para acessar propriedades e métodos estáticos das classes. Tá preparade?

Inté!!