Ao criar código, você sempre tem duas opções.
- basta fazê-lo, caso em que praticamente qualquer solução funcionará para você
- ser pedante e projetar uma solução que explore as peculiaridades da linguagem e sua ideologia (linguagens OO, neste caso - o uso do polimorfismo como um meio para fornecer a decisão)
Não vou me concentrar no primeiro dos dois, porque realmente não há nada a ser dito. Se você quiser que ele funcione, deixe o código como está.
Mas o que aconteceria, se você escolhesse fazê-lo da maneira pedante e realmente resolvesse o problema com os padrões de design, da maneira que queria?
Você pode observar o seguinte processo:
Ao projetar o código OO, a maioria dos if
s que estão em um código não precisa estar lá. Naturalmente, se você deseja comparar dois tipos escalares, como int
s ou float
s, é provável que tenha um if
, mas se desejar alterar os procedimentos com base na configuração, poderá usar o polimorfismo para obter o que deseja, mover as decisões (o if
s) da lógica de negócios para um local onde os objetos são instanciados - para as fábricas .
A partir de agora, seu processo pode percorrer 4 caminhos separados:
data
não é criptografado nem compactado (não ligue, retorne data
)
data
está compactado (ligue compress(data)
e retorne)
data
está criptografado (ligue encrypt(data)
e devolva)
data
está compactado e criptografado (chame encrypt(compress(data))
e devolva)
Apenas olhando os 4 caminhos, você encontra um problema.
Você tem um processo que chama 3 (teoricamente 4, se você não contar nada como um) métodos diferentes que manipulam os dados e os retornam. Os métodos têm nomes diferentes , diferente da API pública (a maneira pela qual os métodos comunicam seu comportamento).
Usando o padrão do adaptador , podemos resolver a colisão de nomes (podemos unir a API pública) que ocorreu. Simplificando, o adaptador ajuda duas interfaces incompatíveis a trabalharem juntas. Além disso, o adaptador funciona definindo uma nova interface do adaptador, que as classes que tentam unir sua API implementam.
Esta não é uma linguagem concreta. É uma abordagem genérica, a palavra-chave any está lá para representar que pode ser de qualquer tipo, em uma linguagem como C #, você pode substituí-la por genéricos ( <T>
).
Suponho que agora você pode ter duas classes responsáveis pela compactação e criptografia.
class Compression
{
Compress(data : any) : any { ... }
}
class Encryption
{
Encrypt(data : any) : any { ... }
}
No mundo corporativo, mesmo essas classes específicas provavelmente serão substituídas por interfaces, como a class
palavra - chave seria substituída por interface
(se você estiver lidando com linguagens como C #, Java e / ou PHP) ou a class
palavra - chave permaneceria, mas o Compress
e Encrypt
métodos seriam definidos como um virtual puro , caso você codifique em C ++.
Para fazer um adaptador, definimos uma interface comum.
interface DataProcessing
{
Process(data : any) : any;
}
Então temos que fornecer implementações da interface para torná-la útil.
// when neither encryption nor compression is enabled
class DoNothingAdapter : DataProcessing
{
public Process(data : any) : any
{
return data;
}
}
// when only compression is enabled
class CompressionAdapter : DataProcessing
{
private compression : Compression;
public Process(data : any) : any
{
return this.compression.Compress(data);
}
}
// when only encryption is enabled
class EncryptionAdapter : DataProcessing
{
private encryption : Encryption;
public Process(data : any) : any
{
return this.encryption.Encrypt(data);
}
}
// when both, compression and encryption are enabled
class CompressionEncryptionAdapter : DataProcessing
{
private compression : Compression;
private encryption : Encryption;
public Process(data : any) : any
{
return this.encryption.Encrypt(
this.compression.Compress(data)
);
}
}
Ao fazer isso, você acaba com 4 classes, cada uma fazendo algo completamente diferente, mas cada uma delas fornecendo a mesma API pública. O Process
método
Na sua lógica de negócios, onde você lida com a decisão none / encryption / compressão / both, você projetará seu objeto para fazê-lo depender da DataProcessing
interface que projetamos anteriormente.
class DataService
{
private dataProcessing : DataProcessing;
public DataService(dataProcessing : DataProcessing)
{
this.dataProcessing = dataProcessing;
}
}
O processo em si pode ser tão simples como este:
public ComplicatedProcess(data : any) : any
{
data = this.dataProcessing.Process(data);
// ... perhaps work with the data
return data;
}
Não há mais condicionais. A turma DataService
não tem idéia do que realmente será feito com os dados quando eles forem passados para o dataProcessing
membro, e realmente não se importa com isso, não é de sua responsabilidade.
Idealmente, você teria testes de unidade testando as 4 classes de adaptadores que você criou para garantir que funcionem e faça seu teste ser aprovado. E se eles forem aprovados, você pode ter certeza de que eles funcionarão, não importa onde você os chame em seu código.
Então, fazendo assim, nunca mais terei if
s no meu código?
Não. É menos provável que você tenha condicionais na lógica de negócios, mas eles ainda precisam estar em algum lugar. O lugar é suas fábricas.
E isso é bom. Você separa as preocupações da criação e realmente usa o código. Se você torna suas fábricas confiáveis (em Java, você pode até usar algo como a estrutura Guice do Google), em sua lógica de negócios, não está preocupado em escolher a classe certa a ser injetada. Porque você sabe que suas fábricas funcionam e entregará o que é pedido.
É necessário ter todas essas classes, interfaces etc.?
Isso nos leva de volta ao começo.
No OOP, se você escolher o caminho para usar o polimorfismo, realmente deseja usar padrões de design, deseja explorar os recursos da linguagem e / ou deseja seguir a ideologia de tudo é um objeto, então é. E mesmo assim, este exemplo não mesmo mostrar todas as fábricas que você vai precisar e se você tivesse que refazer os Compression
e Encryption
aulas e torná-los interface em vez disso, você tem que incluir suas implementações bem.
No final, você acaba com centenas de pequenas classes e interfaces, focadas em coisas muito específicas. O que não é necessariamente ruim, mas pode não ser a melhor solução para você, se tudo o que você deseja é fazer algo tão simples quanto adicionar dois números.
Se você deseja fazê-lo rapidamente, pode pegar a solução do Ixrec , que pelo menos conseguiu eliminar os blocos else if
e else
, que, na minha opinião, são até um pouco piores que os comuns if
.
Leve em consideração que esta é a minha maneira de criar um bom design de OO. Codificando para interfaces em vez de implementações, é assim que eu faço nos últimos anos e é com essa abordagem que me sinto mais confortável.
Eu pessoalmente gosto mais da programação if-less e gostaria muito mais da solução mais longa nas 5 linhas de código. É o modo como estou acostumado a projetar código e me sinto muito confortável em lê-lo.
Atualização 2: Houve uma discussão selvagem sobre a primeira versão da minha solução. Discussão causada principalmente por mim, pela qual peço desculpas.
Decidi editar a resposta de uma maneira que é uma das maneiras de olhar para a solução, mas não a única. Também removi a parte do decorador, onde eu quis dizer fachada, que no final decidi deixar de fora completamente, porque um adaptador é uma variação da fachada.
if
declarações?