¿Por qué muchos desarrolladores de software violan el principio abierto / cerrado?

74

¿Por qué muchos desarrolladores de software violan el principio de abrir / cerrar al modificar muchas cosas como renombrar funciones que romperán la aplicación después de la actualización?

Esta pregunta salta a mi cabeza después de las versiones rápidas y continuas en la biblioteca React .

Cada corto período noto muchos cambios en la sintaxis, nombres de componentes, ... etc.

Ejemplo en la próxima versión de React :

Nuevas advertencias de desaprobación

El mayor cambio es que hemos extraído React.PropTypes y React.createClass en sus propios paquetes. Aún se puede acceder a ambos a través del objeto React principal, pero el uso de ambos registrará una advertencia de desaprobación única en la consola cuando se encuentre en modo de desarrollo. Esto permitirá futuras optimizaciones de tamaño de código.

Estas advertencias no afectarán el comportamiento de su aplicación. Sin embargo, nos damos cuenta de que pueden causar cierta frustración, especialmente si utiliza un marco de prueba que trata la consola.error como un error.


  • ¿Se consideran estos cambios como una violación de ese principio?
  • Como principiante en algo como React , ¿cómo lo aprendo con estos rápidos cambios en la biblioteca (es muy frustrante)?
Anyname Donotcare
fuente
66
Este es claramente un ejemplo de observarlo , y su reclamo 'tantos' no tiene fundamento. Los proyectos de Lucene y RichFaces son ejemplos notorios, y la API de puerto de Windows COMM, pero no puedo pensar en ningún otro. ¿Y es React realmente un "gran desarrollador de software"?
user207421
6262
Como cualquier principio, el OCP tiene su valor. Pero requiere que los desarrolladores tengan una previsión infinita. En el mundo real, las personas a menudo se equivocan en su primer diseño. A medida que pasa el tiempo, algunos prefieren solucionar sus viejos errores por razones de compatibilidad, otros prefieren limpiarlos con el fin de tener una base de código compacta y sin cargas.
Theodoros Chatzigiannakis
1
¿Cuándo fue la última vez que vio un lenguaje orientado a objetos "como se pretendía originalmente"? El principio central era un sistema de mensajería que significaba que cada parte del sistema es infinitamente extensible por cualquiera. Ahora compare eso con su típico lenguaje tipo OOP: ¿cuántos le permiten extender un método existente desde el exterior? ¿Cuántos hacen que sea lo suficientemente fácil como para ser útil?
Luaan
El legado apesta. 30 años de experiencia han demostrado que debe eliminar completamente el legado y comenzar de nuevo, en todo momento. Hoy todos tienen conexión en todas partes en todo momento, por lo que el legado es totalmente irrelevante hoy. El último ejemplo fue "Windows versus Mac". Microsoft tradicionalmente trató de "soportar el legado", lo ves de muchas maneras. Apple siempre acaba de decir "F- - - You" a los usuarios heredados. (Esto se aplica a todo, desde idiomas hasta dispositivos y sistemas operativos). De hecho, Apple estaba totalmente en lo correcto y MSFT estaba totalmente equivocado, claro y simple.
Fattie
44
Porque hay exactamente cero "principios" y "patrones de diseño" que funcionan el 100% del tiempo en la vida real.
Matti Virkkunen

Respuestas:

148

En mi humilde opinión, la respuesta de JacquesB, aunque contiene mucha verdad, muestra un malentendido fundamental de la OCP. Para ser justos, su pregunta ya expresa este malentendido, también: cambiar el nombre de las funciones rompe la compatibilidad con versiones anteriores , pero no el OCP. Si parece necesario romper la compatibilidad (o mantener dos versiones del mismo componente para no romper la compatibilidad), ¡el OCP ya estaba roto antes!

Como Jörg W Mittag ya mencionó en sus comentarios, el principio no dice "no se puede modificar el comportamiento de un componente", dice, uno debe tratar de diseñar los componentes de una manera que estén abiertos para ser reutilizados (o extendidos) de varias maneras, sin necesidad de modificación. Esto se puede hacer proporcionando los "puntos de extensión" correctos o, como lo menciona @AntP, "descomponiendo una estructura de clase / función en el punto donde cada punto de extensión natural está allí por defecto". En mi humilde opinión, seguir el OCP no tiene nada en común con "mantener la versión anterior sin cambios para la compatibilidad con versiones anteriores" . O, citando el comentario de @ DerekElkin a continuación:

El OCP es un consejo sobre cómo escribir un [...] módulo, no sobre la implementación de un proceso de gestión de cambios que nunca permita que los módulos cambien.

Los buenos programadores usan su experiencia para diseñar componentes con los puntos de extensión "correctos" en mente (o, mejor aún, de alguna manera no se necesitan puntos de extensión artificiales). Sin embargo, para hacer esto correctamente y sin una ingeniería excesiva innecesaria, debe saber de antemano cómo podrían ser los casos de uso futuros de su componente. Incluso los programadores experimentados no pueden mirar hacia el futuro y conocer de antemano todos los requisitos futuros. Y es por eso que a veces se debe violar la compatibilidad con versiones anteriores : no importa cuántos puntos de extensión tenga su componente o cuán bien siga el OCP con respecto a ciertos tipos de requisitos, siempre habrá un requisito que no se puede implementar fácilmente sin modificar el componente.

Doc Brown
fuente
14
En mi opinión, la razón más importante para "violar" el OCP es que se requiere mucho esfuerzo para cumplirlo adecuadamente. Eric Lippert tiene una excelente publicación de blog sobre por qué muchas de las clases de framework .NET parecen violar OCP.
BJ Myers
2
@BJMyers: gracias por el enlace. Jon Skeet tiene una excelente publicación sobre el OCP ya que es muy similar a la idea de la variación protegida.
Doc Brown
8
¡ESTA! ¡El OCP dice que debe escribir código que se pueda cambiar sin ser tocado! ¿Por qué? Por lo tanto, solo tiene que probarlo, revisarlo y compilarlo una vez. El nuevo comportamiento debe provenir de un nuevo código. No atornillando con el viejo código probado. ¿Qué pasa con la refactorización? ¡Refactorizar bien es una clara violación de OCP! Es por eso que es un pecado escribir código pensando que simplemente lo refactorizará si sus suposiciones cambian. ¡No! Ponga cada suposición en su propia cajita. Cuando está mal, no arregles la caja. Escribe uno nuevo. ¿Por qué? Porque es posible que deba volver al anterior. Cuando lo hagas, sería bueno si todavía funcionara.
candied_orange
77
@CandiedOrange: gracias por tu comentario. No veo refactorización y OCP tan contrarios como lo describe. Para escribir componentes que siguen el OCP a menudo se requieren varios ciclos de refactorización. El objetivo debe ser un componente que no necesite modificaciones para resolver una "familia" completa de requisitos. Sin embargo, uno no debería agregar puntos de extensión arbitrarios a un componente "por si acaso", que conduce demasiado fácilmente a la ingeniería excesiva. Confiar en la posibilidad de refactorizar puede ser la mejor alternativa para esto en muchos casos.
Doc Brown
44
Esta respuesta hace un buen trabajo al señalar los errores en la respuesta principal (actualmente): creo que la clave para tener éxito con abierto / cerrado es dejar de pensar en términos de "puntos de extensión" y comenzar a pensar en descomponer su estructura de clase / función hasta el punto donde cada punto de extensión natural está allí por defecto. Programación "afuera hacia adentro" es una muy buena manera de conseguir esto, en todos los escenarios de su método / función actual abastece a los que se haya expulsado a una interfaz externa, que forma un punto de extensión natural para decoradores, adaptadores, etc.
Ant P
67

El principio abierto / cerrado tiene beneficios, pero también tiene algunos inconvenientes serios.

En teoría, el principio resuelve el problema de la compatibilidad con versiones anteriores mediante la creación de código que está "abierto para la extensión pero cerrado para la modificación". Si una clase tiene algunos requisitos nuevos, nunca modifica el código fuente de la clase en sí, sino que crea una subclase que anula solo los miembros apropiados necesarios para cambiar el comportamiento. Por lo tanto, todo el código escrito en la versión original de la clase no se ve afectado, por lo que puede estar seguro de que su cambio no rompió el código existente.

En realidad , fácilmente terminas con una gran cantidad de código y un lío confuso de clases obsoletas. Si no es posible modificar el comportamiento de un componente a través de la extensión, debe proporcionar una nueva variante del componente con el comportamiento deseado y mantener la versión anterior sin cambios para la compatibilidad con versiones anteriores.

Supongamos que descubre una falla de diseño fundamental en una clase base de la cual muchas clases heredan. Digamos que el error se debe a que un campo privado es del tipo incorrecto. No puede solucionar esto anulando un miembro. Básicamente, debe anular toda la clase, lo que significa que termina extendiéndose Objectpara proporcionar una clase base alternativa, y ahora también debe proporcionar alternativas a todas las subclases, lo que termina con una jerarquía de objetos duplicados, una jerarquía defectuosa, una mejorada . Pero no puede eliminar la jerarquía defectuosa (ya que la eliminación del código es una modificación), todos los clientes futuros estarán expuestos a ambas jerarquías.

Ahora la respuesta teórica a este problema es "simplemente diseñarlo correctamente la primera vez". Si el código se descompone perfectamente, sin fallas ni errores, y está diseñado con puntos de extensión preparados para todos los posibles cambios de requisitos futuros, entonces evitará el desastre. Pero en realidad todos cometen errores y nadie puede predecir el futuro a la perfección.

Tomemos algo como el marco .NET: todavía contiene el conjunto de clases de colección que fueron diseñadas antes de que se introdujeran los genéricos hace más de una década. Esto es ciertamente una bendición para la compatibilidad con versiones anteriores (puede actualizar el marco sin tener que volver a escribir nada), pero también aumenta el marco y presenta a los desarrolladores un gran conjunto de opciones donde muchas simplemente son obsoletas.

Aparentemente, los desarrolladores de React han sentido que no valía la pena el costo en complejidad y en código inflado para seguir estrictamente el principio abierto / cerrado.

La alternativa pragmática a abrir / cerrar es la depreciación controlada. En lugar de romper la compatibilidad con versiones anteriores en una sola versión, los componentes antiguos se mantienen durante un ciclo de lanzamiento, pero los clientes son informados a través de advertencias del compilador de que el enfoque anterior se eliminará en una versión posterior. Esto les da a los clientes tiempo para modificar el código. Este parece ser el enfoque de React en este caso.

(Mi interpretación del principio se basa en El principio abierto-cerrado de Robert C. Martin)

JacquesB
fuente
37
"El principio básicamente dice que no se puede modificar el comportamiento de un componente. En cambio, debe proporcionar una nueva variante del componente con el comportamiento deseado y mantener la versión anterior sin cambios para la compatibilidad con versiones anteriores". - No estoy de acuerdo con esto. El principio dice que debe diseñar componentes de tal manera que no sea necesario cambiar su comportamiento porque puede extenderlo para hacer lo que desee. El problema es que todavía no hemos descubierto cómo hacerlo, especialmente con los idiomas que actualmente se usan ampliamente. El problema de expresión es una parte de ...
Jörg W Mittag
8
... eso, por ejemplo. Ni Java ni C♯ tienen una solución para la Expresión. Haskell y Scala lo hacen, pero su base de usuarios es mucho más pequeña.
Jörg W Mittag
1
@Giorgio: en Haskell, la solución son las clases de tipos. En Scala, la solución es implicits y objetos. Lo siento, no tengo los enlaces a mano, actualmente. Sí, los métodos múltiples (en realidad, ni siquiera necesitan ser "múltiples", es más bien la naturaleza "abierta" de los métodos de Lisp lo que se requiere) también son una posible solución. Tenga en cuenta que existen múltiples fraseos del problema expresión, porque normalmente los documentos están escritos de tal manera que el autor añade una restricción al problema de la expresión que se traduce en el hecho de que todas las soluciones existentes en la actualidad no son válidas, a continuación, muestra cómo su propia ...
Jörg W Mittag
1
... el lenguaje puede incluso resolver esta versión "más difícil". Por ejemplo, Wadler originalmente formuló el problema de la expresión no solo sobre la extensión modular, sino también sobre la extensión modular estáticamente segura. Métodos múltiples de Common Lisp son sin embargo no estáticamente segura, sólo son dinámicamente seguro. Luego, Odersky fortaleció esto aún más al decir que debería ser modular estáticamente seguro, es decir, la seguridad debería ser estáticamente verificable sin mirar todo el programa, solo mirando el módulo de extensión. En realidad, esto no se puede hacer con las clases de tipo Haskell, pero se puede hacer con Scala. Y en el ...
Jörg W Mittag
2
@ Jorge: Exactamente. Lo que hace que los métodos múltiples de Common Lisp resuelvan el EP no es un despacho múltiple. Es el hecho de que los métodos están abiertos. En la típica FP (o programación procesal), la discriminación de tipo está vinculada a las funciones. En OO típico, los métodos están vinculados a los tipos. Los métodos comunes de Lisp son abiertos , se pueden agregar a las clases después del hecho y en un módulo diferente. Esa es la característica que los hace utilizables para resolver el EP. Por ejemplo, los protocolos de Clojure son de envío único, pero también resuelven el EP (siempre y cuando no insista en la seguridad estática).
Jörg W Mittag
20

Yo llamaría al principio abierto / cerrado un ideal. Como todos los ideales, da poca consideración a las realidades del desarrollo de software. También, como todos los ideales, es imposible alcanzarlo en la práctica: uno simplemente se esfuerza por alcanzar ese ideal lo mejor que puede.

El otro lado de la historia se conoce como las esposas doradas. Las esposas doradas son lo que obtienes cuando te esclavas demasiado al principio abierto / cerrado. Las esposas doradas son lo que ocurre cuando su producto que nunca rompe la compatibilidad con versiones anteriores no puede crecer porque se han cometido demasiados errores pasados.

Un famoso ejemplo de esto se encuentra en el administrador de memoria de Windows 95. Como parte de la comercialización de Windows 95, se afirmó que todas las aplicaciones de Windows 3.1 funcionarían en Windows 95. Microsoft adquirió licencias para miles de programas para probarlas en Windows 95. Uno de los casos problemáticos fue Sim City. Sim City en realidad tenía un error que hacía que escribiera en la memoria no asignada. En Windows 3.1, sin un administrador de memoria "adecuado", este fue un pequeño paso en falso. Sin embargo, en Windows 95, el administrador de memoria detectaría esto y causaría un error de segmentación. ¿La solución? En Windows 95, si el nombre de su aplicación es simcity.exe, ¡el sistema operativo realmente relajará las restricciones del administrador de memoria para evitar la falla de segmentación!

El verdadero problema detrás de este ideal son los conceptos recortados de productos y servicios. Nadie realmente hace uno u otro. Todo se alinea en algún lugar de la región gris entre los dos. Si piensa desde un enfoque orientado al producto, abrir / cerrar suena como un gran ideal. Sus productos son confiables. Sin embargo, cuando se trata de servicios, la historia cambia. Es fácil demostrar que con el principio abierto / cerrado, la cantidad de funcionalidad que su equipo debe admitir debe acercarse asintóticamente al infinito, porque nunca puede limpiar la funcionalidad anterior. Esto significa que su equipo de desarrollo debe admitir más y más código cada año. Eventualmente llegas a un punto de ruptura.

La mayoría del software actual, especialmente el de código abierto, sigue una versión relajada común del principio abierto / cerrado. Es muy común ver abierto / cerrado seguido servilmente para lanzamientos menores, pero abandonado para lanzamientos mayores. Por ejemplo, Python 2.7 contiene muchas "malas elecciones" de los días Python 2.0 y 2.1, pero Python 3.0 los barrió a todos. (Además, el cambio del código base de Windows 95 con el código base de Windows NT cuando lanzaron Windows 2000 rompió todo tipo de cosas, pero no quiere decir que nunca tenemos que hacer frente a un administrador de memoria comprobar el nombre de la aplicación para decidir comportamiento!)

Cort Ammon
fuente
Esa es una muy buena historia sobre SimCity. Tienes una fuente?
BJ Myers
55
@BJMyers Es una vieja historia, Joel Spoleky lo menciona cerca del final de este artículo . Originalmente lo leí como parte de un libro sobre el desarrollo de videojuegos hace años.
Cort Ammon
1
@BJMyers: Estoy bastante seguro de que tenían "hacks" de compatibilidad similares para docenas de aplicaciones populares.
Doc Brown
3
@BJMyers hay muchas cosas como esta, si quieres una buena lectura ve al blog The Old New Thing de Raymond Chen , busca la etiqueta Historial o busca "compatibilidad". Se recuerdan muchas historias, incluyendo algo notablemente cercano al caso de SimCity mencionado anteriormente . Addentum: A Chen no le gusta culpar a los nombres.
Theraot
2
Muy pocas cosas se rompieron incluso en la transición 95-> NT. El SimCity original para Windows todavía funciona muy bien en Windows 10 (32 bits). Incluso los juegos de DOS funcionan perfectamente bien siempre que desactive el sonido o use algo como VDMSound para permitir que el subsistema de la consola maneje el audio correctamente. Microsoft se toma muy en serio la compatibilidad con versiones anteriores y tampoco se está tomando ningún atajo de "vamos a ponerlo en una máquina virtual". A veces necesita una solución, pero aún así es bastante impresionante, especialmente en términos relativos.
Luaan
11

La respuesta de Doc Brown es más cercana a precisa, las otras respuestas ilustran malentendidos del Principio Abierto Cerrado.

Para articular de forma explícita el malentendido, parece que hay una creencia de que el OCP significa que no se debe hacer cambios hacia atrás incompatibles (o incluso cualquier cambios o algo en este sentido.) El OCP se trata de diseñar componentes de manera que usted no necesita a realizar cambios en ellos para ampliar su funcionalidad, independientemente de si esos cambios son compatibles con versiones anteriores o no. Hay muchas otras razones además de agregar funcionalidad para que pueda realizar cambios en un componente, ya sea que sean compatibles con versiones anteriores (por ejemplo, refactorización u optimización) o incompatibles con versiones anteriores (por ejemplo, depreciar y eliminar la funcionalidad). Que pueda hacer estos cambios no significa que su componente haya violado el OCP (y definitivamente no significa que usted están violando el OCP).

Realmente, no se trata del código fuente en absoluto. Una declaración más abstracta y relevante del OCP es: "un componente debe permitir la extensión sin necesidad de violar sus límites de abstracción". Yo iría más lejos y diría que una interpretación más moderna es: "un componente debería imponer sus límites de abstracción pero permitir la extensión". Incluso en el artículo sobre el OCP de Bob Martin mientras "describe" "cerrado a modificación" como "el código fuente es inviolable", más tarde comienza a hablar sobre la encapsulación que no tiene nada que ver con modificar el código fuente y todo que ver con la abstracción fronteras

Entonces, la premisa defectuosa en la pregunta es que el OCP es (destinado como) una guía sobre la evolución de una base de código. El OCP generalmente se eslogan como "un componente debe estar abierto a extensiones y cerrado a modificaciones por parte de los consumidores". Básicamente, si un consumidor de un componente desea agregar funcionalidad al componente, debería poder extender el componente antiguo a uno nuevo con la funcionalidad adicional, pero no debería poder cambiar el componente anterior.

El OCP no dice nada sobre el creador de un componente que cambia o elimina la funcionalidad. El OCP no recomienda mantener la compatibilidad de errores para siempre. Usted, como creador, no está violando el OCP al cambiar o incluso eliminar un componente. Usted, o más bien los componentes que ha escrito, están violando el OCP si la única forma en que los consumidores pueden agregar funcionalidad a sus componentes es mediante la mutación, por ejemplo, mediante parches de monoo tener acceso al código fuente y volver a compilar. En muchos casos, ninguna de estas opciones son para el consumidor, lo que significa que si su componente no está "abierto a la extensión" no tiene suerte. Simplemente no pueden usar su componente para sus necesidades. El OCP argumenta que no debe poner a los consumidores de su biblioteca en esta posición, al menos con respecto a alguna clase identificable de "extensiones". Incluso cuando se pueden hacer modificaciones al código fuente o incluso a la copia primaria del código fuente, es mejor "pretender" que no puede modificarlo, ya que existen muchas posibles consecuencias negativas.

Entonces, para responder a sus preguntas: No, estas no son violaciones del OCP. Ningún cambio que realice un autor puede ser una violación del OCP porque el OCP no es una proporción de los cambios. Sin embargo, los cambios pueden crear violaciones del OCP y pueden estar motivados por fallas del OCP en versiones anteriores de la base de código. El OCP es una propiedad de un código particular, no la historia evolutiva de una base de código.

Por el contrario, la compatibilidad con versiones anteriores es una propiedad de un cambio de código. No tiene sentido decir que algún código es o no compatible con versiones anteriores. Solo tiene sentido hablar sobre la compatibilidad con versiones anteriores de algunos códigos con respecto a algunos códigos más antiguos. Por lo tanto, nunca tiene sentido hablar de que el primer corte de algún código sea compatible o no con versiones anteriores. El primer corte de código puede satisfacer o no satisfacer el OCP, y en general podemos determinar si algún código satisface el OCP sin hacer referencia a ninguna versión histórica del código.

En cuanto a su última pregunta, podría decirse que está fuera de tema para StackExchange en general, ya que se basa principalmente en la opinión, pero en resumen es bienvenido a la tecnología y particularmente a JavaScript, donde en los últimos años el fenómeno que describe se ha llamado fatiga de JavaScript . (Siéntase libre de google para encontrar una variedad de otros artículos, algunos satíricos, que hablan de esto desde múltiples perspectivas).

Derek Elkins
fuente
3
"Usted, como creador, no está violando el OCP al cambiar o incluso eliminar un componente". - ¿Puedes proporcionar una referencia para esto? Ninguna de las definiciones del principio que he visto establece que "el creador" (lo que sea que eso signifique) está exento del principio. Eliminar un componente publicado es claramente un cambio radical.
JacquesB
1
@JacquesB Las personas e incluso los cambios de código no violan el OCP, los componentes (es decir, piezas de código reales) sí lo hacen. (Y, para ser perfectamente claro, eso significa que el componente no está a la altura del OCP en sí mismo, no es que viole el OCP de algún otro componente). El punto principal de mi respuesta es que el OCP no está hablando de cambios de código , rompiendo o de otra manera. Un componente está abierto a la extensión y cerrado a la modificación, o no lo es, al igual que un método puede ser privateo no. Si un autor hace un privatemétodo publicmás adelante, eso no significa que haya violado el control de acceso, (1/2)
Derek Elkins
2
... ni significa que el método no era realmente privateantes. "Quitar un componente publicado es claramente un cambio radical", es una no secuencia. O los componentes de la nueva versión satisfacen el OCP o no, no necesita el historial de la base de código para determinar esto. Según su lógica, nunca podría escribir código que satisfaga el OCP. Está combinando compatibilidad con versiones anteriores, una propiedad de cambios de código, con el OCP, una propiedad de código. Su comentario tiene tanto sentido como decir que quicksort no es compatible con versiones anteriores. (2/2)
Derek Elkins
3
@JacquesB Primero, tenga en cuenta nuevamente que se trata de un módulo que se ajusta al OCP. El OCP es un consejo sobre cómo escribir un módulo para que, dada la restricción de que el código fuente no se pueda cambiar, el módulo se pueda extender. Anteriormente en el documento, habla sobre el diseño de módulos que nunca cambian, no sobre la implementación de un proceso de gestión de cambios que nunca permita que los módulos cambien. Refiriéndose a la edición de su respuesta, no "rompe el OCP" modificando el código del módulo. En cambio, si "extiende" el módulo requiere que modifique el código fuente, (1/3)
Derek Elkins
2
"El OCP es una propiedad de un código particular, no la historia evolutiva de una base de código". - excelente!
Doc Brown