¿Existe un patrón de diseño para eliminar la necesidad de verificar las banderas?

28

Voy a guardar una carga útil de cadena en la base de datos. Tengo dos configuraciones globales:

  • cifrado
  • compresión

Estos pueden habilitarse o deshabilitarse utilizando la configuración de forma que solo uno de ellos esté habilitado, ambos estén habilitados o ambos estén deshabilitados.

Mi implementación actual es esta:

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

Estoy pensando en el patrón Decorador. ¿Es la elección correcta, o tal vez hay una mejor alternativa?

Damith Ganegoda
fuente
55
¿Qué tiene de malo lo que tienes actualmente? ¿Es probable que los requisitos cambien para esta funcionalidad? IE, ¿es probable que haya nuevas ifdeclaraciones?
Darren Young
No, estoy buscando cualquier otra solución para mejorar el código.
Damith Ganegoda el
46
Vas sobre esto al revés. No encuentra un patrón y luego escribe el código para que se ajuste al patrón. Usted escribe el código para cumplir con sus requisitos, luego, opcionalmente, usa un patrón para describir su código.
Lightness compite con Monica el
1
tenga en cuenta que si cree que su pregunta es un duplicado de esta , entonces, como autor de la pregunta, tiene la opción de "anular" la reapertura reciente y cerrarla como tal. Hice eso a algunas de mis propias preguntas y funciona de maravilla. Así es como lo hice, 3 pasos sencillos : la única diferencia con mis "instrucciones" es que, dado que tienes menos de 3K rep, tendrás que pasar por el cuadro de diálogo de marca para llegar a la opción "duplicar"
mosquito
8
@LightnessRacesinOrbit: Hay algo de verdad en lo que dices, pero es perfectamente razonable preguntar si hay una mejor manera de estructurar el código, y es perfectamente razonable invocar un patrón de diseño para describir una mejor estructura propuesta. (Aún así, estoy de acuerdo en que es un poco un problema XY pedir un patrón de diseño cuando lo que quieres es un diseño , que puede o no seguir estrictamente cualquier patrón conocido). Además, es legítimo que los "patrones" afectará ligeramente su código, ya que si está utilizando un patrón conocido, a menudo tiene sentido nombrar sus componentes en consecuencia.
ruakh

Respuestas:

15

Al diseñar el código, siempre tiene dos opciones.

  1. solo hazlo, en cuyo caso prácticamente cualquier solución funcionará para ti
  2. Sea pedante y diseñe una solución que explote las peculiaridades del lenguaje y su ideología (en este caso, los lenguajes OO: el uso del polimorfismo como un medio para tomar la decisión)

No me voy a centrar en el primero de los dos, porque realmente no hay nada que decir. Si solo desea que funcione, puede dejar el código tal como está.

Pero, ¿qué pasaría si eliges hacerlo de forma pedante y realmente resuelves el problema con los patrones de diseño, de la manera que quisieras?

Podrías estar mirando el siguiente proceso:

Al diseñar el código OO, la mayoría de los ifs que están en un código no tienen que estar allí. Naturalmente, si desea comparar dos tipos escalares, como ints o floats, es probable que tenga un if, pero si desea cambiar los procedimientos en función de la configuración, puede usar el polimorfismo para lograr lo que desea, mover las decisiones (el ifs) de su lógica de negocios a un lugar, donde se crean instancias de objetos, a fábricas .

A partir de ahora, su proceso puede pasar por 4 caminos separados:

  1. datano está encriptado ni comprimido (no llamar a nada, regresar data)
  2. dataestá comprimido (llamar compress(data)y devolverlo)
  3. dataestá cifrado (llame encrypt(data)y devuélvalo)
  4. dataestá comprimido y encriptado (llame encrypt(compress(data))y devuélvalo)

Simplemente mirando los 4 caminos, encuentras un problema.

Tiene un proceso que llama a 3 (en teoría 4, si cuenta no llamar a nada como uno) diferentes métodos que manipulan los datos y luego los devuelve. Los métodos tienen diferentes nombres , diferentes llamadas API públicas (la forma en que los métodos comunican su comportamiento).

Usando el patrón del adaptador , podemos resolver el nombre de la colisión (podemos unir la API pública) que ha ocurrido. Simplemente dicho, el adaptador ayuda a dos interfaces incompatibles a trabajar juntas. Además, el adaptador funciona definiendo una nueva interfaz de adaptador, cuyas clases intentan unir su implementación API.

Este no es un lenguaje concreto. Es un enfoque genérico, cualquier palabra clave que esté allí para representarla puede ser de cualquier tipo, en un lenguaje como C # puede reemplazarlo con genéricos ( <T>).

Voy a suponer que en este momento puede tener dos clases responsables de la compresión y el cifrado.

class Compression
{
    Compress(data : any) : any { ... }
}

class Encryption
{
    Encrypt(data : any) : any { ... }
}

En un mundo empresarial, es muy probable que incluso estas clases específicas sean reemplazadas por interfaces, como la classpalabra clave sería reemplazada por interface(si se trata de lenguajes como C #, Java y / o PHP) o la classpalabra clave permanecería, pero Compressy los Encryptmétodos se definirían como un virtual puro , si se codifica en C ++.

Para hacer un adaptador, definimos una interfaz común.

interface DataProcessing
{
    Process(data : any) : any;
}

Luego tenemos que proporcionar implementaciones de la interfaz para que sea ú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)
        );
    }
}

Al hacer esto, terminas con 4 clases, cada una haciendo algo completamente diferente, pero cada una de ellas brinda la misma API pública. El Processmetodo.

En su lógica de negocios, donde se ocupa de la decisión ninguno / cifrado / compresión / ambos, diseñará su objeto para que dependa de la DataProcessinginterfaz que diseñamos anteriormente.

class DataService
{
    private dataProcessing : DataProcessing;

    public DataService(dataProcessing : DataProcessing)
    {
        this.dataProcessing = dataProcessing;
    }
}

El proceso en sí podría ser tan simple como esto:

public ComplicatedProcess(data : any) : any
{
    data = this.dataProcessing.Process(data);

    // ... perhaps work with the data

    return data;
}

No más condicionales. La clase DataServiceno tiene idea de lo que realmente se hará con los datos cuando se pasan al dataProcessingmiembro, y realmente no le importa, no es su responsabilidad.

Idealmente, usted tendría pruebas unitarias que prueben las 4 clases de adaptadores que creó para asegurarse de que funcionen, usted hará pasar su prueba. Y si pasan, puede estar bastante seguro de que funcionarán sin importar dónde los llame en su código.

Entonces, al hacerlo de esta manera, ¿ya nunca tendré ifs en mi código?

No. Es menos probable que tenga condicionales en su lógica de negocios, pero aún así tienen que estar en algún lugar. El lugar son tus fábricas.

Y esto está bien. Separa las preocupaciones de la creación y de hecho usa el código. Si hace que sus fábricas sean confiables (en Java, incluso podría ir tan lejos como usar algo como el marco Guice de Google), en su lógica comercial no le preocupa elegir la clase correcta para inyectarse. Porque sabe que sus fábricas funcionan y entregarán lo que se le pide.

¿Es necesario tener todas estas clases, interfaces, etc.?

Esto nos lleva de vuelta al comienzo.

En OOP, si elige el camino para usar el polimorfismo, realmente quiere usar patrones de diseño, quiere explotar las características del lenguaje y / o quiere seguir que todo es una ideología de objeto, entonces lo es. Y aun así, este ejemplo no incluso mostrar todas las fábricas que van a necesitar y si se va a refactorizar los Compressiony las Encryptionclases y hacer que las interfaces en su lugar, usted tiene que incluir sus implementaciones también.

Al final, terminas con cientos de pequeñas clases e interfaces, centradas en cosas muy específicas. Lo cual no es necesariamente malo, pero podría no ser la mejor solución para usted si todo lo que desea es hacer algo tan simple como sumar dos números.

Si desea hacerlo de manera rápida, puede tomar la solución de Ixrec , que al menos logró eliminar los bloques else ify else, que, en mi opinión, son incluso un poco peores que una simple if.

Tenga en cuenta que esta es mi forma de hacer un buen diseño de OO. Codificación para interfaces en lugar de implementaciones, así es como lo he hecho durante los últimos años y es el enfoque con el que me siento más cómodo.

Personalmente, me gusta más la programación if-less y agradecería mucho más la solución más larga en las 5 líneas de código. Es la forma en que estoy acostumbrado a diseñar código y me siento muy cómodo al leerlo.


Actualización 2: ha habido una discusión salvaje sobre la primera versión de mi solución. Discusión principalmente causada por mí, por lo cual me disculpo.

Decidí editar la respuesta de una manera que es una de las formas de ver la solución, pero no la única. También eliminé la parte del decorador, donde me refería a la fachada, que al final decidí dejar por completo, porque un adaptador es una variación de la fachada.

Andy
fuente
28
No voté en contra, pero la razón podría ser la cantidad ridícula de nuevas clases / interfaces para hacer algo que el código original hizo en 8 líneas (y la otra respuesta hizo en 5). En mi opinión, lo único que logra es aumentar la curva de aprendizaje para el código.
Maurycy
66
@Maurycy Lo que OP solicitó fue tratar de encontrar una solución a su problema utilizando patrones de diseño comunes, si existe tal solución. ¿Mi solución es más larga que su código o el de Ixrec? Es. Lo admito. ¿Mi solución resuelve su problema usando patrones de diseño y, por lo tanto, responde a su pregunta y también elimina de manera efectiva todos los ifs necesarios del proceso? Lo hace. Ixrec no lo hace.
Andy
26
Creo que escribir código que sea claro, confiable, conciso, eficiente y fácil de mantener es el camino a seguir. Si tuviera un dólar por cada vez que alguien citara SOLID o citara un patrón de software sin articular claramente sus objetivos y sus razones, sería un hombre rico.
Robert Harvey
12
Creo que tengo dos problemas que veo aquí. Primero, las interfaces Compressiony Encryptionparecen totalmente superfluas. No estoy seguro de si estás sugiriendo que de alguna manera son necesarios para el proceso de decoración, o simplemente implicando que representan conceptos extraídos. El segundo problema es que hacer una clase como CompressionEncryptionDecoratorlleva al mismo tipo de explosión combinatoria que los condicionales del OP. Tampoco veo el patrón decorador con suficiente claridad en el código sugerido.
cbojar el
55
El debate sobre SOLID frente a simple está un poco perdido: este código no es ninguno de los dos y tampoco utiliza el patrón decorador. El código no es SOLIDO automáticamente solo porque usa un montón de interfaces. La inyección de dependencia de una interfaz de procesamiento de datos es bastante agradable; todo lo demás es superfluo. SOLID es una preocupación a nivel de arquitectura dirigida a manejar bien el cambio. OP no proporcionó información sobre su arquitectura ni cómo espera que cambie su código, por lo que ni siquiera podemos hablar de SOLID en una respuesta.
Carl Leth el
120

El único problema que veo con su código actual es el riesgo de explosión combinatoria a medida que agrega más configuraciones, que pueden mitigarse fácilmente estructurando el código más de esta manera:

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

No conozco ningún "patrón de diseño" o "modismo" del que esto pueda considerarse un ejemplo.

Ixrec
fuente
18
@DamithGanegoda No, si lees mi código cuidadosamente, verás que hace exactamente lo mismo en ese caso. Es por eso que no hay elseentre mis dos declaraciones if, y por qué estoy asignando datacada vez. Si ambas marcas son verdaderas, comprimir () se ejecuta, y encriptar () se ejecuta en el resultado de comprimir (), tal como lo desea.
Ixrec
14
@DavidPacker Técnicamente, también lo hace cada instrucción if en cada lenguaje de programación. Fui por simplicidad, ya que esto parecía un problema donde una respuesta muy simple era apropiada. Su solución también es válida, pero personalmente la guardaría para cuando tenga que preocuparme por más de dos banderas booleanas.
Ixrec
15
@DavidPacker: correcto no está definido por qué tan bien el código se adhiere a alguna directriz de algún autor sobre alguna ideología de programación. Correcto es "hace el código lo que se supone que debe hacer y se implementó en un período de tiempo razonable". Si tiene sentido hacerlo de la "manera incorrecta", entonces la manera incorrecta es la correcta porque el tiempo es dinero.
cuál es el
9
@DavidPacker: Si estaba en la posición del OP y formulaba esa pregunta, el comentario de Lightness Race in Orbit es lo que realmente necesito. "Encontrar una solución usando patrones de diseño" ya está comenzando con el pie equivocado.
cuál es el
66
@DavidPacker En realidad, si lees la pregunta más de cerca, no insiste en un patrón. Dice: "Estoy pensando en el patrón Decorador. ¿Es la elección correcta, o tal vez hay una mejor alternativa?" . Abordaste la primera oración de mi cita, pero no la segunda. Otras personas adoptaron el enfoque de que no, no es la elección correcta. No puedes afirmar que solo el tuyo responde la pregunta.
Jon Bentley
12

Supongo que su pregunta no busca practicidad, en cuyo caso la respuesta de lxrec es la correcta, sino aprender sobre patrones de diseño.

Obviamente, el patrón de comando es una exageración para un problema tan trivial como el que propones, pero a modo de ilustración aquí va:

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 puede ver, poner los comandos / transformación en una lista permite ejecutarlos secuencialmente. Obviamente, ejecutará ambos, o solo uno de ellos dependerá de lo que ponga en la lista sin condiciones.

Obviamente, los condicionales terminarán en algún tipo de fábrica que reúne la lista de comandos.

EDITAR para el comentario de @ texacre:

Hay muchas formas de evitar las condiciones if en la parte de creación de la solución, tomemos por ejemplo una aplicación GUI de escritorio . Puede tener casillas de verificación para las opciones de compresión y cifrado. En el on cliccaso de esas casillas de verificación, crea una instancia del comando correspondiente y lo agrega a la lista, o lo elimina de la lista si está deseleccionando la opción.

Tulains Córdova
fuente
A menos que pueda proporcionar un ejemplo de "algún tipo de fábrica que reúna la lista de comandos" sin un código que esencialmente se parece a la respuesta de Ixrec, entonces, en mi opinión, esto no responde la pregunta. Esto proporciona una mejor manera de implementar las funciones de compresión y cifrado, pero no cómo evitar las banderas.
thexacre
@thexacre agregué un ejemplo.
Tulains Córdova
Entonces, ¿en su casilla de verificación de escucha de eventos tiene "if checkbox.ticked entonces add command"? Me parece que solo estás arrastrando la bandera si hay declaraciones alrededor ...
thexacre
@thexacre No, un oyente para cada casilla de verificación. En el evento click solo commands.add(new EncryptCommand()); o commands.add(new CompressCommand());respectivamente.
Tulains Córdova
¿Qué pasa con el manejo de desmarcar la caja? En casi todos los juegos de herramientas de idioma / UI que he encontrado, aún tendrá que marcar el estado de la casilla de verificación en el detector de eventos. Estoy de acuerdo en que este es un patrón mejor, pero no evita tener que hacerlo básicamente si flag hace algo en alguna parte.
thexacre
7

Creo que los "patrones de diseño" están innecesariamente orientados hacia los "patrones oo" y evitan completamente ideas mucho más simples. De lo que estamos hablando aquí es de una tubería de datos (simple).

Intentaría hacerlo en clojure. Cualquier otro lenguaje donde las funciones sean de primera clase probablemente también esté bien. Tal vez podría hacer un ejemplo de C # más adelante, pero no es tan bueno. Mi forma de resolver esto sería los siguientes pasos con algunas explicaciones para los no clojurianos:

1. Representar un conjunto de transformaciones.

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

Este es un mapa, es decir, una tabla de búsqueda / diccionario / lo que sea, desde palabras clave hasta funciones. Otro ejemplo (palabras clave para cadenas):

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

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

Entonces, escribir (transformations :encrypt)o (:encrypt transformations)devolvería la función de cifrado. ( (fn [data] ... )es solo una función lambda).

2. Obtenga las opciones como una secuencia de palabras clave:

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

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

3. Filtre todas las transformaciones utilizando las opciones proporcionadas.

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

Ejemplo:

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

4. Combina funciones en una:

(apply comp transformations-to-run)

Ejemplo:

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

5. Y luego 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])

Los ÚNICOS cambios si queremos agregar una nueva función, digamos "debug-print", son los siguientes:

(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
NiklasJ
fuente
¿Cómo se rellenan los funcs para incluir solo las funciones que deben aplicarse sin utilizar esencialmente una serie de sentencias if de una forma u otra?
thexacre
La fila funcs-to-run-here (map options funcs)está haciendo el filtrado, eligiendo así un conjunto de funciones para aplicar. Tal vez debería actualizar la respuesta y entrar un poco más en detalles.
NiklasJ
5

[Esencialmente, mi respuesta es una continuación de la respuesta de @Ixrec anterior . ]

Una pregunta importante: ¿va a crecer el número de combinaciones distintas que necesita cubrir? Conoces mejor tu dominio de materia. Este es tu juicio para hacer.
¿Puede crecer el número de variantes? Bueno, eso no es inconcebible. Por ejemplo, es posible que deba acomodar más algoritmos de cifrado diferentes.

Si anticipa que el número de combinaciones distintas va a crecer, el patrón de Estrategia puede ayudarlo. Está diseñado para encapsular algoritmos y proporcionar una interfaz intercambiable para el código de llamada. Aún tendrá una pequeña cantidad de lógica cuando cree (cree una instancia) la estrategia adecuada para cada cadena en particular.

Ha comentado anteriormente que no espera que los requisitos cambien. Si no espera que aumente el número de variantes (o si puede diferir esta refactorización), mantenga la lógica tal como está. Actualmente, tiene una pequeña y manejable cantidad de lógica. (Quizás se ponga una nota en los comentarios sobre una posible refactorización de un patrón de Estrategia).

Nick Alexeev
fuente
1

Una forma de hacer esto en scala sería:

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 el patrón de decorador para lograr los objetivos anteriores (separación de la lógica de procesamiento individual y cómo se conectan entre sí) sería demasiado detallado.

En el caso de que necesite un patrón de diseño para lograr estos objetivos de diseño en un paradigma de programación OO, el lenguaje funcional ofrece soporte nativo al usar funciones como ciudadanos de primera clase (líneas 1 y 2 en el código) y composición funcional (línea 3)

Sachin K
fuente
¿Por qué es esto mejor (o peor) que el enfoque del OP? ¿Y / o qué piensa sobre la idea del OP de usar un patrón de decorador?
Kasper van den Berg
este fragmento de código es mejor y es explícito sobre el pedido (compresión antes del cifrado); evita interfaces no deseadas
Rag