30-50% off hundreds of digital assets! WordPress themes, video, music and more 30-50% Off Go to Sale
Advertisement
  1. Code
  2. PHP
Code

Princípios SOLID Parte 3 - Princípios da Substituição de Liskov e Segregação de Interfaces

by
Difficulty:BeginnerLength:MediumLanguages:
This post is part of a series called The SOLID Principles.
SOLID: Part 2 - The Open/Closed Principle
SOLID: Part 4 - The Dependency Inversion Principle

Portuguese (Português) translation by Erick Patrick (you can also view the original English article)

Responsabilidade Única (SRP - Single Responsability), berto para Expansão, Fechado para Modificações (Open/Close), Substituição de Liskov e Segregação de Interfaces, e Inversão de Dependência (Dependency Inversion). Cinco princípios ágeis que deveriam guiar você todas as vezes que fosse programar.

Pelos Princípios da Substituição de Liskov (Liskov Substitution Principle - LSP) e da Segregação de Interfaces (Interface Segregation Principle - ISP) serem bem simples de definir e exemplificar, nessa lição, nós falaremos dos dois.

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

Classes filhas nunca deveriam infringir as definições de tipo da classe pai.

O conceito desse princípio foi apresentado por Barbara Liskov em uma apresentação de uma conferência em 1987, e depois publicada em um artigo científico, junto de Jannette Wing, em 1994. A definição original é a que segue:

Seja q(x) uma propriedade que se pode provar do objeto x do tipo T. Então, q(y) também é possível provar para o objeto y do tipo S, sendo S um subtipo de T.

Pouco depois, com a publicação dos princípios SOLID por Robert C. Martin em eu livro Agile Software Development, Principles, Patterns, and Practices e republicado na versão usando C# do mesmo livro, Agile Principles, Patterns, and Practices in C#, a definição ficou conhecida como o Princípio de Substituição de Liskov.

O que nos leva à definição apresentada por Robert C. Martin:

Subtipos devem ser substituíveis pelos seus tipos base.

Tão simples quanto parece, uma subclasse deve sobrescrever os métodos da classe pai, de tal maneira que não quebre a funcionalidade do ponto de vista do cliente. Eis um exemplo que demonstra o conceito.

Dada uma classe Vehicle - ela pode ser uma classe abstrata - e duas implementações:

Uma classe cliente deve ser capaz de usar qualquer uma das duas implementações, desde que a classe cliente seja capaz de usar a classe Vehicle.

O que nos leva a uma implementação simples do Padrão de Projeto do Método Modelo, como vimos no tutorial do princípio passado, o OCP.

template_method

Baseados em nossa experiência anterior com o Princípio do Aberto para Expansão, Fechado para Modificação, nós podemos concluir que o Princípio da Substituição de Liskov é bastante relacionado ao OCP. Na verdade, "uma violação do LSP é uma violação latente ao OCP" (Robert C. Martin), e o Padrão de Projeto do Método Modelo é um clássico exemplo de respeito e implementação do LSP, que, também, é uma das soluções para implementar o OCP.

O Exemplo Clássico de Violação ao LSP

Para ilustrar isso completamente, nós mostraremos um exemplo clássico, uma vez que ele é bastante significante e de fácil entendimento.

Começamos com uma forma geométrica básica, um Rectangle (Retângulo). Ela é um simples objeto de dados com setters e getters para width (largura) e height (altura). Imagine que nossa aplicação está em funcionamento e já está implantada em diversos clientes. Agora, eles precisam de uma nova funcionalidade. Eles precisam ser capazes de manipular quadrados.

Na vida real, na geometria, um quadrado é um tipo especial de retângulo. Então, nós podemos tentar implementar uma classe Square (quadrado) que estende a classe Rectangle. Frequentemente, diz-se que uma classe filha é uma classe pai, e que essa expressão também se sujeita ao LSP, pelo menos à primeira vista.

SquareRect

Mas, uma classe Square é uma classe Rectangle, programaticamente falando?

Uma quadrado é um retângulo com largura e alturas iguais. Nós poderíamos implementar isso, usando essa forma esquisita que vemos no exemplo acima. Poderíamos sobrescrever ambos os setters para atribuir tanto a altura quanto a largura, juntas. Mas, como isso afetaria o código cliente?

É aceitável que exista uma classe cliente que verifique a área de um retângulo e lance uma exceção no caso da área estar errada.

Claro, adicionamos o método acima à nossa classe Rectangle para calcular a área da forma em questão.

E nós também criamos um testes simples, enviando um objeto retângulo vazio para o método verificador de área. O teste passa. Se nossa classe Square estiver definida corretamente, enviá-la para o método areaVerifier() da classe cliente não deveria quebrar sua funcionalidade. Afinal, uma classe Square é uma classe Rectangle em todos os sentidos matemáticos. Mas, e nos sentidos programáticos?

Testá-lo é bem simples e veremos que ele falhará fácil, fácil. Uma exceção é lançada quando executarmos o teste acima.

Então, nossa classe Square não é uma classe Rectangle, no fim das contas. Ela quebra as leis da geometria. Ela falha e viola o Princípio de Substituição de Liskov.

Gosto bastante desse exemplo porque ele não só viola o LSP, mas também demostra que a programação orientada a objetos não se trata de mapear itens da vida real em objetos programáticos. Cada objeto em nosso programa deve ser uma abstração de um conceito. Se tentarmos mapear objetos da vida real, um a um, com objetos programáticos, é quase certo que falharemos.

O Princípio da Segregação de Interfaces

O Princípio da Responsabilidade Única é sobre atores e estruturas de alto nível. O Princípio do Aberto para Expansão, Fechado para Modificação, é sobre projeto de classes e extensão de funcionalidades. O Princípio de Substituição de Liskov é sobre subtipos e herança. O Princípio da Segregação de Interfaces (ISP) é sobre lógica de negócios relacionada a comunicação com clientes.

Em todas aplicações modulares, deve existir algum tipo de interface que o cliente possa contar. Talvez elas sejam entidades do tipo Interface ou outros objetos clássicos implementando padrões de projeto como padrão Facades. Não importa qual a solução seja usada. Sempre é o mesmo escopo: mostrar ao código cliente como usar um módulo. Essas interfaces podem ficar entre módulos diferentes na mesma aplicação ou projeto, ou entre um projeto e uma biblioteca de terceiros, servindo outro projeto diferente. Mais uma vez, não interessa. Comunicação é comunicação, e clientes são clientes, independente de quem esteja escrevendo os códigos.

Então, como nós definiríamos essas interfaces? Poderíamos pensar em nosso módulo e expor todas as funcionalidades que quisermos oferecer.

hugeInterface

Isso parece um bom começo, uma ótima maneira de como implementar nosso módulo. Mas, será que é? Um começo como esse levará a um desses dois tipos de implementações:

  • Uma classe carro Car ou Bus gigante, implementando todos os métodos da interface Vehicle. Basta ver as dimensões mais simples de tais classes para vermos que devemos evita-las a todo custo.
  • Ou, muitas classes pequenas, como as classes LightsControl, SpeedControl, ou RadioCD, onde todas implementam toda a interface, porém, provendo algo de útil somente para as partes que eles implementam.

É claro que nenhuma desses soluções é aceitável para implementar a lógica do nosso negócio.

specializedImplementationInterface

Nós poderíamos escolher outra abordagem. Dividir a interface em partes, especializadas para cada implementação. Isso ajudaria a criar e usar pequenas classes que importam-se com suas próprias interfaces. Os objetos implementando as interfaces serão usados pelos diferentes tipos de veículos, como o carro na imagem acima. O carro usará as implementações, mas dependerá das interfaces. Assim, um esquema como o de logo abaixo é demonstra de forma mais clara.

carUsingInterface

Mas isso muda, fundamentalmente, nossa percepção da aruitetura. A classe Car torna-se o cliente ao invés da implementação. Nós ainda queremos prover a nossos clientes, maneiras para usarem nosso módulo por inteiro, aquele que forma um veículo.

oneInterfaceManyClients

Assuma que resolvemos o problema da implementação e temos uma lógica de negócios estável. A coisa mais fácil a fazer é prover uma única interface com todas as implementações, e deixar o cliente, em nosso caso, as classes BusStation, HighWay, Driver e por aí em diante, a usar o que quer que eles queiram da implementação de nossa interface. Basicamente, isso passa a responsabilidade de seleção de comportamento para os clientes. Você é capaz de encontrar esse tipo de solução em várias aplicações mais antigas.

O Princípio da Segregação de Interface (ISP) afirma que nenhum cliente deve ser forçado a depender de métodos que ele não use.

Entretanto, essa solução tem seus próprios problemas. Nem todos os clientes dependem de todos os métodos. Por que uma classe BusStation dependeria da situação das luzes do ônibus ou da estação de rádio selecionada pelo motorista? Bem, ele não deveria. E se ele depender? Isso importa? Bem, se pensarmos no Princípio da Responsabilidade Única, ele é um conceito irmão desse que estamos tratando. Se a classe BusStation depende de várias implementações individuais, até mesmo daqueles não usadas por ela, ela pode necessitar de mudanças se qualquer pequena mudança ocorrer na implementação das classes das quais ela depende. Isso é, especialmente, verdade para as linguagens compiladas, mas ainda podemos ver o efeito da mudança da classe LightControl impactando a classe BusStation. Esse tipo de coisa nunca deveria acontecer.

As interfaces pertencem a seus clientes e não às implementações. Assim, deveríamos, sempre, projetá-las de modo que melhor se adaptem aos clientes. Algumas vezes, nós os conhecemos, outras vezes, não. Mas, nós podemos e deveríamos dividir nossas interfaces em interfaces menores, assim, elas satisfazem, de uma forma melhor, as necessidades de nossos clientes.

segregatedInterfaces

Claro, isso levará a um certo nível de duplicação. Mas, lembre-se: Interfaces são só simples definições de nomes de funções. Não há qualquer tipo de implementação ou lógica nelas. Logo, as duplicações são mínimas e administráveis.

Então, nós temos a grande vantagem dos clientes dependerem só e somente daquilo que eles realmente precisam e usam. Em alguns casos, os clientes podem chegar a usar várias interfaces, e isso é normal, desde que eles usem todos os métodos das interfaces das quais dependam.

Outra grande dica é que, em nossa lógica de negócio, uma única classe pode implementar várias interfaces, se necessário. Logo, podemos prover um única implementação para todos os métodos em comuns das várias interfaces. Segregar as interfaces também nos forçará a pensar em nosso código, do ponto de vista do cliente, o que, por sua vez, resultará em menor acoplamento e maior testabilidade. Assim, não somente melhoramos nosso código para nossos clientes, como também o tornamos mais fácil para que, nós mesmos, possamos entendê-los, testá-los e implementá-los.

Considerações finais

O LSP nos ensinou o porque de não podermos fazer uma adaptação 1-para-1 de objetos reais em objetos programáticos, e como subtipos respeitam seus parentes. E nós também aprendemos como ele se relaciona aos outros princípios que já tínhamos aprendido.

O ISP nos ensinou a respeitar nossos clientes, mais do que pensávamos ser necessário. Respeitar as necessidades deles fará de nossos códgos muito melhor e a nossa vida, enquanto programadores, mais fácil.

Obrigado pelo seu tempo.

Seja o primeiro a saber sobre novas traduções–siga @tutsplus_pt no Twitter!

Advertisement
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.