Back-End

27 mar, 2015

Refatoração utilizando Strategy com PHP

Publicidade

Neste artigo, vamos descrever um caso de uso e uma implementação do padrão Strategy em PHP, com o intuito de demonstrar como a linguagem tem avançado e melhorado nos últimos tempos, em detrimento de épocas onde era tido como “fraco” ou “incompleto”.

Atualmente, o PHP possui um excelente suporte á orientação a objetos, algo que há tempos atrás fazia muitos programadores sentirem certo “receio” quando iniciavam-se debates sobre o assunto relacionado à PHP.

Um problema, uma solução

Em um sistema desenvolvido para um laboratório de estatística foi solicitado o seguinte requisito:

  • É necessário que o sistema efetue o cálculo de médias com base nos valores informados pelos pesquisadores;
  • Inicialmente existirão 2 cálculos para média:
    • Cálculo da média aritmética, ao qual o pesquisador deverá informar os valores e o sistema, por sua vez, efetuará o cálculo devolvendo a média obtida;
    • Cálculo da média ponderada, onde o pesquisador deverá sempre informar a nota seguida de seu respectivo peso, e o sistema procederá efetuando o cálculo e retornando a média obtida.

A solução atual

O desenvolvedor responsável pela criação de tal requisito sugeriu a criação de uma classe que receberia os parâmetros necessários e realizaria os cálculos através de seus métodos para cálculos de média.

diagramaSolucaoAtual

Estudo da solução atual

O código da classe resolve o problema. A classe possui atributos internos utilizados para fazer o cálculo, métodos que adicionam e utilizam dados para o cálculo e dois métodos cada um com seu algoritmo de média respectivo. Contudo, essa classe viola alguns princípios SOLID, tais como o da responsabilidade única, onde uma classe deve ter um (e apenas um) motivo para mudar, e o fato de que temos os dois métodos de cálculos de média e os métodos que adicionam e recuperam os dados para os dois casos de média fazem com que a classe viole este princípio.

Outro princípio SOLID também é violado: o princípio de entidades abertas e fechadas (entidades de software – classes, módulos, funções etc – devem ser abertas para extensão, mas fechadas para modificação). Por exemplo, a necessidade de inserir um novo cálculo de média com novos métodos para adicionar e recuperar valores específicos a essa nova rotina de cálculo, seria um motivo para uma alteração na classe Média (Average) em um código já existente e funcional, que não deveria sofrer mais alterações após colocado em produção. Afinal, mexer em uma parte de código que já está funcionando aumenta os riscos de quebra do aplicativo.

Refletir sobre tais pontos é importante, pois não é desejável que, após implementar um novo requisito, os requisitos antigos deixem de funcionar por alguma falha de programação que possa ter ocorrido no código em funcionamento, enquanto o novo era implementado. Logo, é necessário preocupar-se com o design do código escrito e com a forma como ele pode escalonar para atender novos requisitos sem influenciar os já existentes.

Uma solução que não viole os princípios

A partir do momento que as fraquezas surgem em um código, tenhamos em mente os seguintes pensamentos:

“É necessário separar o cálculo de média ponderada e seus parâmetros de entrada, do cálculo de média aritmética e seus parâmetros de entrada”.

“Cada algoritmo de cálculo de média possui uma implementação diferente”.

Notar a violação dos princípios SOLID resultou em tais pensamentos, que por sua vez facilitam as coisas, definindo de forma clara o que precisa ser feito para melhorar o código.

A solução cabível para este problema engloba um padrão de desenvolvimento (design pattern) chamado Strategy, que é um design pattern comportamental: define uma família de algoritmos e encapsula cada uma deles, permitindo que sejam intercambiáveis.

Este padrão provê uma abstração do conceito que está envolvido no problema e uma separação das variadas formas de implementação que possa existir para o conceito em questão. Isso torna o código que resolve o problema mais flexível, tornando possível realizar a modificação do comportamento sem alterar código existente, apenas adicionando novas estratégias de uso.

Para o melhoramento do código, o uso desse padrão faz sentido, pois é preciso separar as responsabilidades para que o princípio da responsabilidade única não seja violado. Enxergar o conceito envolvido no problema e toda variação que possa existir para ele é importante nesse caso, para que não se viole o princípio de entidades abertas e fechadas; ou seja, o novo código deve ser capaz de suportar novas implementações sem que seja preciso alterar o código já funcional, apenas adicionando novas estratégias. Segue um diagrama de classe do padrão aplicado à solução:

Figura 2: Strategy representando a solução do problema
Figura 2: Strategy representando a solução do problema

O conceito principal por trás do problema são as médias. Existe uma família de médias: as aritméticas, as ponderadas, geométricas etc. Logo, temos diversos tipos de cálculo para uma determinada média, que no caso é utilizada de duas formas: para o cálculo de média aritmética e para o cálculo de média ponderada. Através do diagrama também fica notável como é feita a separação das responsabilidades e o encapsulamento dos algoritmos de cálculos em suas respectivas classes. Fica evidente também como se torna fácil adicionar um novo algoritmo de cálculo de média sem precisar alterar código existente e que já está funcionando.

Outro participante importante no padrão é o contexto, no caso, os valores (Values), que pode configurar todos os parâmetros que serão utilizados pelas estratégias. No exemplo, Valores (Values) é o contexto. Sendo uma classe abstrata, que define um método de chamada para estratégia e métodos para que se adicione e recupere parâmetros que serão usados para os cálculos.

Sendo assim, a classe Valores é estendida para Ponderada (Pondered) e Aritmética (Arithmetic). O fato de tal comportamento ser necessário neste exemplo, é que o cálculo de médias ponderadas e aritméticas possuem parâmetros de entrada diferentes.

Enquanto para o cálculo de média aritmética só são esperados os valores em si, para o de média ponderada já são necessários, além dos valores, seus respectivos pesos. Logo, fazê-lo de tal forma permite que isso aconteça sem sobrecarregar as responsabilidades de uma classe, no exemplo inicial a classe Média recebe os parâmetros para os dois métodos de cálculos além de possuir também suas implementações.

Na forma refatorada isso não acontece, pois seguindo o padrão Strategy foi possível abstrair um conceito do problema, no caso os cálculos de média e, posteriormente identificando as variadas implementações de algoritmos que existiam foi possível seguir com a implementação do padrão. Possuindo uma interface Media(Average), que seria o participante estratégia (Strategy), a partir daí tornou-se possível delegar a implementação de seu método de cálculo para cada classe que derivar dessa interface. As classes que derivam dessa interface são conhecidas como estratégias concretas (Concrete Strategy).

Assim sendo, cada classe que implementar a interface Média deve implementar o seu método de cálculo da maneira que for necessário. O interessante é que, é possível ter agora, de forma separada e organizada, as mais variadas implementações de cálculo de média existente. A responsabilidade em lidar com os parâmetros necessários para cada estratégia de cálculo de média ficou por conta do contexto, como cada tipo de cálculo de média requer parâmetros de entrada diferentes, ter classes que saibam como fazê-lo para cada caso torna-se necessário.

Ao observar o código inicial e refletir sobre o que alguns princípios SOLID propõe, tornou-se possível identificar os principais pontos de melhorias que, para serem colocados em prática, fizeram o uso de um padrão que permitiu à nova implementação, a não violação dos princípios adicionando mais flexibilidade e organização ao código.