Refactorización y principio abierto / cerrado


12

Recientemente he estado leyendo un sitio web sobre el desarrollo de código limpio (no pongo un enlace aquí porque no está en inglés).

Uno de los principios anunciados por este sitio es el Principio Abierto Cerrado : cada componente de software debe estar abierto para la extensión y cerrado para la modificación. Por ejemplo, cuando hemos implementado y probado una clase, solo debemos modificarla para corregir errores o agregar nuevas funcionalidades (por ejemplo, nuevos métodos que no influyan en los existentes). La funcionalidad y la implementación existentes no deben modificarse.

Normalmente aplico este principio definiendo una interfaz Iy una clase de implementación correspondiente A. Cuando la clase se Aha estabilizado (implementado y probado), normalmente no lo modifico demasiado (posiblemente, para nada), es decir

  1. Si llegan nuevos requisitos (por ejemplo, rendimiento o una implementación totalmente nueva de la interfaz) que requieren grandes cambios en el código, escribo una nueva implementación By sigo usando Amientras Bno esté madura. Cuando Bestá maduro, todo lo que se necesita es cambiar cómo Ise instancia.
  2. Si los nuevos requisitos también sugieren un cambio en la interfaz, defino una nueva interfaz I'y una nueva implementación A'. Por lo tanto I, Ase congelan y se mantienen a la aplicación del sistema de producción, siempre y cuando I'y A'no son lo suficientemente estable como para reemplazarlos.

Entonces, en vista de estas observaciones, me sorprendió un poco que la página web sugiriera el uso de refactorizaciones complejas , "... porque no es posible escribir código directamente en su forma final".

¿No hay una contradicción / conflicto entre hacer cumplir el Principio Abierto / Cerrado y sugerir el uso de refactorizaciones complejas como una mejor práctica? ¿O la idea aquí es que uno puede usar refactorizaciones complejas durante el desarrollo de una clase A, pero cuando esa clase se haya probado con éxito, debería congelarse?

Respuestas:


9

Pienso en el principio de Open-Closed como un objetivo de diseño . Si terminas teniendo que violarlo, eso significa que tu diseño inicial falló, lo que ciertamente es posible e incluso probable.

Refactorizar significa que está cambiando el diseño sin cambiar la funcionalidad. Probablemente estás cambiando tu diseño porque hay un problema con él. Quizás el problema es que es difícil seguir el principio de abrir-cerrar al hacer modificaciones al código existente, y está tratando de solucionarlo.

Es posible que esté haciendo una refactorización para que sea posible implementar su próxima función sin violar el OCP cuando lo haga.


Ciertamente no debe pensar como un principio como un objetivo de diseño . Son herramientas: no estás haciendo que el software sea bonito y teóricamente correcto por dentro, estás tratando de generar valor para tu cliente. Es una guía , nada más.
T. Sar

@ T.Sar Un principio es una guía, algo por lo que se esfuerza, están orientados a la mantenibilidad y la escalabilidad. Eso me parece un objetivo de diseño. No puedo ver un principio como herramienta en la forma en que veo un patrón de diseño o un marco como herramienta.
Tulains Córdova

@ TulainsCórdova Mantenimiento, rendimiento, corrección, escalabilidad: esos son los objetivos. El principio abierto-cerrado es un medio para ellos, solo uno de muchos. No es necesario empujar algo hacia el principio de abrir-cerrar si no es aplicable a él o si iría en detrimento de los objetivos reales del proyecto. Usted no vende "Cerrado abierto" a un cliente. Como una mera guía , no es mejor que una regla general que puede descartarse si termina encontrando una manera de hacer lo suyo de una manera más legible y clara. Las pautas son herramientas, después de todo, nada más.
T. Sar

@ T.Sar Hay tantas cosas que no puede vender a un cliente ... Por otro lado, estoy de acuerdo con usted en que no se deben hacer cosas que menoscaben los objetivos del proyecto.
Tulains Córdova

9

El principio Open-Closed es más un indicador de qué tan bien está diseñado su software ; No es un principio a seguir literalmente. También es un principio que ayuda a evitar que cambiemos accidentalmente las interfaces existentes (clases y métodos a los que llama y cómo espera que funcionen).

El objetivo es escribir software de calidad. Una de estas cualidades es la extensibilidad. Esto significa que es fácil agregar, eliminar, cambiar el código con esos cambios que tienden a limitarse a la menor cantidad posible de clases existentes. Agregar nuevo código es menos riesgoso que cambiar el código existente, por lo que, en este sentido, Open-Closed es algo bueno. ¿Pero de qué código estamos hablando exactamente? El delito de violar OC es mucho menor cuando puede agregar nuevos métodos a una clase en lugar de tener que modificar los existentes.

OC es fractal . Manzanas en todas las profundidades de su diseño. Todos asumen que solo se aplica a nivel de clase. Pero es igualmente aplicable a nivel de método o a nivel de ensamblaje.

La violación demasiado frecuente de OC en el nivel apropiado sugiere que tal vez sea hora de refactorizar . El "nivel apropiado" es una llamada de juicio que tiene todo que ver con su diseño general.

Seguir a Abierto-Cerrado significa literalmente que el número de clases explotará. Crearás (mayúscula "I") interfaces innecesariamente. Terminará con bits de funcionalidad repartidos entre las clases y luego tendrá que escribir mucho más código para conectarlo todo. En algún momento se te ocurrirá que cambiar la clase original hubiera sido mejor.


2
"El delito de violar OC es mucho menor cuando se pueden agregar nuevos métodos a una clase en lugar de tener que alterar los existentes". . El problema es cambiar los métodos existentes que implementan una interfaz bien definida y, por lo tanto, ya tienen una semántica bien definida (cerrada para modificación). En principio, la refactorización no cambia la semántica, por lo que el único riesgo que puedo ver es introducir errores en un código ya estable y bien probado.
Giorgio

1
Aquí está la respuesta de CodeReview que ilustra abierto para la extensión . Ese diseño de clase es extensible. Por el contrario, agregar un método es modificar la clase.
radarbob

Agregar nuevos métodos viola LSP, no OCP.
Tulains Córdova

1
Agregar nuevos métodos no viola LSP. Si agrega un método, ha introducido una nueva interfaz @ TulainsCórdova
RubberDuck

6

El principio abierto-cerrado parece ser un principio que apareció antes de que TDD fuera más frecuente. La idea es que es riesgoso refactorizar el código porque puede romper algo, por lo que es más seguro dejar el código existente tal como está y simplemente agregarlo. En ausencia de pruebas, esto tiene sentido. La desventaja de este enfoque es la atrofia del código. Cada vez que extiende una clase en lugar de refactorizarla, termina con una capa adicional. Simplemente estás atornillando el código en la parte superior. Cada vez que agregue más código, aumentará las posibilidades de duplicación. Imagina; Hay un servicio en mi base de código que quiero usar, creo que no tiene lo que quiero, así que creo una nueva clase para extenderlo e incluir mi nueva funcionalidad. Otro desarrollador aparece más tarde y también quiere usar el mismo servicio. Desafortunadamente, ellos no No me doy cuenta de que mi versión extendida existe. Codifican contra la implementación original pero también necesitan una de las características que codifiqué. En lugar de usar mi versión, ahora también amplían la implementación y agregan la nueva característica. Ahora tenemos 3 clases, la original y dos versiones nuevas que tienen alguna funcionalidad duplicada. Siga el principio abierto / cerrado y esta duplicación continuará acumulándose durante la vida útil del proyecto, lo que conducirá a una base de código innecesariamente compleja.

Con un sistema bien probado no hay necesidad de sufrir esta atrofia del código, puede refactorizar el código de manera segura permitiendo que su diseño asimile nuevos requisitos en lugar de tener que atornillarse continuamente en un nuevo código. Este estilo de desarrollo se llama diseño emergente y conduce a bases de código que pueden mantenerse en buena forma durante toda su vida útil en lugar de recolectar gradualmente cruft.


1
No soy un defensor del principio abierto-cerrado ni de TDD (en el sentido de que no los inventé). Lo que me sorprendió fue que alguien propuso el principio abierto-cerrado Y el uso de refactorización Y TDD al mismo tiempo. Esto me parecía contradictorio, por lo que estaba tratando de descubrir cómo reunir todas estas pautas en un proceso coherente.
Giorgio

"La idea es que es riesgoso refactorizar el código porque podría romper algo, por lo que es más seguro dejar el código existente como está y simplemente agregarlo".: En realidad, no lo veo de esta manera. La idea es más bien tener unidades pequeñas y autónomas que pueda reemplazar o extender (permitiendo así que el software evolucione), pero no debe tocar cada unidad una vez que se haya probado a fondo.
Giorgio

Debe pensar que la clase no solo se utilizará en su base de código. La biblioteca que escriba puede usarse en otros proyectos. Entonces OCP es importante. Además, un nuevo programador que no conoce una clase extendida con la funcionalidad que necesita es un problema de comunicación / documentación, no un problema de diseño.
Tulains Córdova

@ TulainsCórdova en el código de la aplicación esto no es relevante. Para el código de la biblioteca, diría que el control de versiones semántico es más adecuado para comunicar cambios importantes.
opsb

1
@ TulainsCórdova con código de biblioteca La estabilidad de la API es mucho más importante porque no es posible probar el código del cliente. Con el código de aplicación, su cobertura de prueba le informará de cualquier rotura de inmediato. Dicho de otra manera, el código de la aplicación puede realizar cambios importantes sin riesgo, mientras que el código de la biblioteca debe administrar el riesgo manteniendo una API estable y señalizando las roturas utilizando, por ejemplo, versiones semánticas
opsb

6

En palabras simples:

R. El principio de O / C significa que la especialización debe hacerse extendiendo, no modificando una clase para acomodar necesidades especializadas.

B. Agregar funcionalidades faltantes (no especializadas) significa que el diseño no se completó y debe agregarlo a la clase base, obviamente sin violar el contrato. Creo que esto no está violando el principio.

C. La refactorización no viola el principio.

Cuando un diseño madura , por ejemplo, después de un tiempo en producción:

  • Debería haber muy pocas razones para hacerlo (punto B), tendiendo a cero con el tiempo.
  • (Punto C) siempre será posible, aunque más infrecuente.
  • Se supone que toda nueva funcionalidad es una especialización, lo que significa que las clases deben extenderse (heredarse de) (punto A).

El principio abierto / cerrado es muy mal entendido. Sus puntos A y B lo hacen exactamente bien.
gnasher729

1

Para mí, el Principio Abierto-Cerrado es una guía, no una regla dura y rápida.

Con respecto a la parte abierta del principio, las clases finales en Java y las clases en C ++ con todos los constructores declarados privados violan la parte abierta del principio abierto-cerrado. Hay buenos casos de uso sólidos (nota: sólido, no SÓLIDO) para las clases finales. Diseñar para la extensibilidad es importante. Sin embargo, esto requiere una gran cantidad de previsión y esfuerzo, y siempre estás bordeando la línea de violar a YAGNI (no lo vas a necesitar) e inyectando el olor del código de generalidad especulativa. ¿Deberían los componentes clave del software estar abiertos para la extensión? Si. ¿Todas? No. Eso en sí mismo es generalidad especulativa.

Con respecto a la parte cerrada, cuando se pasa de la versión 2.0 a 2.1 a 2.2 a 2.3 de algún producto, no modificar el comportamiento es una buena idea. A los usuarios realmente no les gusta cuando cada versión menor rompe su propio código de barras. Sin embargo, en el camino a menudo se encuentra que la implementación inicial en la versión 2.0 se rompió fundamentalmente, o que las restricciones externas que limitaron el diseño inicial ya no se aplican. ¿Sonríe y lo soporta y mantiene ese diseño en la versión 3.0, o hace que 3.0 no sea compatible con versiones anteriores en algún aspecto? La compatibilidad con versiones anteriores puede ser una gran limitación. Los límites de liberación principales son el lugar donde es aceptable romper la compatibilidad con versiones anteriores. Debe tener cuidado de que hacer esto puede molestar a sus usuarios. Tiene que haber un buen caso de por qué se necesita esta ruptura con el pasado.


0

Refactorizar, por definición, es cambiar la estructura del código sin cambiar el comportamiento. Entonces, cuando refactoriza, no agrega nuevas características.

Lo que hiciste como ejemplo para el principio Open Close suena bien. Este principio se trata de extender el código existente con nuevas características.

Sin embargo, no obtenga esta respuesta incorrecta. No quiero decir que solo debas hacer funciones o solo refactorizar grandes cantidades de datos. La forma más común de programación es hacer un poco de una función que inmediatamente hacer un poco de refactorización (combinado con pruebas, por supuesto, para asegurarse de que no haya cambiado ningún comportamiento). Refactorización compleja no significa refactorización "grande", significa aplicar técnicas de refactorización complicadas y bien pensadas.

Sobre los principios SÓLIDOS. Son realmente buenas pautas para el desarrollo de software, pero no son reglas religiosas que se sigan ciegamente. A veces, muchas veces, después de agregar una segunda y tercera característica, se da cuenta de que su diseño inicial, incluso si respeta Open-Close, no respeta otros principios o requisitos de software. Hay puntos en la evolución de un diseño y de un software cuando hay que hacer cambios más complejos. El objetivo es encontrar y darse cuenta de estos problemas lo antes posible y aplicar técnicas de refactorización lo mejor posible.

No existe el diseño perfecto. No existe tal diseño que pueda y deba respetar todos los principios o patrones existentes. Eso es codificar la utopía.

Espero que esta respuesta te haya ayudado en tu dilema. No dude en solicitar aclaraciones si es necesario.


1
"Entonces, cuando refactoriza, no agrega nuevas funciones": pero podría introducir errores en un software probado.
Giorgio

"A veces, muchas veces, después de agregar una segunda y tercera y enésima función, te das cuenta de que tu diseño inicial, incluso si respeta Open-Close, no respeta otros principios o requisitos de software". comience a escribir una nueva implementación By, cuando esté listo, reemplace la implementación anterior Acon la nueva implementación B(ese es un uso de interfaces). AEl código puede servir como base para Bel código y luego puedo usar la refactorización del Bcódigo durante su desarrollo, pero creo que el Acódigo ya probado debería permanecer congelado.
Giorgio

@Giorgio Cuando refactoriza puede introducir errores, es por eso que escribe pruebas (o incluso mejor hace TDD). La forma más segura de refactorizar es cambiar el código cuando sabes que está funcionando. Sabes esto al tener un conjunto de pruebas que están pasando. Después de cambiar su código de producción, las pruebas aún tienen que pasar, para que sepa que no introdujo un error. Y recuerde, las pruebas son tan importantes como el código de producción, por lo que debe aplicarles la misma regla que al código de producción y mantenerlas limpias y refactorizarlas periódicamente y con frecuencia.
Patkos Csaba

@Giorgio Si el código Bse basa en el código Acomo una evolución de A, entonces, cuando Bse libera, Adebe eliminarse y nunca volver a usarse. Los clientes que usaban anteriormente Asolo lo usarán Bsin saber sobre el cambio, ya que la interfaz Ino se cambió (¿tal vez un poco del Principio de sustitución de Liskov aquí? ... la L de SOLID)
Patkos Csaba

Sí, esto es lo que tenía en mente: no deseche el código de trabajo hasta que tenga un reemplazo válido (bien probado).
Giorgio

-1

Según tengo entendido, si agrega nuevos métodos a la clase existente, entonces no romperá el OCP. Sin embargo, estoy un poco confundido con la adición de nuevas variables en la clase. Pero si cambia el método y los parámetros existentes en el método existente, seguramente romperá el OCP, porque el código ya se ha probado y aprobado si cambiamos intencionalmente el método [Cuando el cambio de requisito] entonces será un problema.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.