Acho que tem um detalhe que vc não entendeu: usar if
é uma das formas de implementar esses patterns.
Um design pattern só descreve de forma geral (teórica, genérica) o que é pra fazer. Mas como isso será feito fica a cargo de quem for implementar.
Vc pode fazer com if
, switch
, polimorfismo, ponteiro de função ou seja lá o que for que a linguagem que está usando disponibiliza. Se no fim o código faz o que o design pattern descreve, então vc o usou - mesmo que ache que não :-) Claro que dá pra discutir qual solução é "melhor" de acordo com vários critérios arbitrários, mas isso vai além da definição do pattern (de novo: ele só diz o que deve ser feito, mas não como).
E repito que o grande problema é que as implementações orientadas a objeto se tornaram tão populares que muita gente acha que é a única forma de usar DP. Ou pior, muitos acham que se não usar classes ou uma linguagem orientada a objeto (ou se "trocar por um if
"), então não está usando DP.
Só pra dar um exemplo prático (adaptado do link que indiquei acima, que por sua vez tem o link pra este artigo). Vamos supor que um site de e-commerce tenha categorias diferentes de consumidor, e cada um tem uma faixa de desconto: clientes bronze têm 2% de desconto, clientes prata têm 5% e clientes ouro têm 10%.
Então na hora de calcular o preço, eu preciso saber a categoria do cliente e aplicar o respectivo desconto. Ou seja, tem um comportamento diferente dependendo de cada caso. Ou, para ser mais técnico, eu posso selecionar um algoritmo diferente de cálculo de desconto em runtime, de acordo com determinados parâmetros (no caso, as condições - seja lá quais forem - que determinam a categoria de um cliente).
E olha só, toda esta situação (precisar escolher um algoritmo/comportamento diferente em cada caso) tem um nome: strategy!
Então eu poderia implementar com if
ou switch
(exemplos em JavaScript, sem nenhum motivo especial):
function calcularPreco(categoria, valor) {
if (categoria == 'bronze') {
return valor * 0.98;
} else if (categoria == 'prata') {
return valor * 0.95;
} else if (categoria == 'ouro') {
return valor * 0.9;
}
return valor;
}
function calcularPreco(categoria, valor) {
switch (categoria) {
case 'bronze':
return valor * 0.98;
case 'prata':
return valor * 0.95;
case 'ouro':
return valor * 0.9;
default:
return valor;
}
}
Qual desses é a implementação do strategy? Ambos!
"Ah, mas o livro do GoF diz pra usar classes, interfaces, polimorfismo, blablabla"
E daí? E se eu estiver usando uma linguagem que não é orientada a objetos? No link que indiquei, ao final, tem vários links para artigos com implementações dos patterns em C (o exemplo acima foi adaptado deste).
Mas aqui caímos em outra questão, que vai além dos patterns. E talvez seja por isso que muita gente acha que só existe uma única forma de implementar cada um deles.
Os exemplos acima têm alguns problemas (também citados no mesmo artigo). O principal - a meu ver - é que agora o cálculo de desconto está fortemente acoplado com as categorias. Se uma nova categoria surgir, eu preciso mudar o cálculo de preço.
Indo mais além, vamos supor que existam outras coisas associadas à categoria. Por exemplo, o preço do frete pode ser diferente, os descontos podem aumentar de acordo com a quantidade de itens (e essa quantidade também varia para cada categoria), clientes ouro podem escolher mais parcelas, etc etc etc. E vamos supor que para cada uma dessas situações existe uma função com um if
ou switch
.
Então se uma categoria nova é adicionada (ou alguma existente é removida, ou algum desses valores muda para uma delas), vc precisará mudar todas as funções associadas. O que era só um "simples if
" se torna um pesadelo de manutenção.
E como resolver? Uma solução é desacoplar as categorias dos respectivos cálculos. E é aí que surge a implementação "clássica" com classes:
class CategoriaBronze {
calcularDesconto(valor) {
return valor * 0.98;
}
}
class CategoriaPrata {
calcularDesconto(valor) {
return valor * 0.95;
}
}
class CategoriaOuro {
calcularDesconto(valor) {
return valor * 0.9;
}
}
function calcularPreco(categoria, valor) {
if (!categoria)
return valor;
return categoria.calcularDesconto(valor);
}
Obs: claro que na implementação "clássica" existe uma interface (ou classe abstrata) Categoria
, da qual todas as categorias herdam. Mas a ideia geral é essa.
Desta forma, para a função calcularPreco
tanto faz se eu criar, remover ou modificar alguma categoria, pois eu não preciso mais mudá-la.
E no caso das categorias terem mais coisas (cálculo de desconto, de frete, benefícios específicos, etc), basta adicionar os métodos em cada uma. E cada função só recebe a categoria, e delega para ela os respectivos cálculos.
Ou seja, em vez disso:
function calcularPreco(categoria, valor) {
if (categoria == 'bronze') {
return valor * 0.98;
} else if (categoria == 'prata') {
return valor * 0.95;
} else if (categoria == 'ouro') {
return valor * 0.9;
}
return valor;
}
function maximoParcelas(categoria) {
if (categoria == 'ouro') {
return 20;
}
return 10;
}
Eu poderia ter isso:
class Categoria {
maxParcelas() {
return 10;
}
calcularDesconto(valor) {
return valor;
}
}
class CategoriaBronze extends Categoria {
calcularDesconto(valor) {
return valor * 0.98;
}
}
class CategoriaPrata extends Categoria {
calcularDesconto(valor) {
return valor * 0.95;
}
}
class CategoriaOuro extends Categoria {
calcularDesconto(valor) {
return valor * 0.9;
}
maxParcelas() {
return 20;
}
}
function calcularPreco(categoria, valor) {
if (!categoria)
return valor;
return categoria.calcularDesconto(valor);
}
function maximoParcelas(categoria) {
if (!categoria)
return 5;
return categoria.maxParcelas();
}
No primeiro código, se eu mudar alguma categoria (seja adicionando, removendo ou modificando uma existente), precisarei verificar todas as funções (calcularPreco
e maximoParcelas
). E vamos supor que o sistema tem mais trocentas funções para tratar de diferentes aspectos relacionados à categoria (cada uma tem um preço de frete diferenciado, ofertas especiais em itens específicos, brindes, etc etc etc). Se eu usar if
/switch
, cada alteração nas categorias implica em ter que revisar o código de todas essas funções.
Já se usar o segundo código, eu não preciso alterar as funções calcularPreco
e maximoParcelas
(e nem todas as outras trocentas funções que mencionei). Claro que ainda vou ter que testar tudo, mas pelo menos eu não precisei revisar o código de todas elas pra saber qual precisa ser modificada (em caso de adicionar ou remover uma categoria, por exemplo, eu teria que mexer em todas se usasse if
).
"Ah, então só dá pra fazer com classes?". Claro que não. Menciono novamente o artigo que explica como fazer em C, e ele usa ponteiros de função.
Ou seja, se vc só "trocar por if
", ainda estará implementando o pattern. Mas existem outras questões que vão além desta simples troca, como a dificuldade de manutenção em um código com alto acomplamento.
Talvez por isso muita gente associe o pattern com "não use if
", ou ache que se usar if
não está implementando o pattern. Está sim, mas talvez não seja da melhor forma, por causa desses problemas de manutenção.