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 2 - Princípio do Aberto para Expansão, Fechado para Modificação

by
Difficulty:IntermediateLength:LongLanguages:
This post is part of a series called The SOLID Principles.
SOLID: Part 1 - The Single Responsibility Principle
SOLID: Part 3 - Liskov Substitution & Interface Segregation Principles

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

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

Definição

Entidades de software (classes, módulos, funções, etc) devem ser abertas para expansão, porém, fechadas para modificações.

O princípio do Aberto para Expansão, fechado para Modificação (The Open/Closed Principle - OCP), é creditado a Bertrand Mayer, programador francês, publicado, primeiramente, em seu livro Object-Oriented Software Construction, de 1988.

Esse princípio cresceu em popularidade no começo dos anos 2000, quando ele passou a integrar os princípios SOLID, definido por Robert C. Martin em seu livro Agile Software Development, Principles, Patterns, and Practices e, mais tarde, republicado na versão em C# do mesmo livro Agile Principles, Patterns, and Practices in C#.

Basicamente, o que estamos falando aqui é sobre projetar nossos módulos, classes e funções, de forma que, quando uma nova funcionalidade for necessária, não precisemos modificar o código existente, mas criar novo código que será usado pelo código atual. Isso parece um pouco estranho, especialmente se você vem de linguagens como o Java, C, C++ ou C#, onde isso se aplica, não só ao código fonte em si, mas, também, ao código compilado. Nós queremos criar novas funcionalidades de forma que não seja necessário reimplantar nossos binários, executáveis ou DLLs existentes.

OCP no contexto dos Princípios SOLID

De acordo com que avançamos com esses tutoriais, podemos trabalhar os novos princípios nos contextos daqueles já discutidos. Nós já discutimos sobre o Princípio da Responsabilidade Única (SRP), que afirmou que um módulo deve ter, apenas, um único motivo para mudar. Se pensarmos no COP e no SRP, podemos observar que eles são complementares. Códigos projetados com o SRP em mente estarão bem próximos de seguir o OCP ou serão bem fáceis de fazê-los respeitar esse princípio. Quando temos código onde há, somente, uma razão para mudar, introduzir uma nova funcionalidade gerará um segundo motivo para mudança. Então, tanto o SRP quanto o OCP seriam violados. Da mesma forma, se tivermos código que só será alterado quando sua função principal mudar, e mantem-se inalterado quando novas funcionalidades são adicionadas, ele respeita o OCP e, muito provavelmente, respeitará o SRP também.

Isso não quer dizer que um código que segue o SRP sempre permitirá seguir o OCP ou vice e versa, mas, na maioria dos casos, se um deles é respeitado, conseguir respeitar o outro se torna bem simples.

O Exemplo Óbvio de Violação do OCP

De um ponto de vista, puramente, técnico, o princípio do Aberto para Extensão, Fechado para Modificação, é bem simples. Um relacionamento entre duas classes, como o que vemos logo abaixo, viola o OCP.

violate1

A classe User usa a classe Logic diretamente. Se precisarmos implementar uma segunda classe Logic, de forma que permita usar ambas as classes, atual e nova, a classe Logic já existente precisará mudar. User é diretamente ligado à implementação de Logic, não há outra maneira de prover uma nova classe Logic sem afetar a atual. E, quando falamos de linguagens fortemente tipadas, é bem possível que a classe User também necessite de mudanças. Se estamos falando de linguagens compiladas, é quase certo que tanto o executável, ou a biblioteca dinâmica (DLL), de User quanto de Logic precisarão ser recompilados e reimplantados em nossos clientes. Um processo que queremos evitar a todo custo.

Mostre-me o Código

Baseado somente no esquema acima, alguém pode deduzir que qualquer classe usando diretamente outra, na verdade, viola o OCP. E isso é verdade, estritamente falando. Percebi que é bem interessante encontrar os limites, o momento que você cria a linha e decide que é mais difícil respeitar o OCP que modificar o código existente, ou que o custo arquitetural não justifica o custo da mudança do código existente.

Vamos criar uma classe que provê o progresso de download de um arquivo, na forma de percentual, dentro de nossa aplicação. Nós temos duas classes principais, a classe Progress e a File, e, creio eu, que queremos usá-las de acordo com o teste abaixo.

Nesse teste, somos um usuário da classe Progress. Queremos obter o valor como percentual, independente do tamanho real do arquivo. Usamos a classe File como fonte de informação para a classe Progress. Um arquivo tem um tamanho em bytes e um campo chamado sent, representando a quantidade de dados que já foi enviada para download. Não precisamos nos preocupar sobre como esses valores são atualizados na aplicação. Nós só precisamos acreditar que há uma mágica que faça essa lógica por nós, para que, em um teste, possamos atribuí-los manualmente.

A classe File é só um objeto de dados com duas propriedades. Claramente, na vida real, ela conteria outras informações e comportamentos, como o nome do arquivo, os caminhos real e relativo, diretório atual, tipo, permissões e por aí vai.

A classe Progress é bem simples e injeta uma classe File através de seu construtor. Para ficar claro, nós induzimos o tipo nos parâmetros do construtor (injetamos uma variável daquele tipo em específico). Há um único método útil na classe Progress, o getAsPercent(), que pegará os valores que foram enviados bem como o tamanho do arquivo, contidos nas propriedades da classe File, e os transformará em percentual. Simples e funciona.

O código parece correto, porém, ele viola o OCP. Mas, por que? E como?

Mudança de Requerimentos

Toda aplicação que se espera que cresça, com o tempo terá novas funcionalidades adicionadas. Uma das novas funcionalidades da nossa aplicação poderia ser o *streaming* de músicas, ao invés do download dos arquivos. O tamanho de um arquivo, sendo visto através da classe File, é representado em bytes e a duração da música em segundos. Nós queremos oferecer uma barra de progresso bem bacana, mas, será que podemos reusar aquela que já temos?

Não, não podemos. Nossa barra de progresso está atrelada à classe File. Ela só entende arquivos, embora ela pudesse ser usada com conteúdo musical também. Mas, para tornar isso possível, precisamos modificá-la, precisamos fazer com que a classe Progress tenha conhecimento tanto da classe Music quanto da File. Se nosso projeto respeitasse o OCP, não precisaríamos tocar na classe File ou na Progress. Nós, simplesmente, reusaríamos a classe Progress existente e a aplicaríamos para a classe Music.

Solução 1: Lançar Mão da Natureza Dinâmica do PHP

Linguagens fracamente tipadas tem a vantagem de descobrir os tipos dos objetos em tempo de execução. Isso permite que removamos a indução de tipo do objeto que esperamos receber no construtor da classe Progress e ainda permite que o código funcione.

Agora, podemos enviar qualquer coisa para a classe Progress. E, por qualquer coisa, quero dizer qualquer coisa, mesmo:

E uma classe Music como a que vemos acima, funcionará como esperado. Podemos testá-la, criando um teste bem parecido ao teste que criamos para a classe File.

Então, basicamente, qualquer conteúdo mensurável pode ser usado com a classe Progress. Talvez devêssemos demonstrar isso no código, alterando o nome da variável em nossa classe:

Certo, mas temos um problema enorme com essa abordagem. Quando especificamos (induzimos o tipo da) a classe File nos parâmetros do construtor, nós sabiamos com o que nossa classe era capaz de lidar. Era tudo explícito e se qualquer outra coisa surgisse, um erro nos avisaria.

Mas, sem a indução de tipo, nós dependemos no fato de que, qualquer coisa que vier, deverá ter duas propriedades públicas, chamadas de "length" e "sent". De outro modo, nós teremos uma quebra de contrato.

Quebra de contrato: uma classe que sobrescreve um método da classe mãe, de forma que o contrato da classe mãe não é honrado pela classe filha. ~Source Wikipedia.

Esse é um dos sintomas apresentados em mais detalhes no curso Detecting Code Smells. Resumindo, nós não queremos acabar invocando métodos ou acessando atributos em objetos que não obedecem nosso contrato. Quando tínhamos uma indução de tipos, o contrato era especificado por ele, os campos e métodos da classe File. Agora, que temos nada, nós podemos enviar qualquer coisa, até mesmo uma cadeia de caracteres (string) e resultaria num erro grotesco.

Um teste como esses, onde enviamos uma simples cadeia de caracteres, será uma quebra de contrato:

Enquanto o resultado final é igual em ambos os casos, significando uma falha no código, o primeiro produz uma mensagem legal. Esse, por outro lado, é bem obscuro. Não há como saber o que a variável é - uma cadeia de caracteres, em nosso caso - e quais propriedades requisitadas não foram encontradas. É difícil de depurar e resolver o problema. Um programador precisa abrir a classe Progress, lê-la e entendê-la. O contrato, nesse caso, quando não especificamos explícitamente a indução de tipo, é definida pelo comportamento da classe Progress. É um contrato implícito, sabido somente pela classe Progress. Em nosso exemplo, ele é definido pelo acesso aos dois atributos, sent e length, no método getAsPercent(). Na vidade real, o contrato implícito pode ser bem complexo e difícil de descobrir só dando uma olhada na classe em questão.

Essa solução só é recomendada se nenhuma das outras sugestões abaixo forem fáceis de implementar ou se elas causarem mudanças arquiteturais muito grandes, que não justifiquem o esforço.

Solução 2: Use o Padrão de Projeto Strategy

Essa é a solução mais comum e, provavelmente, a solução mais apropriada para respeitar a OCP. É simples e efetiva.

strategy

O padrão Strategy, simplesmente, introduz o uso de uma interface. Uma interface é um tipo especial de entidade da Programação Orientada a Objetos (POO), que define um contrato entre uma classe cliente e uma classe servidora. Ambas as classes aderirão ao contrato para garantir o comportamento esperado. Pode haver inúmeras classes servidoras não relacionadas que respeitam o mesmo contrato e, dessa forma, capazes de servir à mesma classe cliente.

Em uma interface, nós podemos definir nosso comportamento. É por isso que, ao invés de usar atributos públicos, passaremos a usar getters e setters. Adaptar as outras classes não será difícil, a essa altura. Nossa IDE é capaz de fazer a maior parte do trabalho "sujo".

Como antes, começamos pelos nossos testes. Teremos de usar os setters para atribuir os valores aos atributos. Se eles, realmente, forem obrigatórios, é melhor definí-los na interface Measurable. Entretanto, tenha cuidado com o que coloca por lá. A interface está ali para definir um contrato entre a classe cliente Progress e as diferentes classes servidoras, como a File e Music. A classe Progress precisa atribuir os valores? Provavelmente, não. Então, os setters, muito provavelmente, não precisam ser definidos na interface. Tenha em mente que, se você os definisse por lá, você forçaria a todas as classes servidoras a implementá-los. Para algumas delas, até faz sentido tê-los, mas, as outras, comportam-se totalmente diferente. E se quisermos usar nossa classe Progress para mostrar a temperatura de um forno? A classe OvenTemperature pode ser inicializada com os valores através do construtor ou obter essa informação a partir de uma terceira classe. Qualquer uma das formas funcionaria. Ter setters nessa classe seria estranho.

A classe File foi um pouco modificada para acomodar os requerimentos acima. Ela, agora, implementa a interface Measurablee tem setters e getters para os atributos que estamos interessados. A classe Music está bem parecida e você pode checar eu conteúdo no código fonte anexado. Estamos quase terminando.

A classe Progress também precisa de uma pequena atualização. Agora, nós podemos especificar um tipo, usando uma indução de tipo, no construtor. O tipo esperado é o da classe Measurable. Agora, temos um contrato explícito. A classe Progress, agora, pode ficar "despreocupada" que os métodos acessados, sempre, estarão presentes, porque eles estarão definidos na interface Measurable. As classes File e Music também podem ter certeza que proveem tudo que é requesitado pela Progress ao, simplesmente, implementar todos os métodos da interface. Um requerimento que toda classe tem de realizar ao implementar uma interface.

Esse padrão de projeto é melhor explicado no curso Agile Design Patterns.

Uma Nota Sobre a Nomeação de Interface

Algumas pessoas tendem a nomear interfaces com uma letra maiúscula I, logo antes do nome da interface, ou com a palavra Interface ao final do nome dela, como, em IFile e FileInterface, respectivamente. Esse é um método de notação antigo, imposto por alguns padrões ultrapassados. Já faz muito tempo que não usamos a notação Húngara ou mesmo a especificação do tipo da variável ou objeto em seu nome para facilitar a identificação. As IDEs identificam qualquer coisa em fração de segundos. Isso ajuda a nos concentrar no que realmente queremos abstrair.

As interfaces pertencem a seus clientes. Quando quiser nomear alguma interface você deve pensar no cliente e esquecer sobre a implementação. Quando nomeamos nossa interface de Measurable, nós o fizemos pensando na classe Progress. Se eu fosse uma forma de progressão, o que eu deveria ser capaz de fazer para prover o percentual de progresso? A resposta é simples: algo que possa ser medido. Logo, o nome Measurable (mensurável, em inglês).

Outro motivo é que a implementação pode ser de vários domínios. Em nosso caso, há arquivos e músicas. Mas podemos reusar nossa classe Progress em um simulador de corridas. Neste caso, as classes medidas seriam Velocidade, Combustível, Tempo de Corrida, etc. Legal, não é?

Solução 3: Use o Padrão de Projeto do Método Modelo

O padrão de projeto do Método Modelo é bem parecido com o padrão Strategy, porém, ao invés de usar uma interface, ele usa uma classe abstrata. É recomendável usar o padrão do Método Modelo quando tivermos um cliente bastante específico à nossa aplicação, com reduzida capacidade de reutilização, e quando as classes servidoras possuírem um comportamento em comum.

template_method

Esse padrão de projeto é melhor explicado no curso Agile Design Patterns.

Uma Visão de Alto Nível

Então, como que isso afeta nossa arquitetura no geral?

HighLevelDesign

Levando em consideração que a imagem acima representa a arquitetura atual da nossa aplicação, adicionar um novo módulo com cinco classes (as que estão em azul), deve afetar nosso projeto de forma moderada (classes em vermelho).

HighLevelDesignWithNewClasses

Na maioria dos sistemas, você não pode esperar que não surja efeitos colaterais nos códigos já existentes quando adicionar novas classes. Entretanto, respeitando o OCP, irá reduzir, consideravelmente, as classes e módulos que requerem mudanças constantes.

Assim como com qualquer outro princípio, tente não estabelecer tudo desde o começo. Se o fizer, é bem fácil que acabe com uma interface para cada uma de suas classes. Tal projeto será muito difícil de manter e compreender. Geralmente, a maneira mais segura de se fazer é pensar nas possibilidades e, se conseguir, determinar se haverá outros tipos de classes servidoras. Muitas vezes, você imagina facilmente uma nova funcionalidade ou acha outra nos documentos do projeto que gerará outra classe servidora. Nesses casos, adicione uma interface desde o começo. Se você não puder determinar, ou caso esteja em dúvida, omita-a. Deixe o próximo programador, ou mesmo o seu eu do futuro, adicionar uma interface quando for necessária uma segunda implementação.

Pensamentos Finais

Se você for disciplinado e adicionar uma interface tão logo uma segunda classe servidora for necessária, as modificações serão poucas e fáceis. Lembre-se, se o código em questão muda uma vez, é bem provável que ele mudará de novo. Quando essa possibilidade tornar-se realidade, OCP salvará muito do seu tempo e esforço.

Obrigado pela leitura.

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.