Ir para conteúdo

Arquivado

Este tópico foi arquivado e está fechado para novas respostas.

Wilson Batista

Princípio de substituição de Liskov

Recommended Posts

Ola pessoal do imaster tudo bem?

 

Estou estudando os 5 princípios de SOLID, estou na parte do princípio de substituição de Liskov.

E confesso que estou com muita dificuldade de entender o objetivo deste princípio, já fiz varias buscas no google sobre este assunto, e todos aborda com exemplos confusos.

 

Poderiam me ajudar a compreender este princípio de substituição de Liskov, numa forma fácil de entender?

 

 

Desde já agradeço

Compartilhar este post


Link para o post
Compartilhar em outros sites

O Princípio de Substituição de Liskov delimita regras para se utilizar a herança, uma vez que a regrinha do é um é meio subjetiva demais.

 

Em seu lugar, Liskov propões que para se utilizar herança, façamos um outro questionamento: é substituível por?

 

Em outras palavras, antes de estender uma classe X, criando uma classe Y, pergunte-se a si mesmo se é possível substituir objetos X por objetos Y sem alterar a funcionalidade do programa.

 

Um exemplo claro onde o é um falha é no caso do Retângulo/Quadrado. Matematicamente, todo quadrado é um retângulo, então, inicialmente, faria sentido o seguinte código:

class Rectangle {}

class Square extends Rectangle {}

Agora vamos pensar em implementação. Um retângulo é um paralelogramo que possui todos os ângulos retos e cujos lados tem medidas iguais 2 a 2:

class Rectangle {
    private $width;
    private $height;

    public function __construct($width, $height) {
        $this->setWidth($width);
        $this->setHeight($height);
    }

    public function setWidth($width) {
        $this->width = (int) $width;
    }

    public function setHeight($height) {
        $this->height = (int) $height;
    }

    public function getWidth() {
        return $this->width;
    }

    public function getHeight() {
        return $this->height;
    }

    public function getArea() {
        return $this->height * $this->width;
    }
}

E agora, vamos à classe que representará um quadrado. Como sabemos, para ter um quadrado, precisamos de um retângulo e precisamos garantir ele que possui todos os lados iguais. Fazendo com herança, temos:

class Square extends Rectangle {
    public function __construct($side) {
        $this->setWidth($side);
        $this->setHeight($side);
    }
}

Ok, a princípio funciona, mas há um bug: o que acontece se depois de instanciar um objeto [inline]Square[/inline] eu vou lá e resolvo setar um dos lados independentemente?

$square = new Square(5);
echo $square->getArea(); // 25
$square->setWidth(10);
echo $square->getArea(); // 50 e não o esperado valor 100.

Aaah, mas podemos forçar algo do tipo:

class Square extends Rectangle {
    public function __construct($side) {
        $this->setWidth($side);
    }

    public function setWidth($width) {
        parent::setWidth($width);
        parent::setHeight($width);
    }

    public function setHeight($height) {
        parent::setWidth($height);
        parent::setHeight($height);
    }
}

Legal, agora funciona como esperado, mas você tem 2 atributos que precisa manter sincronizados entre si, além do desperdício de espaço, porque você precisa de um dado só.

 

Além do mais, quando você altera a largura de um retângulo, você não espera que a altura seja afetada.

 

Imagine que você utilize essas formas geométricas para algum outro fim, e você tem:

public function doSomethingWithRectangle(Rectangle $r) {
    $r->setWidth($r->getWidht() * 2); // dobrando a largura
}

Entretanto, nada te impedirá de chamar essa função como:

$obj->doSomethingWithRectangle(new Square(5));

Quando o código executar, sem saber, ele irá alterar não só a largura do objeto, mas também a altura. Isso é conhecido como efeito colateral, mas também pode ser chamado de bomba de bugs.

 

Observe então que um objeto [inline]Rectangle[/inline] NÃO É SUBSTITUÍVEL por um objeto [inline]Square[/inline], logo, não deveríamos utilizar herança neste caso.

Compartilhar este post


Link para o post
Compartilhar em outros sites

Observe então que um objeto [inline]Rectangle[/inline] NÃO É SUBSTITUÍVEL por um objeto [inline]Square[/inline], logo, não deveríamos utilizar herança neste caso.

 

Não é substituível por que, o quadrado tem a largura e altura sempre que sincronizados, diferente do retângulo que a sua altura e largura são totalmente diferentes. Certo?

 

Como seria a solução deste problema de quadrado e retângulo?

Compartilhar este post


Link para o post
Compartilhar em outros sites

Não é substituível por que, o quadrado tem a largura e altura sempre que sincronizados, diferente do retângulo que a sua altura e largura são totalmente diferentes. Certo?

É, pode-se colocar dessa forma.

Como seria a solução deste problema de quadrado e retângulo?

NÃO usar herança no caso:

class Sqare {}
class Rectangle {}

Compartilhar este post


Link para o post
Compartilhar em outros sites

Outra maneira simples de entender este princípio.

 

Jeito errado que quebra o princípio de substituição de Liskov:

abstract class Bird {
    function public fly() {}
    function public eat() {}
}

// Pássaro corvo
class Crow extends Bird {}

// Pássaro avestruz, porem é um tipo de pássaro que não voa
class Ostrich extends Bird {
    function public fly () {
        // Avestruz não voa, por este motivo dará um erro quando alguém chamar este método
        throw new UnsupportedOpperationException('avestruz não voa');
    }
}

No exemplo acima existe uma classe "Bird" que é genérica para todos os tipos de pássaros, bom pensando o que seria genérica para todos os pássaro já pensamos que é voar não é! Mas existem pássaros(aves) que não voam como avestruz, galinha, pinguim e muitos outros ...

 

Jeito certo de se aplicar o princípio de substituição de Liskov:

abstract class Bird {
    // Método comer seria o método certo para todos os tipo de pássaro, por que isso que é genérico para todos os tipo de passaro no nosso exemplo
    public function eat () {} // comer
}

// Pássaros que voam, implementaria o método fly()
class FlightBird extends Bird {
    public function fly () {}
}

// Pássaros que não voa, não precisam do método fly() , assim evitando erros ou bugs
class NonFlightBird extends Bird {}

Como podemos ver foi retirado o método fly() da classe "Bird" , e só ficou o método eat() nela, pois isso que é genérico para todos os pássaros.

 

E criamos 2 classes que estende "Bird" as classes FlightBird(pássaro que voa) e NonFlightBird(pássaro que não voa), agora classes como avestruz, galinha, pinguim estenderiam a classe NonFlightBird pois eles não precisam do método fly() , assim evitando erros ou bugs. Esse seria o jeito certo de se fazer e não quebrando o princípio de substituição de Liskov.

 

Espero ter ajudado

Compartilhar este post


Link para o post
Compartilhar em outros sites

Em outras palavras, antes de estender uma classe X, criando uma classe Y, pergunte-se a si mesmo se é possível substituir objetos X por objetos Y sem alterar a funcionalidade do programa.

 

@Henrique esqueci de falar deste trecho, nessa parte você fala que uma "super classe" que no caso é X, criando uma classe "sub classe" que no caso é Y, as "sub classes" não podem modificar o comportamento da suas "super classe". Certo?

Compartilhar este post


Link para o post
Compartilhar em outros sites

×

Informação importante

Ao usar o fórum, você concorda com nossos Termos e condições.