Cómo adherirse al principio abierto-cerrado en la práctica

14

Entiendo la intención del principio abierto-cerrado. Está destinado a reducir el riesgo de romper algo que ya funciona mientras se modifica, al decirle que intente extender sin modificar.

Sin embargo, tuve algunos problemas para entender cómo se aplica este principio en la práctica. A mi entender, hay dos formas de aplicarlo. Antes y después de un posible cambio:

  1. Antes: programe abstracciones y "prediga el futuro" tanto como pueda. Por ejemplo, un método drive(Car car)tendrá que cambiar si Motorcyclese agregan s al sistema en el futuro, por lo que probablemente viola OCP. Pero drive(MotorVehicle vehicle)es menos probable que el método tenga que cambiar en el futuro, por lo que se adhiere a OCP.

    Sin embargo, es bastante difícil predecir el futuro y saber de antemano qué cambios se realizarán en el sistema.

  2. Después: cuando se necesita un cambio, extienda una clase en lugar de modificar su código actual.

La práctica # 1 no es difícil de entender. Sin embargo, es la práctica # 2 que tengo problemas para entender cómo presentar una solicitud.

Por ejemplo (la aparté de un video en YouTube): Digamos que tenemos un método en una clase que acepta CreditCardobjetos: makePayment(CraditCard card). Un día Vouchers se agregan al sistema. Este método no los admite, por lo que debe modificarse.

Al implementar el método en primer lugar, no pudimos predecir el futuro y programar en términos más abstractos (por ejemplo makePayment(Payment pay), ahora tenemos que cambiar el código existente.

La práctica # 2 dice que deberíamos agregar la funcionalidad extendiendo en lugar de modificar. Qué significa eso? ¿Debo subclasificar la clase existente en lugar de simplemente cambiar su código existente? ¿Debo hacer algún tipo de envoltura alrededor para evitar reescribir el código?

¿O el principio ni siquiera se refiere a 'cómo modificar / agregar correctamente la funcionalidad', sino que se refiere a 'cómo evitar tener que hacer cambios en primer lugar (es decir, programa para abstracciones)?

Aviv Cohn
fuente
1
El principio abierto / cerrado no dicta el mecanismo que utiliza. La herencia suele ser la elección incorrecta. Además, es imposible protegerse contra todos los cambios futuros. Es mejor no tratar de predecir el futuro, pero una vez que se necesite un cambio, modifique el diseño para que los cambios futuros del mismo tipo puedan adaptarse.
Doval

Respuestas:

14

Los principios de diseño siempre deben equilibrarse entre sí. No se puede predecir el futuro, y la mayoría de los programadores lo hacen horriblemente cuando lo intentan. Es por eso que tenemos la regla de tres , que se trata principalmente de duplicación, pero también se aplica a la refactorización de cualquier otro principio de diseño.

Cuando solo tiene una implementación de una interfaz, no necesita preocuparse demasiado por OCP, a menos que esté claro dónde se realizarían las extensiones. De hecho, a menudo pierdes claridad cuando intentas sobre-diseñar en esta situación. Cuando lo extiende una vez, refactoriza para que sea compatible con OCP si esa es la forma más fácil y clara de hacerlo. Cuando lo extiende a una tercera implementación, se asegura de refactorizarlo teniendo en cuenta OCP, incluso si requiere un poco más de esfuerzo.

En la práctica, cuando solo tiene dos implementaciones, la refactorización cuando agrega una tercera generalmente no es demasiado difícil. Es cuando dejas que crezca más allá de ese punto que se vuelve problemático de mantener.

Karl Bielefeldt
fuente
1
Gracias por responder. Déjame ver si entiendo lo que estás diciendo: lo que estás diciendo es que debería preocuparme por OCP principalmente después de que me vi obligado a hacer cambios en una clase. Significado: cuando implemente una clase por primera vez, no debería preocuparme mucho por OCP, ya que de todos modos es difícil predecir el futuro. Cuando necesito extenderlo / modificarlo por primera vez, tal vez sea una buena idea refactorizar un poco para ser más flexible en el futuro (más OCP). Y en la tercera vez que necesito extender / modificar la clase, es hora de refactorizar para que se adhiera más a OCP. ¿Es esto lo que quieres decir?
Aviv Cohn
1
Esa es la idea.
Karl Bielefeldt
2

Creo que estás mirando demasiado lejos en el futuro. Resuelva el problema actual de una manera flexible que se adhiera a abrir / cerrar.

Digamos que necesita implementar un drive(Car car)método. Dependiendo de su idioma, tiene un par de opciones.

  • Para los lenguajes que admiten sobrecarga (C ++), simplemente use drive(const Car& car)

    En algún momento posterior, es posible que lo necesite drive(const Motorcycle& motorcycle), pero no interferirá drive(const Car& car). ¡No hay problema!

  • Para los lenguajes que no admiten sobrecarga (Objetivo C), incluya el nombre del tipo en el método -driveCar:(Car *)car.

    En algún momento posterior, es posible que lo necesite -driveMotorcycle:(Motorcycle *)motorcycle, pero nuevamente, no interferirá.

Esto permite drive(Car car)estar cerrado a modificaciones, pero está abierto a extenderse a otros tipos de vehículos. Esta planificación futura minimalista que le permite realizar el trabajo hoy, pero le impide bloquearse en el futuro.

Tratar de imaginar los tipos más básicos que necesita puede conducir a una regresión infinita. Qué sucede cuando quieres conducir un Segue, una bicicleta o un Jumbo jet. ¿Cómo se construye un único tipo de resumen genérico que pueda dar cuenta de todos los dispositivos en los que las personas entran y usan para la movilidad?

Jeffery Thomas
fuente
La modificación de una clase para agregar su nuevo método viola el Principio Abierto-Cerrado. Su sugerencia también elimina la capacidad de aplicar el Principio de sustitución de Liskov a todos los vehículos que pueden conducir, lo que esencialmente elimina una de las partes más fuertes de OO.
Dunk
@Dunk Basé mi respuesta en el principio abierto / cerrado polimórfico, no en el estricto principio abierto / cerrado de Meyer. Está permitido actualizar las clases para admitir nuevas interfaces. En este ejemplo, la interfaz del automóvil se mantiene separada de la interfaz de la motocicleta. Estos podrían formalizarse como clases abstractas de conducción separadas para automóviles y motocicletas que la clase implementadora podría apoyar.
Jeffery Thomas
@Dunk El Principio de sustitución de Liskov es útil, pero no es gratis. Si la especificación original solo requiere un automóvil, entonces crear un vehículo más genérico puede no valer el costo adicional de dinero, tiempo y complejidad. Además, es poco probable que un vehículo más genérico sea perfectamente adecuado para manejar subclases no planificadas. O bien, la interfaz para una motocicleta tendrá que ser calzada en la interfaz del vehículo (que fue diseñada para manejar solo el automóvil), o deberá modificar el vehículo para manejar la motocicleta (una violación real de abrir / cerrar).
Jeffery Thomas
El principio de sustitución de Liskov no es gratuito, pero tampoco tiene un costo elevado. Y por lo general, paga mucho más de lo que cuesta muchas veces, incluso si otra subclase nunca se hereda de ella en la aplicación principal. La aplicación de LSP hace que las pruebas automatizadas sean mucho más fáciles, lo cual ya es una victoria. Además, si bien no debería volverse loco y asumir que todo necesitará LSP, si está creando una aplicación y no tiene una buena idea de lo que es probable que la necesite en una futura revisión, entonces no saber lo suficiente sobre su aplicación o su dominio.
Dunk
1
Con respecto a la definición de OCP. Puede que sean las industrias en las que he trabajado, que tienden a requerir niveles de verificación más altos que solo una empresa comercial ordinaria, pero en general, si un archivo / clase cambia, no solo necesita volver a probar el archivo / clase sino todo lo que hace uso de ese archivo / clase en su prueba de regresión. Por lo tanto, no importa si alguien dice que abrir / cerrar polimórficos está bien, cambiar la interfaz tiene amplias consecuencias, por lo que no está tan bien.
Dunk
2

Entiendo la intención del principio abierto-cerrado. Está destinado a reducir el riesgo de romper algo que ya funciona mientras se modifica, al decirle que intente extender sin modificar.

También se trata de no romper todos los objetos que dependen de ese método al no cambiar el comportamiento de los objetos ya existentes. Una vez que un objeto ha anunciado un cambio de comportamiento, es arriesgado ya que está alterando el comportamiento conocido y esperado del objeto sin saber exactamente qué otros objetos esperan ese comportamiento.

Qué significa eso? ¿Debo subclasificar la clase existente en lugar de simplemente cambiar su código existente?

Sip.

"Solo acepta tarjetas de crédito" se define como parte del comportamiento de esa clase, a través de su interfaz pública. El programador ha declarado al mundo que el método de este objeto solo acepta tarjetas de crédito. Lo hizo usando un nombre de método no particularmente claro, pero está hecho. El resto del sistema depende de esto.

Eso podría haber tenido sentido en ese momento, pero ahora si necesita cambiar ahora, debe hacer una nueva clase que acepte otras cosas que no sean tarjetas de crédito.

Nuevo comportamiento = nueva clase

Como comentario aparte , una buena manera de predecir el futuro es pensar en el nombre que le has dado a un método. ¿Le ha dado un nombre de método de sonido realmente general como makePayment a un método con reglas específicas en el método en cuanto a qué pago puede hacer exactamente? Ese es un código de olor. Si tiene reglas específicas, esto debe aclararse a partir del nombre del método: makePayment debe ser makeCreditCardPayment. Haga esto cuando esté escribiendo el objeto por primera vez y otros programadores se lo agradecerán.

Cormac Mulhall
fuente