Swift: Princípio de Substituição de Liskov [Artigo 3]

Compartilhe

Compartilhar no facebook
Compartilhar no google
Compartilhar no twitter
Compartilhar no linkedin

Introdução

Se você é um desenvolvedor ou desenvolvedora, e possui um certo interesse pela área de engenharia de software, muito provavelmente já ouviu falar de um termo chamado SOLID

Caso você nunca tenha ouvido falar, não se preocupe, o primeiro artigo desta série possui uma introdução a respeito.

 

Princípio de Substituição de Liskov (LSP)

A definição matemática original, de Barbara Liskov para esse princípio é 

Se para cada objeto o1 de tipo S, existe um objeto o2 de tipo T, de modo que para todos programas P definidos em termos de T, o comportamento de P é inalterado quando o1 é substituído por o2 , então S é um subtipo de T.

Pode ser mais fácil de se entender, especialmente para pessoas que estão estudando esses princípios pela primeira vez, se não olharmos para essa definição matemática. Uma outra maneira e provavelmente mais fácil de descrever esse princípio poderia ser: 

Programas que fazem referência a um objeto de uma classe base devem ser capazes de usar um objeto de uma classe derivada sem diferenças de comportamento e sem saber sobre sua existência.

Relembrando, no artigo anterior desta série falamos sobre o Princípio Aberto-Fechado (OCP), que basicamente diz que as entidades de software devem ser abertas para extensão, mas fechadas para modificações, o que torna o código sustentável e reutilizável. Além disso, para obter esses resultados, devemos desenvolver nosso código usando abstrações, como herança e interfaces.

Portanto, neste artigo vamos nos concentrar em heranças para apresentar os benefícios de se respeitar o LSP. Vamos mostrar alguns exemplos que quebram esse princípio e também mostrar que, quando não o seguimos, frequentemente quebramos o OCP também. É por isso que esses princípios podem ser correlacionados.

Exemplos em Swift

Exemplo 1 – Comportamentos inesperados

Considere o exemplo a seguir, onde temos a superclasse Rectangle e uma subclasse Square. Esta é uma herança muito simples, apenas para demonstrar uma quebra de LSP e os problemas ao se fazê-lo.

Aqui temos uma classe Rectangle com duas propriedades, width e height, e um método area() para calcular a área do retângulo, com base nessas propriedades.

Também definimos a subclasse Square, mas com uma pequena diferença, sobrescrevemos essas propriedades para garantir que ao definir cada uma delas, atribuímos à outra o mesmo valor. Isso parece correto e agradável, já que estamos garantindo quadrados teriam o mesmo valor para largura e altura, o que está definitivamente de acordo com a definição matemática de um quadrado.

Finalmente, imagine que utilizamos dessas classes acima, como fazemos na seguinte função main().

Primeiro, definimos um quadrado com os valores dos seus lados sendo 10. Em seguida, e o ponto essencial deste exemplo, usamos do polimorfismo para criar uma referência a este quadrado, mas do tipo da superclasse Rectangle. Com isso, temos um objeto do tipo Square que será tratado como um objeto do tipo Rectangle.

Com isso em mente, atribuímos a altura desse retângulo como 7 e, em seguida, definimos sua largura como 5. Já que estamos manipulando um objeto do tipo Rectangle (supondo que não saibamos que em tempo de execução estamos realmente manipulando um quadrado), o resultado esperado para a área seria 7 x 5 = 35. No entanto, se o leitor executar este código, ele obterá 25 para a área após o print(). E é por isso que estamos quebrando o Princípio de Substituição de Liskov, porque não conseguimos usar um objeto do tipo subclasse sem obter comportamentos diferentes.

 

Exemplo 1 – Problemas em quebrar o LSP

Pode ser um pouco difícil de entender. O leitor poderia pensar “Eu sei que era um objeto Square que tinha suas propriedades configuradas, e sei que um Square sempre terá seus lados iguais. Então, na minha opinião, o resultado esperado deveria ser 35, como executado, e não 25, como foi mencionado”.

Se você pensou isso, não se preocupe, é totalmente normal. Tentaremos esclarecer por que isso não está certo e, depois disso, por que isso poderia ser um problema realmente enorme em casos reais.

O primeiro ponto aqui é que geralmente somos ajudados pelo polimorfismo quando queremos abstrair as coisas. Por exemplo, se tivermos um aplicativo de mercado, e neste aplicativo temos uma tela que lista todos os itens que o usuário pode comprar. Ao implementar isso, provavelmente teríamos uma abstração que agruparia todos os diferentes tipos de itens.

Isso seria muito importante para tornar mais fácil ao parsear os dados de uma API, por exemplo. Além disso, abstrair os diferentes itens tornaria o aplicativo independente do que poderia ser enviado do servidor, e isso significa que o servidor poderia alterar a resposta a qualquer momento, com o aplicativo ainda sendo capaz de lidar com isso sem nenhum problema.

O que estamos tentando dizer é que, se pegarmos um único item e mudarmos algumas de suas propriedades, ou chamarmos um de seus métodos, não sabemos com qual tipo de item estaríamos lidando. Então, seria muito bom se nada inesperado acontecesse, certo?

Agora, voltando ao nosso exemplo, imagine que o retângulo é o nosso único item, que não sabemos que tipo pode ser, um quadrado, um retângulo ou mesmo outros tipos, se o tivermos. A única coisa que sabemos sobre isso é que é um retângulo. Então, é justo que este objeto se comporte como um retângulo, certo? E é aí que está o problema neste exemplo. O objeto não está se comportando como um retângulo.

Outro exemplo muito bom em que quebrar esse princípio pode ser terrível para nós, desenvolvedores. Por exemplo, quando estamos consumindo um framework. Não queremos e não precisamos conhecer todas as estruturas privadas que este framework possui, especialmente quando se usa uma pública. Então, seria muito bom se tivéssemos comportamentos esperados para essas estruturas públicas, que não dependessem do nosso conhecimento das estruturas privadas.

 

Exemplo 1 – Respeitando o LSP

Com o intuito de respeitar o LSP naquele exemplo, nós poderíamos implementá-lo diferente, como mostrado abaixo.

Basicamente, se preferirmos composição em vez de herança, resolveremos esse problema de uma maneira mais fácil. Criar um protocolo para centralizar o mesmo comportamento que ambas as estruturas deveriam ter vai garantir isso. Ao consumirmos esses métodos em comum do protocolo, não usamos propriedades ou métodos que não deveríamos. E por causa disso, não teremos comportamentos inesperados.

 

Exemplo 2 – LSP & OCP

Considere agora esse novo exemplo, onde temos uma superclasse Shape, e duas subclasses, Square e Circle. Basicamente, nesse exemplo, a classe Shape funciona similarmente a uma implementação de classe abstrata.

Além disso, temos um método draw(shape:), onde consumimos essas classes e seus métodos de desenho. Neste método recebemos um objeto do tipo Shape, e tentamos fazer um cast para cada um de nossos tipos de subclasses, para poder então desenhá-lo.

Estamos quebrando o LSP neste código porque um objeto parâmetro deste método do tipo de subclasse Square se comporta de maneira diferente de um objeto do tipo de superclasse Shape. Se o método recebe o filho, ele desenha, e se recebe o pai, não faz nada. A mesma diferença ocorre se o método receber objetos do tipo da subclasse Circle em comparação com um objeto do tipo da superclasse Shape.

Além disso, não é o ponto deste artigo, mas algo interessante neste exemplo é que ele também está quebrando o OCP, porque este método não está fechado para modificação se decidirmos estender nossos tipos de formas. Por exemplo, se decidirmos ter um Triângulo, teríamos que adicionar uma instrução if nesse método para poder desenhá-lo.

Exemplo 2 – Problemas em quebrar o LSP

Não será necessária uma explicação tão detalhada como no primeiro exemplo, porque basicamente os problemas estão muito próximos. Como desenvolvedores, não queremos saber tudo o que os outros desenvolvedores estão implementando e definitivamente não podemos fazer isso. Aqui está a importância de se ter ótimas interfaces entre softwares.

Uma primeira etapa realmente importante a ser realizada é desenvolver um software em que as subclasses privadas não tenham nenhum comportamento público diferente em comparação com a classe base (ou superclasse) pública. Isso vai para métodos, classes e também frameworks e APIs. E este é o principal problema que temos no exemplo anterior.

 

Exemplo 2 – Respeitando o OCP

Uma possibilidade de fazermos o segundo exemplo respeitar o LSP é criarmos um protocolo com uma método draw() em comum.

Isso evitaria ter comportamentos diferentes para um objeto do tipo superclasse e um objeto do tipo  subclasse, resolvendo a quebra do LSP.

Além disso, isso tornaria o método draw(shape:) fechado para modificação ao estender novas formas. Porque a nova forma teria que implementar o protocolo, e portanto deveria ter o método draw() por causa disso.

Resumindo LSP

  • Este princípio diz que Programas que fazem referência a um objeto de uma classe base devem ser capazes de usar um objeto de uma classe derivada sem diferenças de comportamento e sem saber sobre sua existência.
  • Garantir essa condição é positivo por causa de alguns motivos:
    1. Isso evitará que outros desenvolvedores tenham que conhecer estruturas privadas e seus comportamentos ao consumir uma estrutura, uma API ou qualquer entidade de software.
    2. Isso com certeza ajudará a reduzir bugs, uma vez que comportamentos inesperados são definitivamente sinônimos de bugs. 
    3. Isso tornará mais fácil a vida de outros desenvolvedores que terão que manter ou alterar alguns de seus códigos.
  • Este princípio pode estar realmente correlacionado com o OCP, uma vez que quando quebramos o LSP, frequentemente quebramos o OCP simultaneamente. Então, todas as desvantagens lidas no artigo anterior desta série seriam aplicadas, o que não é muito bom.
  • Finalmente, para evitar quebrar este princípio, a melhor recomendação é preferir composição em vez de herança para as abstrações. Com certeza não é uma bala de prata, mas realmente acreditamos que resolve a maioria dos casos.

Referências

  1. https://en.wikipedia.org/wiki/SOLID
  2. https://en.wikipedia.org/wiki/Robert_C._Martin
  3. Robert C. Martin. Design Principles and Design Patterns, 2000

Esse é o terceiro artigo da série de cinco artigos sobre SOLID e seu uso em Swift. Espero que tenham gostado e sintam-se livres para deixar feedbacks, sugerir melhorias ou até mesmo me enviar uma mensagem.

 

Outros artigos que podem ser interessantes para você:

Rodrigo Máximo

Rodrigo Máximo

Rodrigo Noronha Máximo Computer Engineer at Unicamp iOS Developer at Movilepay

Deixe um comentário

Categorias

Posts relacionados

Siga-nos

Baixe nosso e-book!

%d blogueiros gostam disto: