Swift: Princípio Aberto-Fechado [Artigo 2] 

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 [1]

Caso você nunca tenha ouvido falar, não se preocupe, o artigo anterior dessa série possui uma introdução a respeito: https://movile.blog/swift-principio-da-responsabilidade-unica/

Open Closed Principle (OCP)

O Princípio do Aberto-Fechado diz que “Entidades de software (classes, módulos, funções, etc) devem ser abertos para extensão, porém fechados para modificação”. 

Isso pode soar um pouco estranho, já que não é um absurdo pensar que é necessário modificar um determinado código quando se pretende estender um de seus comportamentos, o que parece então contraditório. Com os exemplos a seguir, será possível entender melhor e enxergar que isso até que faz sentido.

Exemplo 1 em Swift – Abstração

Considere o exemplo abaixo, onde são apresentadas uma classe Person e uma classe House, que basicamente é uma classe que armazena todos seus moradores, que são objetos do tipo Person.

art01

Esse exemplo não respeita o princípio de Aberto-Fechado, pois se for desejado criar um novo tipo de implementação para Person, por exemplo, criando uma nova estrutura chamada NewPerson (por qualquer motivo que seja), a classe House teria que ser modificada.

Logo, como uma extensão desse tipo causaria uma modificação, o princípio seria quebrado.

Uma possível alternativa seria então alterar a classe Person para atender exatamente às mudanças desejadas, e isso faria com que a classe House não precisasse ser alterada, certo? Sim, isso realmente não geraria mudanças na classe House.

No entanto, fazer isso também iria violar o princípio, dado que neste caso quem seria modificada é a classe Person.

Problemas em quebrar o OCP

Antes de falar da solução para esse exemplo, talvez seja importante entender melhor porque violar esse princípio é algo prejudicial ao design de um software. 

Considerando a suposição em estender o comportamento da classe Person, o fato de ter que alterar a classe House, no caso em que se crie uma nova classe NewPerson é ruim simplesmente pelo fato de que isso não necessariamente precisa acontecer.

Isso pode ser evidenciado caso o design desse código seja implementado de outra maneira. 

Nesse exemplo talvez isso não fique tão claro, mas pense em um projeto de larga escala, com diversos módulos. Certamente seria muito mais vantajoso se alterações em um determinado módulo não impactassem e nem obrigassem mudanças em outros módulos, correto? Bem, esse é o mesmo caso do exemplo acima, porém em uma escala infinitamente maior.

Ainda assim, o problema nos dois casos é exatamente o mesmo, a única diferença é o tamanho do esforço que será necessário para se obter a desejada extensão. 

Pensando agora na outra maneira sugerida, onde se altera a própria classe Person ao invés de se criar uma nova estrutura: isso também pode ser ruim, pois a aplicação pode utilizar a classe Person em diversos outros lugares, além do fato de que o código dessa classe (já em produção) garantidamente funciona e está correto.

Então qual a necessidade de alterá-lo, tendo a possibilidade de impactar seu funcionamento e gerar bugs dentro da aplicação, quando isso não é necessário e existem outros meios de se atingir o objetivo?

Respeitando o OCP

Uma possível forma de fazer com que esse código do exemplo 1 respeite o OCP é apresentada abaixo.

art02

Basicamente cria-se um protocolo Resident e faz-se com que a classe Person o implemente.

Além disso, a classe House não depende mais da estrutura Person, mas sim deste protocolo criado.

Isso faz com que a classe House esteja fechada agora para modificação, caso deseje-se estender o comportamento de uma pessoa.

Dessa forma, se for necessário criar uma nova estrutura NewPerson (que em Swift inclusive poderia ser uma struct, e não uma classe) de modo a se estender o comportamento de Person, basta conformar essa estrutura com o protocolo, e isso fará com que a extensão aconteça, sem obrigar uma modificação em House para que o código continue funcionando.

oioioi

Exemplo 2 em Swift – Enumerado

Considere agora esse segundo exemplo a seguir, onde são apresentadas duas classes que representam deeplinks, HomeDeeplink e ProfileDeeplink, que nada mais são do que estruturas utilizadas para apresentar telas.

Essas duas estruturas contêm, cada uma delas, uma propriedade type, do tipo DeeplinkType. Essa última estrutura nada mais é do que um enumerado responsável por conter cada uma das telas da aplicação.

Além disso, as duas estruturas HomeDeeplink e ProfileDeeplink, implementam um protocolo do tipo Deeplink, que as obriga a possuir essa propriedade type.

oi33

Por fim, e não menos importante, existe uma classe Router, que é responsável por receber um deeplink e executá-lo, ou seja, apresentar a sua tela correspondente.

oi34

Esse exemplo não respeita o princípio de Aberto-Fechado, pois se por algum motivo for preciso criar um novo deeplink (ou seja, estender o comportamento dos deeplinks), será preciso modificar o método execute(_ deeplink: Deeplink) do Router.

Isso vai acontecer pelo simples fato de que será preciso adicionar um novo caso no enumerado DeeplinkType( o que já representa uma modificação), e também porque será preciso adicionar o tratamento desse novo caso no método do Router (ou seja, mais uma modificação).

Isso demonstra que essas duas estruturas (Router e DeeplinkType) não estão fechadas para modificações para essa devida extensão.

Abaixo segue a exemplificação do que foi mencionado acima, isto é, a modificação do DeeplinkType e a criação a nova estrutura de Deeplink, com o nome de SettingsDeeplink. Além disso, também é retratado a alteração necessária no método do Router.

oi35

Problemas em quebrar o OCP

Não será necessário uma explicação tão densa quanto a do exemplo anterior, porque basicamente os problemas desse exemplo são exatamente os mesmos, isto é, não é necessário alterar a estrutura do Router para estender o comportamento de outro componente.

Se não é necessário e existe uma forma de não ter que fazê-lo, não há muito sentido em seguir com essa abordagem.

Além disso, é válido atribuir uma leve atenção à estrutura enumerado.

Não serão em todos os casos, mas na maioria das vezes em que se optar pelo uso dessa estrutura, é provável que o código desenvolvido acabe se tornando algo próximo do que foi exemplificado aqui, ou seja, será um código que irá conter margens para se quebrar o OCP.

Isso pode ser um problema grande, dado que podem existir inúmeros switch-cases ou if-else espalhados por todo o código, e que tratem desse enumerado.

Isso significa que a criação de um novo caso irá implicar em ter que alterar todos esses lugares, correndo o risco de se esquecer de algum, e também tendo que lidar com lógicas desconhecidas e às vezes muito complexas.

Obviamente, existirão cenários em que será necessário o uso de um enumerado, e tudo bem, isso não é um problema, desde que seja bem pensado.

Uma alternativa seria isolar o uso do switch-case referente a esse enumerado em uma única estrutura, a fim de evitar repetição de código e também de evitar que vários módulos ou classes tenham que ser alterados, recompilados e retestados por conta de uma determinada extensão.

Respeitando o OCP

Uma possível forma de fazer com que esse código do exemplo 2 respeite o OCP é apresentada abaixo, com a eliminação do enumerado e através de uma padronização dos métodos execute().

oi36

Com isso, caso se deseje adicionar um novo deeplink (SettingsDeeplink, por exemplo), basta criar essa nova classe. Não será preciso a alteração do método execute(_ deeplink: Deeplink) do Router, nem a alteração do enumerado, que não existe mais.

oi37

Estratégia de “Fechamento”

Algo extremamente importante para se deixar claro ao leitor, é que é praticamente impossível, se não impossível, fazer com que um código se torne 100% fechado para modificações.

Sempre haverá cenários não pensados ou não abordados, os quais fazem com que um determinado trecho de código precise ser alterado para que uma nova extensão ocorra.

Tendo isso em mente, uma estratégia bastante válida e coerente é tentar escolher com sabedoria quais medidas devem ser tomadas para atingir a maior e mais razoável quantidade possível de casos em que esse código se torne fechado para modificação. 

Saber quais são essas medidas a serem tomadas é adquirido com tempo, experiência e prática no design de software.

Um fator que auxilia bastante nisso é a vivência na indústria de desenvolvimento e o contato com clientes e usuários, o que faz com que o desenvolvedor/designer de software adquira repertório e consiga prever alguns cenários recorrentes, o que o ajuda a tornar seu código fechado para esse tipo de mudança mais frequente.

Algumas Heurísticas e Convenções 

Algumas heurísticas e convenções surgiram justamente por conta desse princípio. Serão citadas brevemente duas delas: Tornar todas as variáveis de uma classe privada e Não utilizar-se de variáveis globais. Aqui, presume-se que não vale entrar muito em detalhes, até para não fugir muito do assunto que este artigo tem o objetivo de tratar.

Em resumo, tornar as propriedades de uma classe privadas auxilia com relação a preservar o OCP, dado que garante que outras classes não vão acessar essas propriedades indevidamente.

Isso garante que extensões na classe com as propriedades em questão não impliquem em mudanças em outras classes que poderiam estar utilizando dessas propriedades. Em Design Orientado a Objetos chamamos isso de encapsulamento

Além disso, um outro ponto a se mencionar aqui é que propriedades serem privadas permite um controle maior da classe em questão sobre seus estados.

Por exemplo, se uma propriedade for pública isso permitirá que outras classes a utilizem e modifiquem seu estado a qualquer momento, o que pode gerar estados inesperados ao longo de toda a aplicação, além do fato de ser necessário lidar com concorrência e garantir a atomicidade dessa variável dependendo do caso.

Com relação a não utilizar-se de variáveis globais, o argumento é muito parecido com o anterior. Nenhum módulo que dependa de uma variável global pode ser considerado fechado, já que qualquer outro módulo pode utilizar essa variável e alterar seu estado.

O efeito negativo disso é exatamente o mesmo mencionado acima, comportamentos inesperados e necessidade de alteração em classes que utilizem dessa variável global quando algum extensão for necessária.

Para finalizar, é importante também dizer que essas convenções não devem ser levadas ao pé da letra, dado que podem existir cenários em que faça sentido e que realmente seja vantajoso tornar uma variável pública, ou mesmo utilizar-se de variáveis globais.

Nesse artigo apenas foi apresentado argumentos que para a maior parte dos cenários são válidos, mas que não são necessariamente uma regra. Cabe sempre ao desenvolvedor tentar analisar e medir as principais vantagens e desvantagens de cada abordagem.

Considerações Finais

Esse princípio é considerado o coração do Design Orientado a Objetos, e não à toa, afinal sua importância pode ser notada principalmente em projetos de grande escala e mal arquitetados, onde uma simples mudança acarreta em inúmeros problemas e bugs, tornando-a extremamente complexa ou até mesmo impossível de ser implementada.

Como mencionado na seção Estratégica de Fechamento, provavelmente a melhor maneira de lidar com esse princípio é tentar enxergar os mais prováveis eixos de extensão/mudança e tentar, na medida do possível, tornar seu código fechado a eles.

Resumo OCP

  • Esse princípio diz que entidades de software devem estar abertas a extensões, porém fechadas a modificações.
  • Garantir essa condição é positivo pelos seguintes fatores:
    1. Isso evitará que alterações ou extensões em um módulo acabem implicando em alterações em outros módulos, o que pode gerar um trabalho a mais, e também a recompilação e o reteste de alguns módulos de maneira muitas vezes desnecessária.
    2. Isso evitará também o surgimento de novos bugs e problemas, além de facilitar e muito a mudança de regras de negócio de uma aplicação, já que a extensão não irá impactar a alteração de outros módulos.
  • Abstrações (Uso de protocolos em Swift) em geral irão resolver o problema de violação do OCP.
  • Enumerados geralmente irão ferir o OCP, então deve-se tomar muito cuidado com seu uso. Quando for concluído que realmente é necessária a sua utilização, recomenda-se que seja centralizado o uso do switch-case que certifica-se de cada um dos seus casos. Isso evitará alterações em trechos espalhados por todo o código caso seja necessário fazê-la.
  • É impossível tornar um código 100% fechado a modificação, então o melhor a se fazer é tentar medir os principais eixos de mudança e tornar seu código fechado a eles.
  • O uso de variáveis privadas dentro das classes é uma heurística derivada desse princípio e que pode ajudar a preservá-lo. O mesmo ocorre com a heurística que diz para não se usar variáveis globais.

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 foi o segundo 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!

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: