it-swarm-es.com

¿Qué debo tener en cuenta cuando los principios DRY y KISS) son incompatibles?

El principio DRY a veces obliga a los programadores a escribir funciones/clases complejas y difíciles de mantener. Un código como este tiende a volverse más complejo y difícil de mantener con el tiempo. Violar el principio KISS .

Por ejemplo, cuando varias funciones necesitan hacer algo similar. La solución habitual DRY) es escribir una función que tome diferentes parámetros para permitir ligeras variaciones en el uso.

La ventaja es obvia, DRY = un lugar para hacer cambios, etc.

La desventaja y la razón por la que está violando KISS es porque funciones como estas tienden a volverse más y más complejas con más y más parámetros con el tiempo. Al final, los programadores tendrán mucho miedo de realice cambios en dichas funciones o causarán errores en otros casos de uso de la función.

Personalmente, creo que tiene sentido violar el principio DRY para que siga el principio KISS.

Prefiero tener 10 funciones súper simples que son similares a tener una función súper compleja.

Prefiero hacer algo tedioso, pero fácil (hacer el mismo cambio o un cambio similar en 10 lugares), que hacer un cambio muy aterrador/difícil en un solo lugar.

Obviamente, la forma ideal es hacerlo como KISS como sea posible sin violar DRY. Pero a veces parece imposible.

Una pregunta que surge es "¿con qué frecuencia cambia este código?" lo que implica que si cambia con frecuencia, entonces es más relevante hacerlo SECO. No estoy de acuerdo, porque cambiar este complejo DRY función a menudo hará que crezca en complejidad y empeore aún más con el tiempo.

Básicamente, creo que, en general, KISS> DRY.

¿Qué piensas? ¿En qué casos crees que DRY siempre debería ganarse a KISS y viceversa? ¿Qué cosas consideras al tomar la decisión? ¿Cómo evitas la situación?

71
user158443

KISS es subjetivo. DRY es fácil de aplicar en exceso. Ambos tienen buenas ideas detrás de ellos, pero ambos son fáciles de abusar. La clave es el equilibrio.

KISS está realmente en el ojo de tu equipo. No sabes qué es KISS. Tu equipo sí. Muéstrales tu trabajo y ve si piensan que es simple. Eres un mal juez de esto porque ya sabes cómo funciona. Descubra qué tan difícil es su código para que otros lo lean.

DRY no se trata de cómo se ve tu código. No puede detectar problemas reales DRY buscando un código idéntico. Un problema real DRY) podría ser que está resolviendo el mismo problema con un aspecto completamente diferente código en un lugar diferente. No infringe DRY cuando usa un código idéntico para resolver un problema diferente en un lugar diferente. ¿Por qué? Porque diferentes problemas pueden cambiar de forma independiente. Ahora uno necesita cambiar y el otro no.

Tome decisiones de diseño en un solo lugar. No difunda una decisión. Pero no doble todas las decisiones que parezcan iguales ahora mismo en el mismo lugar. Está bien tener ambas xey incluso cuando ambas están configuradas en 1.

Con esta perspectiva, nunca pongo KISS o DRY sobre el otro. No veo casi la tensión entre ellos. Me protejo contra el abuso de Cualquiera de estos dos principios importantes, pero tampoco es una bala de plata.

144
candied_orange

Escribí sobre esto ya en n comentario a otra respuesta por candied_orange a a pregunta similar y también lo toqué en un respuesta diferente , pero vale la pena repetir:

DRY es un lindo acrónimo de tres letras para un mnemotécnico "Don't Repeat Yourself", que fue acuñado en el libro The Pragmatic Programmer , donde es un - sección completa de 8.5 páginas . También tiene un explicación y discusión de varias páginas en la wiki .

La definición en el libro es la siguiente:

Cada conocimiento debe tener una representación única, inequívoca y autorizada dentro de un sistema.

Tenga en cuenta que es enfáticamente ¡no sobre eliminar la duplicación. Se trata de ¡identificación cuál de los duplicados es el canónico. Por ejemplo, si tiene una memoria caché, la memoria caché contendrá valores que son duplicados de otra cosa. ¡Sin embargo, debe quedar muy claro que el caché es ¡no la fuente canónica.

El principio es ¡no las tres letras DRY. Son esas 20 páginas más o menos en el libro y la wiki.

El principio también está estrechamente relacionado con OAOO, que es un acrónimo de cuatro letras no tan lindo para "Once And Only Once", que a su vez es un principio en eXtreme Programming que tiene un explicación y discusión de varias páginas en la wiki .

La página wiki de OAOO tiene una cita muy interesante de Ron Jeffries:

Una vez vi a Beck declarar dos parches de código casi completamente diferente como "duplicación", cambiarlos para que fueran duplicación, y luego eliminar la duplicación recién insertada para obtener algo obviamente mejor.

Sobre lo cual él elabora:

Recuerdo una vez que vi a Beck mirar dos bucles que eran bastante diferentes: tenían estructuras diferentes y contenidos diferentes, que prácticamente no estaban duplicados, excepto la palabra "for", y el hecho de que estaban girando, de manera diferente, sobre el mismo colección.

Cambió el segundo bucle a bucle de la misma manera que el primero. Esto requirió cambiar el cuerpo del bucle para omitir los elementos hacia el final de la colección, ya que la versión anterior solo hacía el frente de la colección. Ahora las declaraciones for eran las mismas. "Bueno, tengo que eliminar esa duplicación, dijo, y movió el segundo cuerpo al primer bucle y eliminó el segundo bucle por completo.

Ahora tenía dos tipos de procesamiento similar en un solo ciclo. Encontró algún tipo de duplicación allí, extrajo un método, hizo un par de otras cosas, ¡y listo! El código era mucho mejor.

Ese primer paso, crear duplicación, fue sorprendente.

Esto muestra: ¡puede tener duplicación sin código duplicado!

Y el libro muestra la otra cara de la moneda:

Como parte de su solicitud de pedido de vino en línea, está capturando y validando la edad de su usuario, junto con la cantidad que está ordenando. Según el propietario del sitio, ambos deben ser números y ambos mayores que cero. Entonces codifica las validaciones:

def validate_age(value):
 validate_type(value, :integer)
 validate_min_integer(value, 0)

def validate_quantity(value):
 validate_type(value, :integer)
 validate_min_integer(value, 0)

Durante la revisión del código, el sabelotodo residente devuelve este código, alegando que es una violación DRY: ambos cuerpos de funciones son iguales.

Están equivocados. El código es el mismo, pero el conocimiento que representan es diferente. Las dos funciones validan dos cosas separadas que tienen las mismas reglas. Eso es una coincidencia, no una duplicación.

Este es un código duplicado que no es una duplicación de conocimiento.

Hay una gran anécdota sobre la duplicación que conduce a una profunda comprensión de la naturaleza de los lenguajes de programación: muchos programadores conocen el lenguaje de programación ¡Esquema y que es un lenguaje de procedimiento en la familia LISP con el primer- procedimientos de clase y de orden superior, alcance léxico, cierres léxicos y un enfoque en estructuras de datos y códigos puramente funcionales y referencialmente transparentes. Lo que, sin embargo, no mucha gente sabe, es que se creó para estudiar la programación orientada a objetos y los sistemas de actores (que los autores de Scheme consideraron estrechamente relacionados, si no lo mismo).

Dos de los procedimientos fundamentales en Scheme son lambda, que crea un procedimiento, y apply, que ejecuta un procedimiento. Los creadores de Scheme agregaron dos más: alpha, que crea un a ctor (u objeto), y send , que envía un mensaje a un actor (u objeto).

Una consecuencia molesta de tener tanto apply como send fue que la elegante sintaxis para las llamadas a procedimientos ya no funcionaba. En Scheme tal como lo conocemos hoy (y en casi cualquier LISP), una lista simple generalmente se interpreta como "interpretar el primer elemento de la lista como un procedimiento y apply al resto de la lista, interpretada como argumentos ". Entonces puedes escribir

(+ 2 3)

y eso es equivalente a

(apply '+ '(2 3))

(O algo cercano, mi esquema está bastante oxidado).

Sin embargo, esto ya no funciona, ya que no sabe si apply o send (suponiendo que no desea priorizar uno de los dos que no hicieron los creadores de Scheme 't, querían que ambos paradigmas fueran iguales). ... ¿O tú? Los creadores de Scheme se dieron cuenta de que en realidad, simplemente necesitan verificar el tipo de objeto al que hace referencia el símbolo: if + es un procedimiento, usted apply it, si + es un actor, usted send un mensaje para él. En realidad, no necesita separar apply y send, puede tener algo como apply-or-send.

Y eso es lo que hicieron: tomaron el código de los dos procedimientos apply y send y los pusieron en el mismo procedimiento, como dos ramas de un condicional.

Poco después, también reescribieron el intérprete de Scheme, que hasta ese momento estaba escrito en un lenguaje ensamblador de transferencia de registros de muy bajo nivel para una máquina de registro, en Scheme de alto nivel. Y notaron algo sorprendente: el código en las dos ramas del condicional ¡se volvió idéntico. No se habían dado cuenta de esto antes: los dos procedimientos se escribieron en momentos diferentes (comenzaron con un "LISP mínimo" y luego agregaron OO), y la verbosidad y el bajo nivel de la Asamblea significaba que en realidad estaban escritos de manera bastante diferente, pero después de reescribirlos en un lenguaje de alto nivel, quedó claro que hicieron lo mismo.

Esto condujo a una comprensión profunda de los actores y OO: ejecutar un programa orientado a objetos y ejecutar un programa en un lenguaje de procedimiento con cierres léxicos y llamadas de cola adecuadas, ¡son lo mismo. La única diferencia es si las primitivas de su lenguaje son objetos/actores o procedimientos. Pero ¡operacionalmente, es lo mismo.

Esto también lleva a otra realización importante que desafortunadamente aún no se comprende bien hoy en día: no se puede mantener la abstracción orientada a objetos sin llamadas de cola adecuadas, o ponerlo de manera más agresiva: un lenguaje que dice estar orientado a objetos pero no tiene llamadas de cola adecuadas , ¡no lo es orientado a objetos. (Desafortunadamente, eso se aplica a todos mis idiomas favoritos, y no es académico: I have me topé con este problema, que tuve que romper la encapsulación para evitar un desbordamiento de pila.)

Este es un ejemplo donde la duplicación muy bien oculta en realidad ¡oscurecida un conocimiento importante, y descubriendo esta duplicación también reveló conocimiento.

39
Jörg W Mittag

En caso de duda, elija siempre la solución más simple posible que resuelva el problema.

Si resulta que la solución simple era demasiado simple, se puede cambiar fácilmente. Por otro lado, una solución demasiado compleja también es más difícil y arriesgada de cambiar.

KISS es realmente el más importante de todos los principios de diseño, pero a menudo se lo pasa por alto, porque nuestra cultura de desarrollador valora mucho ser inteligente y utilizar técnicas sofisticadas. Pero a veces un if realmente es mejor que un patrón de estrategia .

El principio DRY) a veces obliga a los programadores a escribir funciones/clases complejas y difíciles de mantener.

¡Alto ahí! El propósito del principio DRY) es obtener un código más fácil de mantener. Si aplicar el principio en un caso particular conduciría a menos código mantenible, entonces el principio no debe aplicarse.

Tenga en cuenta que ninguno de estos principios son objetivos en sí mismos. El objetivo es hacer un software que cumpla su propósito y que pueda modificarse, adaptarse y ampliarse cuando sea necesario. Tanto KISS, DRY, SOLID como todos los demás principios son significa para lograr este objetivo. Pero todos tienen sus limitaciones y se pueden aplicar de forma tal que funcionen en contra del objetivo final, que es escribir software que funcione y se pueda mantener.

8
JacquesB

En mi humilde opinión: si deja de enfocarse en que el código sea KISS/DRY y comienza a enfocarse en los requisitos que impulsan el código, encontrará la mejor respuesta que está buscando.

Yo creo:

  1. Necesitamos alentarnos unos a otros para seguir siendo pragmáticos (como lo estás haciendo)

  2. Nunca debemos dejar de promover la importancia de las pruebas.

  3. Centrarse en los requisitos más resolverá sus preguntas.

TLDR

Si su requisito es que las partes cambien de forma independiente, mantenga las funciones independientes al no tener funciones auxiliares. Si su requerimiento (y cualquier cambio futuro al mismo) es el mismo para todas las funciones, mueva esa lógica a una función auxiliar.

Creo que todas nuestras respuestas hasta ahora forman un diagrama de Venn: todos decimos lo mismo, pero damos detalles a diferentes partes.

Además, nadie más mencionó las pruebas, que es en parte por qué escribí esta respuesta. ¡Creo que si alguien menciona que los programadores tienen miedo de hacer cambios, entonces es muy imprudente no hablar sobre las pruebas! Incluso si "pensamos" que el problema está relacionado con el código, podría ser el verdadero problema la falta de pruebas. Las decisiones objetivamente superiores se vuelven más realistas cuando las personas han invertido primero en pruebas automatizadas.

Primero, evitar el miedo es sabiduría. ¡Buen trabajo!

Aquí hay una oración que usted dijo: los programadores tendrán mucho miedo de hacer cambios a tales funciones [auxiliares] o causarán errores en otros casos de uso de la función

Estoy de acuerdo en que este miedo es el enemigo, y debes nunca aferrarte a los principios si solo están causando temor a errores/trabajo/cambios en cascada. Si copiar/pegar entre múltiples funciones es la forma única de eliminar este miedo (que no creo que sea, ver más abajo), entonces eso es lo que debes hacer.

El hecho de que sientas este miedo a hacer cambios y que estés tratando de hacer algo al respecto, te convierte en un mejor profesional que muchos otros a quienes no les importa lo suficiente como para mejorar el código: simplemente hacen lo que se les dice. y hacer los cambios mínimos necesarios para cerrar su boleto.

Además (y puedo decir que estoy repitiendo lo que ya sabes): las habilidades de las personas triunfan habilidades de diseño. Si las personas de la vida real en su empresa son totalmente malas, entonces no importa si su "teoría" es mejor. Es posible que tenga que tomar decisiones objetivamente peores, pero sabe que las personas que lo mantendrán son capaces de comprender y trabajar con ellas. Además, muchos de nosotros también entendemos a la gerencia que (IMO) nos microgestúa y encontramos formas de negar siempre la refactorización necesaria.

Como alguien que es un proveedor que escribe código para clientes, tengo que pensar en esto todo el tiempo. Es posible que desee utilizar el curry y la metaprogramación porque hay un argumento de que es objetivamente mejor, pero en la vida real, veo que la gente está confundida por ese código porque no es visualmente obvio lo que está pasando.

Segundo, mejores pruebas resuelven múltiples problemas a la vez

Si (y solo si) tiene pruebas automáticas efectivas, estables y probadas en el tiempo (unidad y/o integración), entonces apuesto a que verá que el miedo se desvanece. Para los recién llegados a las pruebas automatizadas, puede ser muy aterrador confiar en las pruebas automatizadas; los recién llegados pueden ver todos esos puntos verdes y tienen muy poca confianza en que esos puntos verdes reflejen el funcionamiento de la producción en la vida real. Sin embargo, si usted, personalmente, tiene confianza en las pruebas automatizadas, entonces puede comenzar a alentar emocionalmente/relacionalmente a otros para que también confíen en él.

Para usted, (si aún no lo ha hecho), el primer paso es investigar las prácticas de prueba si no lo ha hecho. Sinceramente, supongo que ya sabes estas cosas, pero como no vi esto mencionado en tu publicación original, tengo que hablar de ello. Debido a que las pruebas automatizadas son esto es importante y relevante para su situación que planteó.

No voy a tratar de resumir todas las prácticas de prueba en una sola publicación aquí, pero te desafío a que te concentres en la idea de las pruebas "a prueba de refactorización". Antes de enviar una prueba de unidad/integración al código, pregúntese si hay alguna forma válida de refactorizar el CUT (código bajo prueba) que rompería la prueba que acaba de escribir. Si eso es cierto, entonces (IMO) elimine esa prueba. Es mejor tener menos pruebas automatizadas que no se rompan innecesariamente cuando refactoriza, que tener algo que le diga que tiene una alta cobertura de prueba (calidad sobre cantidad). Después de todo, facilitar la refactorización es (IMO) el propósito principal de las pruebas automatizadas.

A medida que he adoptado esta filosofía "a prueba de refactores" a lo largo del tiempo, he llegado a las siguientes conclusiones:

  1. Las pruebas de integración automatizadas son mejores que las pruebas unitarias.
  2. Para las pruebas de integración, si es necesario, escriba "simuladores/falsificaciones" con "pruebas de contrato"
  3. Nunca pruebe una API privada, ya sean métodos de clase privada o funciones no exportadas de un archivo.

Referencias

Mientras investiga prácticas de prueba, es posible que tenga que dedicar más tiempo para escribir esas pruebas usted mismo. A veces, el único mejor enfoque es no decirle a nadie que estás haciendo eso, porque te microgestión. Obviamente esto no siempre es posible porque la cantidad de necesidad de pruebas puede ser mayor que la necesidad de un buen equilibrio trabajo/vida. Pero, a veces, hay cosas lo suficientemente pequeñas como para que pueda salirse con la suya retrasando en secreto una tarea por un día o dos para escribir las pruebas/códigos necesarios. Esto, lo sé, puede ser una declaración controvertida, pero creo que es la realidad.

Además, obviamente puede ser tan prudentemente político como sea posible para ayudar a alentar a otros a tomar medidas para comprender y escribir las pruebas ellos mismos. O tal vez usted es el líder tecnológico que puede imponer una nueva regla para las revisiones de código.

Mientras habla sobre las pruebas con sus colegas, con suerte el punto 1 anterior (sea pragmático) nos recuerda a todos que debemos seguir escuchando primero y no ser agresivos.

Tercero, enfóquese en los requisitos, no en el código

¡Muchas veces nos enfocamos en nuestro código, y no entendemos profundamente la imagen más grande que se supone que nuestro código debe resolver! A veces, debes dejar de discutir si el código está limpio y comenzar a asegurarte de que comprendes bien los requisitos que se supone que conducen el código.

Es más importante que haga lo correcto que sentir que su código es "bonito" según ideas como KISS/DRY. Es por eso que dudo en preocuparme por esas frases clave, porque (en la práctica) accidentalmente te hacen concentrarte en tu código sin pensar en el hecho de que los requisitos son los que proporcionan un buen juicio de buena calidad de código.


Si los requisitos de dos funciones son interdependientes/iguales, coloque la lógica de implementación de ese requisito en una función auxiliar. Las entradas a esa función auxiliar serán las entradas a la lógica de negocios para ese requisito.

Si los requisitos de las funciones son diferentes, copie/pegue entre ellos. Si ambos tienen el mismo código en este momento, pero podría cambiar legítimamente de forma independiente, entonces una función auxiliar es malo porque está afectando a otra función cuyo requisito es cambiar de forma independiente.

Ejemplo 1: tiene una función llamada "getReportForCustomerX" y "getReportForCustomerY", y ambos consultan la base de datos de la misma manera. Supongamos también que existe un requisito comercial en el que cada cliente puede personalizar su informe literalmente de la forma que desee. En este caso, por diseño , los clientes quieren diferentes números en su informe. Entonces, si tiene un nuevo cliente Z que necesita un informe, puede ser mejor copiar/pegar la consulta de otro cliente, y luego confirmar el código y mover uno. Incluso si las consultas son exactamente iguales, el punto de definición de esas funciones es separar cambios de un cliente impactando a otro. En los casos en que proporcione una nueva función que todos los clientes desearán en su informe, entonces sí: posiblemente escriba los mismos cambios entre todas las funciones.

Sin embargo, supongamos que decidimos seguir adelante y crear una función auxiliar llamada queryData. La razón que es mala es porque habrá más cambios en cascada introduciendo una función auxiliar. Si hay una cláusula "dónde" en su consulta que es la misma para todos los clientes, tan pronto como un cliente quiera que un campo sea diferente para ellos, en lugar de 1) cambiar la consulta dentro de la función X, debe 1 ) cambie la consulta para hacer lo que el cliente X quiere 2) agregue condicionales en la consulta para no hacer eso por otros. Agregar más condicionales a una consulta es lógicamente diferente. Es posible que sepa cómo agregar una subcláusula en una consulta, pero eso no significa que sepa cómo hacer que esa subcláusula sea condicional sin afectar el rendimiento de aquellos que no la usan.

Entonces notará que usar una función auxiliar requiere dos cambios en lugar de uno. Sé que este es un ejemplo artificial, pero la complejidad booleana para mantener crece más que linealmente, en mi experiencia. Por lo tanto, el acto de agregar condicionales cuenta como "una cosa más" que las personas deben preocuparse y "una cosa más" que actualizar cada vez.

Me parece que este ejemplo podría ser como la situación en la que te encuentras. Algunas personas se estremecen emocionalmente ante la idea de copiar/pegar entre estas funciones, y esa reacción emocional está bien. Pero el principio de "minimizar los cambios en cascada" discernirá objetivamente las excepciones para cuando copiar/pegar está bien.

Ejemplo 2: tiene tres clientes diferentes, pero lo único que permite que sean diferentes entre sus informes son los títulos de las columnas. Tenga en cuenta que esta situación es muy diferente. Nuestro requisito comercial ya no es "proporcionar valor al cliente al permitir flexibilidad competitiva en el informe". En cambio, el requisito comercial es "evitar el trabajo excesivo al no permitir que los clientes personalicen mucho el informe". En esta situación, la única vez que cambiaría la lógica de consulta es cuando también tendrá que asegurarse de que todos los demás clientes obtengan el mismo cambio. En este caso, definitivamente desea crear una función auxiliar con una matriz como entrada: cuáles son los "títulos" para las columnas.

En el futuro, si los propietarios de los productos deciden que quieren permitir que los clientes personalicen algo sobre la consulta, entonces agregarán más indicadores a la función auxiliar.

Conclusión

Cuanto más se centre en los requisitos en lugar del código, más será el código isomorfo a los requisitos literales. Usted naturalmente escribe un código mejor.

4
Alexander Bird

Intenta encontrar un término medio razonable. En lugar de una función con una gran cantidad de parámetros y condicionales complejos dispersos, divídala en algunas funciones más simples. Habrá alguna repetición en las personas que llaman, pero no tanto como si no hubiera movido el código común a las funciones en primer lugar.

Recientemente me encontré con esto con un código en el que estoy trabajando para interactuar con las tiendas de aplicaciones de Google e iTunes. Gran parte del flujo general es el mismo, pero hay suficientes diferencias que no podría escribir fácilmente una función para encapsular todo.

Entonces el código está estructurado como:

Google::validate_receipt(...)
    f1(...)
    f2(...)
    some google-specific code
    f3(...)

iTunes::validate_receipt(...)
    some iTunes-specific code
    f1(...)
    f2(...)
    more iTunes-specific code
    f3(...)

No me preocupa demasiado que llamar a f1 () y f2 () en ambas funciones de validación viola el principio DRY), porque combinarlas lo haría más complicado y no realizaría un solo, bien definido tarea.

3
Barmar

Kent Beck adoptó 4 reglas de diseño simple, que se relacionan con esta pregunta. Según lo expresado por Martin Fowler, son:

  • Pasa las pruebas
  • Revela intención
  • Sin duplicación
  • Pocos elementos

Hay mucha discusión sobre el orden de los dos medios, por lo que puede valer la pena considerarlos como igualmente importantes.

DRY es el tercer elemento en la lista, y KISS podría considerarse una combinación de la 2da y 4ta, o incluso la lista completa en conjunto.

Esta lista proporciona una vista alternativa a la dicotomía DRY/KISS. ¿Su código DRY revela intención? ¿Su código KISS? ¿Puede hacer que la versión de ether sea más reveladora o menos duplicada?

El objetivo no es DRY o KISS, es un buen código. DRY, KISS, y estas reglas son meras herramientas para llegar allí.

3
Blaise Pascal