Existe um padrão de design para remover a necessidade de verificar sinalizadores?


28

Vou salvar uma carga útil de string no banco de dados. Eu tenho duas configurações globais:

  • criptografia
  • compressão

Eles podem ser ativados ou desativados usando a configuração de maneira que apenas um deles esteja ativado, ambos estejam ativados ou desativados.

Minha implementação atual é esta:

if (encryptionEnable && !compressEnable) {
    encrypt(data);
} else if (!encryptionEnable && compressEnable) {
    compress(data);
} else if (encryptionEnable && compressEnable) {
    encrypt(compress(data));
} else {
  data;
}

Estou pensando no padrão Decorator. É a escolha certa ou existe talvez uma alternativa melhor?


5
O que há de errado com o que você tem atualmente? É provável que os requisitos sejam alterados para essa funcionalidade? IE, é provável que haja novas ifdeclarações?
Darren Young

Não, estou procurando outra solução para melhorar o código.
Damith Ganegoda 01/01

46
Você está fazendo isso ao contrário. Você não encontra um padrão e, em seguida, escreve um código para ajustá-lo. Você escreve o código para atender aos seus requisitos e, opcionalmente, usa um padrão para descrever seu código.
Lightness Races com Monica

11
nota, se você acredita que a sua pergunta é de fato uma duplicata de um presente , então, como um autor da questão que você tem a opção de "override" recente reabrir e singlehandedly fechá-la como tal. Fiz isso com algumas de minhas próprias perguntas e funciona como um encanto. Aqui está como eu fiz isso, 3 passos simples - a única diferença com os meus "instruções" é que desde que você tem menos de 3K rep, você terá que passar por diálogo bandeira para chegar a "duplicar" opção
mosquito

8
@LightnessRacesinOrbit: Há alguma verdade no que você diz, mas é perfeitamente razoável perguntar se existe uma maneira melhor de estruturar o código de alguém, e é perfeitamente razoável invocar um padrão de design para descrever uma melhor estrutura proposta. (Ainda assim, concordo que é um problema XY solicitar um padrão de design quando o que você deseja é um design , que pode ou não seguir estritamente qualquer padrão conhecido.) Além disso, é legítimo que "padrões" sejam afete um pouco o seu código, pois se você estiver usando um padrão conhecido, geralmente fará sentido nomear seus componentes de acordo.
Ruakh

Respostas:


15

Ao criar código, você sempre tem duas opções.

  1. basta fazê-lo, caso em que praticamente qualquer solução funcionará para você
  2. 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 ifs que estão em um código não precisa estar lá. Naturalmente, se você deseja comparar dois tipos escalares, como ints ou floats, é 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 ifs) 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:

  1. datanão é criptografado nem compactado (não ligue, retorne data)
  2. dataestá compactado (ligue compress(data)e retorne)
  3. dataestá criptografado (ligue encrypt(data)e devolva)
  4. dataestá 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 classpalavra - chave seria substituída por interface(se você estiver lidando com linguagens como C #, Java e / ou PHP) ou a classpalavra - chave permaneceria, mas o Compresse Encryptmé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 Processmé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 DataProcessinginterface 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 DataServicenão tem idéia do que realmente será feito com os dados quando eles forem passados ​​para o dataProcessingmembro, 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 ifs 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 Compressione Encryptionaulas 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 ife 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.


28
Eu não diminuí a votação, mas a lógica pode ser a quantidade ridícula de novas classes / interfaces para fazer algo que o código original fez em 8 linhas (e a outra resposta fez em 5). Na minha opinião, a única coisa que ele realiza é aumentar a curva de aprendizado do código.
Maurycy

6
@Maurycy O que o OP pediu foi tentar encontrar uma solução para seu problema usando padrões de design comuns, se essa solução existir. Minha solução é mais longa que o código dele ou do Ixrec? Isto é. Eu admito isso. Minha solução resolve seu problema usando padrões de design e, assim, responde à sua pergunta e também remove efetivamente todos os ifs necessários do processo? Faz. Ixrec não.
Andy

26
Acredito que escrever um código claro, confiável, conciso, com desempenho e manutenibilidade seja o caminho a seguir. Se eu tivesse um dólar por cada vez que alguém citasse o SOLID ou citasse um padrão de software sem articular claramente seus objetivos e sua lógica, eu seria um homem rico.
Robert Harvey

12
Eu acho que tenho dois problemas que vejo aqui. A primeira é que as interfaces Compressione Encryptionparecem totalmente supérfluas. Não tenho certeza se você está sugerindo que eles são de alguma forma necessários para o processo de decoração ou apenas implica que eles representam conceitos extraídos. A segunda questão é que a criação de uma classe CompressionEncryptionDecoratorleva ao mesmo tipo de explosão combinatória que as condicionais do OP. Também não estou vendo o padrão do decorador com clareza suficiente no código sugerido.
Cbojar

5
O debate sobre o SOLID vs. simple está meio que esquecido: esse código não é nenhum e também não usa o padrão decorador. O código não é automaticamente SOLID apenas porque ele usa várias interfaces. A injeção de dependência de uma interface DataProcessing é bastante agradável; tudo o resto é supérfluo. O SOLID é uma preocupação no nível da arquitetura que visa lidar bem com as mudanças. O OP não forneceu informações sobre sua arquitetura nem como ele espera que seu código seja alterado; portanto, não podemos sequer discutir o SOLID em uma resposta.
Carl Leth

120

O único problema que vejo no seu código atual é o risco de explosão combinatória à medida que você adiciona mais configurações, que podem ser facilmente mitigadas estruturando o código da seguinte maneira:

if(compressEnable){
  data = compress(data);
}
if(encryptionEnable) {
  data = encrypt(data);
}
return data;

Não conheço nenhum "padrão de design" ou "idioma" do qual possa ser considerado um exemplo.


18
@DamithGanegoda Não, se você ler atentamente meu código, verá que ele faz exatamente a mesma coisa nesse caso. É por isso que não há elseentre minhas duas declarações if e por que estou atribuindo datacada vez. Se os dois sinalizadores forem verdadeiros, o compress () será executado, e o encrypt () será executado no resultado do compress (), como você deseja.
Ixrec

14
@DavidPacker Tecnicamente, o mesmo acontece com todas as declarações if em todas as linguagens de programação. Eu fui pela simplicidade, pois isso parecia um problema em que uma resposta muito simples era apropriada. Sua solução também é válida, mas, pessoalmente, eu a guardaria quando tiver muito mais do que dois sinalizadores booleanos para se preocupar.
Ixrec

15
@ DavidPacker: correct não está definido pela forma como o código segue algumas orientações de algum autor sobre alguma ideologia de programação. Correto é "o código faz o que deveria fazer e foi implementado em um período de tempo razoável". Se faz sentido fazê-lo da "maneira errada", então a maneira errada é a maneira certa, porque tempo é dinheiro.
Whatsisname

9
@ DavidPacker: Se eu estava na posição do OP e fiz essa pergunta, o Lightness Race no comentário da Orbit é o que eu realmente preciso. "Encontrar uma solução usando padrões de design" já está começando com o pé errado.
Whatsisname

6
@DavidPacker Na verdade, se você ler a pergunta mais de perto, ela não insiste em um padrão. Ele afirma: "Estou pensando no padrão Decorator. É a escolha certa ou talvez haja uma alternativa melhor?" . Você abordou a primeira frase da minha citação, mas não a segunda. Outras pessoas adotaram a abordagem de que não, não é a escolha certa. Você não pode então afirmar que apenas o seu responde à pergunta.
Jon Bentley

12

Eu acho que sua pergunta não está procurando praticidade, nesse caso a resposta do lxrec é a correta, mas para aprender sobre padrões de design.

Obviamente, o padrão de comando é um exagero para um problema tão trivial quanto o que você propõe, mas para fins de ilustração, aqui está:

public interface Command {
    public String transform(String s);
}

public class CompressCommand implements Command {
    @Override
    public String transform(String s) {
        String compressedString=null;
        //Compression code here
        return compressedString;
    }
}

public class EncryptCommand implements Command {
    @Override
    public String transform(String s) {
        String EncrytedString=null;
        // Encryption code goes here
        return null;
    }

}

public class Test {
    public static void main(String[] args) {
        List<Command> commands = new ArrayList<Command>();
        commands.add(new CompressCommand());
        commands.add(new EncryptCommand()); 
        String myString="Test String";
        for (Command c: commands){
            myString = c.transform(myString);
        }
        // now myString can be stored in the database
    }
}

Como você vê, colocar os comandos / transformação em uma lista permite que eles sejam executados sequencialmente. Obviamente, ele executará ambos, ou apenas um deles dependerá do que você colocar na lista sem as condições.

Obviamente, os condicionais terminarão em algum tipo de fábrica que reúne a lista de comandos.

EDITAR para o comentário de @ texacre:

Existem muitas maneiras de evitar as condições if na parte criadora da solução, vamos usar, por exemplo, um aplicativo GUI de desktop . Você pode ter caixas de seleção para as opções de compactar e criptografar. No on cliccaso dessas caixas de seleção, instancie o comando correspondente e adicione-o à lista ou remova-o da lista se estiver desmarcando a opção.


A menos que você possa fornecer um exemplo de "algum tipo de fábrica que reúna a lista de comandos" sem código que se pareça essencialmente com a resposta do Ixrec, o IMO não responderá à pergunta. Isso fornece uma maneira melhor de implementar as funções de compactação e criptografia, mas não como evitar os sinalizadores.
thexacre

@thexacre Adicionei um exemplo.
Tulains Córdova

Então, no seu ouvinte de evento da caixa de seleção, você tem "if checkbox.ticked then add command"? Parece-me como se estivesse apenas baralhar a bandeira se as declarações em torno ...
thexacre

@thexacre Não, um ouvinte para cada caixa de seleção. No evento click, apenas commands.add(new EncryptCommand()); ou commands.add(new CompressCommand());respectivamente.
Tulains Córdova 03/03

Que tal desmarcar a caixa? Em quase todos os kits de ferramentas de idioma / interface do usuário que encontrei, você ainda precisará verificar o estado da caixa de seleção no ouvinte de eventos. Concordo que esse é um padrão melhor, mas não evita a necessidade basicamente de que a flag faça algo em algum lugar.
precisa saber é o seguinte

7

Eu acho que os "padrões de design" são desnecessariamente direcionados para "oo padrões" e evitam completamente idéias muito mais simples. O que estamos falando aqui é um pipeline de dados (simples).

Eu tentaria fazê-lo em clojure. Qualquer outro idioma em que as funções sejam de primeira classe provavelmente também está ok. Talvez eu pudesse um exemplo de C # mais tarde, mas não é tão bom. Minha maneira de resolver isso seria os seguintes passos com algumas explicações para não-clojurianos:

1. Represente um conjunto de transformações.

(def transformations { :encrypt  (fn [data] ... ) 
                       :compress (fn [data] ... )})

Este é um mapa, ou seja, uma tabela de pesquisa / dicionário / o que for, de palavras-chave a funções. Outro exemplo (palavras-chave para strings):

(def employees { :A1 "Alice" 
                 :X9 "Bob"})

(employees :A1) ; => "Alice"
(:A1 employees) ; => "Alice"

Então, escrevendo (transformations :encrypt)ou (:encrypt transformations)retornaria a função de criptografia. ( (fn [data] ... )é apenas uma função lambda.)

2. Obtenha as opções como uma sequência de palavras-chave:

(defn do-processing [options data] ;function definition
  ...)

(do-processing [:encrypt :compress] data) ;call to function

3. Filtre todas as transformações usando as opções fornecidas.

(let [ transformations-to-run (map transformations options)] ... )

Exemplo:

(map employees [:A1]) ; => ["Alice"]
(map employees [:A1 :X9]) ; => ["Alice", "Bob"]

4. Combine funções em uma:

(apply comp transformations-to-run)

Exemplo:

(comp f g h) ;=> f(g(h()))
(apply comp [f g h]) ;=> f(g(h()))

5. E então juntos:

(def transformations { :encrypt  (fn [data] ... ) 
                       :compress (fn [data] ... )})

(defn do-processing [options data]
  (let [transformations-to-run (map transformations options)
        selected-transformations (apply comp transformations-to-run)] 
    (selected-transformations data)))

(do-processing [:encrypt :compress])

O ONLY muda se queremos adicionar uma nova função, digamos "debug-print", é o seguinte:

(def transformations { :encrypt  (fn [data] ... ) 
                       :compress (fn [data] ... )
                       :debug-print (fn [data] ...) }) ;<--- here to add as option

(defn do-processing [options data]
  (let [transformations-to-run (map transformations options)
        selected-transformations (apply comp transformations-to-run)] 
    (selected-transformations data)))

(do-processing [:encrypt :compress :debug-print]) ;<-- here to use it
(do-processing [:compress :debug-print]) ;or like this
(do-processing [:encrypt]) ;or like this

Como os funcs são preenchidos para incluir apenas as funções que precisam ser aplicadas sem usar essencialmente uma série de instruções if de uma maneira ou de outra?
thexacre

A linha funcs-to-run-here (map options funcs)está fazendo a filtragem, escolhendo assim um conjunto de funções a serem aplicadas. Talvez eu deva atualizar a resposta e entrar em mais detalhes.
precisa saber é o seguinte

5

[Essencialmente, minha resposta é uma continuação da resposta de @Ixrec acima . ]

Uma pergunta importante: o número de combinações distintas que você precisa cobrir vai crescer? Você conhece melhor o seu domínio de assunto. Este é o seu julgamento a fazer.
O número de variantes pode aumentar? Bem, isso não é inconcebível. Por exemplo, pode ser necessário acomodar mais algoritmos de criptografia diferentes.

Se você antecipar que o número de combinações distintas aumentará, o padrão de estratégia poderá ajudá-lo. Ele foi projetado para encapsular algoritmos e fornecer uma interface intercambiável para o código de chamada. Você ainda teria uma pequena quantidade de lógica ao criar (instanciar) a estratégia apropriada para cada sequência específica.

Você comentou acima que não espera que os requisitos sejam alterados. Se você não espera que o número de variantes aumente (ou se você pode adiar essa refatoração), mantenha a lógica do jeito que está. Atualmente, você tem uma quantidade pequena e gerenciável de lógica. (Talvez faça uma observação nos comentários sobre uma possível refatoração para um padrão de estratégia.)


1

Uma maneira de fazer isso no scala seria:

val handleCompression: AnyRef => AnyRef = data => if (compressEnable) compress(data) else data
val handleEncryption: AnyRef => AnyRef = data => if (encryptionEnable) encrypt(data) else data
val handleData = handleCompression andThen handleEncryption
handleData(data)

Usar o padrão decorador para atingir os objetivos acima (separação da lógica de processamento individual e como eles se conectam) seria muito detalhado.

Onde você exigiria um padrão de design para atingir esses objetivos em um paradigma de programação OO, a linguagem funcional oferece suporte nativo usando funções como cidadãos de primeira classe (linhas 1 e 2 no código) e composição funcional (linha 3)


Por que isso é melhor (ou pior) do que a abordagem do OP? E / ou o que você acha da ideia do OP de usar um padrão de decorador?
Kasper van den Berg

esse trecho de código é melhor e é explícito sobre pedidos (compactação antes da criptografia); evita as interfaces indesejados
Rag
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.