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:
Antes: programe abstracciones y "prediga el futuro" tanto como pueda. Por ejemplo, un método
drive(Car car)
tendrá que cambiar siMotorcycle
se agregan s al sistema en el futuro, por lo que probablemente viola OCP. Perodrive(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.
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 CreditCard
objetos: makePayment(CraditCard card)
. Un día Voucher
s 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)?
fuente
Respuestas:
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.
fuente
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?
fuente
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.
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.
fuente