¿Aprovechas los beneficios del principio abierto-cerrado?

12

El principio abierto-cerrado (OCP) establece que un objeto debe estar abierto para la extensión pero cerrado para la modificación. Creo que lo entiendo y lo uso junto con SRP para crear clases que solo hacen una cosa. Y trato de crear muchos métodos pequeños que permitan extraer todos los controles de comportamiento en métodos que pueden ampliarse o anularse en alguna subclase. Por lo tanto, termino con clases que tienen muchos puntos de extensión, ya sea a través de: inyección de dependencia y composición, eventos, delegación, etc.

Considere la siguiente clase simple y extensible:

class PaycheckCalculator {
    // ...
    protected decimal GetOvertimeFactor() { return 2.0M; }
}

Ahora digamos, por ejemplo, que los OvertimeFactorcambios a 1.5. Como la clase anterior se diseñó para ampliarse, puedo subclasificar fácilmente y devolver una diferente OvertimeFactor.

Pero ... a pesar de que la clase está diseñada para la extensión y se adhiere a OCP, modificaré el método único en cuestión, en lugar de subclasificar y anular el método en cuestión y luego volver a cablear mis objetos en mi contenedor de IoC.

Como resultado, violé parte de lo que OCP intenta lograr. Parece que solo estoy siendo flojo porque lo anterior es un poco más fácil. ¿Estoy malentendido OCP? ¿Realmente debería estar haciendo algo diferente? ¿Aprovecha los beneficios de OCP de manera diferente?

Actualización : según las respuestas, parece que este ejemplo artificial es pobre por varias razones diferentes. La intención principal del ejemplo fue demostrar que la clase fue diseñada para extenderse al proporcionar métodos que, cuando se reemplazan, alterarían el comportamiento de los métodos públicos sin la necesidad de cambiar el código interno o privado. Aún así, definitivamente entendí mal el OCP.

Kaleb Pederson
fuente

Respuestas:

10

Si está modificando la clase base, entonces no está realmente cerrada, ¿verdad?

Piense en la situación en la que ha lanzado la biblioteca al mundo. Si va y cambia el comportamiento de su clase base modificando el factor de tiempo extra a 1.5, entonces ha violado a todas las personas que usan su código suponiendo que la clase estaba cerrada.

¿Realmente para hacer que la clase sea cerrada pero abierta, debería recuperar el factor de tiempo extra de una fuente alternativa (tal vez un archivo de configuración) o probar un método virtual que se puede anular?

Si la clase estaba realmente cerrada, después de su cambio, no fallarían los casos de prueba (suponiendo que tenga una cobertura del 100% con todos sus casos de prueba) y supondría que hay un caso de prueba que verifica GetOvertimeFactor() == 2.0M.

No más ingeniero

Pero no tome este principio de apertura cerrada a la conclusión lógica y tenga todo configurable desde el principio (eso es sobre ingeniería). Solo defina los bits que necesita actualmente.

El principio cerrado no le impide volver a diseñar el objeto. Simplemente le impide cambiar la interfaz pública actualmente definida a su objeto ( los miembros protegidos son parte de la interfaz pública). Todavía puede agregar más funcionalidad siempre que la funcionalidad anterior no se rompa.

Martin York
fuente
"El principio cerrado no le impide volver a diseñar el objeto". En realidad lo hace . Si lees el libro donde se propuso por primera vez el Principio de Open-Closed, o el artículo que introdujo el acrónimo "OCP", verás que dice que "Nadie puede hacer cambios en el código fuente" (excepto el error arreglos).
Rogério
@ Rogério: Eso puede ser cierto (en 1988). Pero la definición actual (popularizada en 1990 cuando OO se hizo popular) se trata de mantener una interfaz pública consistente. During the 1990s, the open/closed principle became popularly redefined to refer to the use of abstracted interfaces, where the implementations can be changed and multiple implementations could be created and polymorphically substituted for each other. en.wikipedia.org/wiki/Open/closed_principle
Martin York
Gracias por la referencia de Wikipedia. Pero no estoy seguro de que la definición "actual" sea realmente diferente, ya que todavía depende de la herencia de tipo (clase o interfaz). Y esa cita de "no hay cambios en el código fuente" que mencioné proviene del artículo OCP 1996 de Robert Martin que está (supuestamente) en línea con la "definición actual". Personalmente, creo que el Principio de Abierto-Cerrado ya estaría olvidado, si Martin no le hubiera dado un acrónimo que, aparentemente, tiene mucho valor de marketing. El principio en sí está desactualizado y es dañino, IMO
Rogério
3

Entonces, el Principio Abierto Cerrado es un problema ... especialmente si intentas aplicarlo al mismo tiempo que YAGNI . ¿Cómo me adhiero a ambos al mismo tiempo? Aplica la regla de tres . La primera vez que realice un cambio, hágalo directamente. Y la segunda vez también. La tercera vez, es hora de abstraer ese cambio.

Otro enfoque es "engañarme una vez ...", cuando tenga que hacer un cambio, aplique OCP para protegerse contra ese cambio en el futuro . Casi iría tan lejos como para proponer que cambiar la tasa de horas extra es una historia nueva. "Como administrador de la nómina, quiero cambiar la tasa de horas extra para poder cumplir con las leyes laborales aplicables". Ahora tiene una nueva interfaz de usuario para cambiar la tasa de horas extra, una forma de almacenarla, y GetOvertimeFactor () simplemente le pregunta a su repositorio cuál es la tasa de horas extra.

Michael Brown
fuente
2

En el ejemplo que ha publicado, el factor de tiempo extra debe ser una variable o una constante. * (Ejemplo de Java)

class PaycheckCalculator {
   float overtimeFactor;

   protected float setOvertimeFactor(float overtimeFactor) {
      this.overtimeFactor = overtimeFactor;
   }

   protected float getOvertimeFactor() {
      return overtimeFactor;
   }
}

O

class PaycheckCalculator {
   public static final float OVERTIME_FACTOR = 1.5f;
}

Luego, cuando extienda la clase, establezca o anule el factor. Los "números mágicos" solo deberían aparecer una vez. Esto es mucho más al estilo de OCP y DRY (No te repitas), porque no es necesario hacer una clase completamente nueva para un factor diferente si se usa el primer método, y solo tener que cambiar la constante de una manera idiomática. lugar en el segundo.

Usaría el primero en los casos en que habría múltiples tipos de calculadora, cada una necesitando diferentes valores constantes. Un ejemplo sería el patrón de la Cadena de responsabilidad, que generalmente se implementa utilizando tipos heredados. Un objeto que solo puede ver la interfaz (es decir getOvertimeFactor()) lo usa para obtener toda la información que necesita, mientras que los subtipos se preocupan por la información real que debe proporcionar.

El segundo es útil en casos en los que no es probable que se cambie la constante, pero se usa en múltiples ubicaciones. Tener una constante para cambiar (en el improbable caso de que lo haga) es mucho más fácil que configurarlo por todas partes o obtenerlo de un archivo de propiedades.

El principio abierto-cerrado es menos una llamada a no modificar el objeto existente que una advertencia para dejar la interfaz sin cambios. Si necesita un comportamiento ligeramente diferente de una clase, o funcionalidad adicional para un caso específico, extienda y anule. Pero si los requisitos para la clase misma cambian (como cambiar el factor), debe cambiar la clase. No tiene sentido una gran jerarquía de clases, la mayoría de las cuales nunca se usa.

Michael K
fuente
Este es un cambio de datos, no un cambio de código. La tasa de horas extra no debería haber sido codificada.
Jim C
Parece que tienes tu Get y tu Set al revés.
Mason Wheeler
Whoops! debería haber probado ...
Michael K
2

Realmente no veo su ejemplo como una gran representación de OCP. Creo que lo que realmente significa la regla es esto:

Cuando desee agregar una función, solo debería agregar una clase y no debería necesitar modificar ninguna otra clase (sino posiblemente un archivo de configuración).

Una mala implementación a continuación. Cada vez que agregue un juego, deberá modificar la clase GamePlayer.

class GamePlayer
{
   public void PlayGame(string game)
   {
      switch(game)
      {
          case "Poker":
              PlayPoker();
              break;

          case "Gin": 
              PlayGin();
              break;

          ...
      }
   }

   ...
}

La clase GamePlayer nunca debería necesitar ser modificada

class GamePlayer
{
    ...

    public void PlayGame(string game)
    {
        Game g = GameFactory.GetByName(game); 
        g.Play();   
    }

    ...
}

Ahora, suponiendo que mi GameFactory también cumpla con OCP, cuando quiera agregar otro juego, solo necesitaría construir una nueva clase que herede de la Gameclase y todo debería funcionar.

Con demasiada frecuencia, las clases como la primera se crean después de años de "extensiones" y nunca se refactorizaron correctamente desde la versión original (o peor, lo que deberían ser múltiples clases sigue siendo una gran clase).

El ejemplo que proporciona es OCP-ish. En mi opinión, la forma correcta de manejar los cambios en las tasas de horas extra sería en una base de datos con tasas históricas guardadas para que los datos puedan ser reprocesados. El código aún debe cerrarse para su modificación porque siempre cargaría el valor apropiado de la búsqueda.

Como ejemplo del mundo real, he usado una variante de mi ejemplo y el Principio Abierto-Cerrado realmente brilla. La funcionalidad es realmente fácil de agregar porque solo tengo que derivar de una clase base abstracta y mi "fábrica" ​​la recoge automáticamente y al "jugador" no le importa qué implementación concreta devuelve la fábrica.

Austin Salonen
fuente
1

En este ejemplo particular, tiene lo que se conoce como un "valor mágico". Esencialmente un valor codificado que puede o no cambiar con el tiempo. Intentaré abordar el enigma que expresas genéricamente, pero este es un ejemplo del tipo de cosas en las que crear una subclase es más trabajo que cambiar un valor en una clase.

Lo más probable es que haya especificado un comportamiento demasiado temprano en su jerarquía de clases.

Digamos que tenemos el PaycheckCalculator. Lo OvertimeFactormás probable es que se elimine la información sobre el empleado. Un empleado por hora puede disfrutar de un bono de horas extra, mientras que a un empleado asalariado no se le pagaría nada. Aún así, algunos empleados asalariados tendrán tiempo directo debido al contrato en el que estaban trabajando. Puede decidir que hay ciertas clases conocidas de escenarios de pago, y así es como construiría su lógica.

En la PaycheckCalculatorclase base , lo hace abstracto y especifica los métodos que espera. Los cálculos básicos son los mismos, es solo que ciertos factores se calculan de manera diferente. Su HourlyPaycheckCalculatorentonces poner en práctica el getOvertimeFactormétodo y volver 1.5 o 2.0 como su caso. Su StraightTimePaycheckCalculatorimplementaría el getOvertimeFactorpara devolver 1.0. Finalmente, una tercera implementación sería una NoOvertimePaycheckCalculatorque implementaría el getOvertimeFactorpara devolver 0.

La clave es describir solo el comportamiento en la clase base que se pretende extender. Los detalles de las partes del algoritmo general o los valores específicos serían completados por subclases. El hecho de que haya incluido un valor predeterminado para los getOvertimeFactorcables conduce a la "corrección" rápida y fácil de la línea en lugar de extender la clase como lo deseaba. También destaca el hecho de que hay un esfuerzo involucrado con la extensión de las clases. También hay un esfuerzo involucrado en comprender la jerarquía de clases en su aplicación. Desea diseñar sus clases de tal manera que minimice la necesidad de crear subclases y, a la vez, proporcione la flexibilidad que necesita.

Para reflexionar: cuando nuestras clases encapsulan ciertos factores de datos como OvertimeFactoren su ejemplo, es posible que necesite una forma de extraer esa información de otra fuente. Por ejemplo, un archivo de propiedades (ya que esto se parece a Java) o una base de datos contendría el valor, y PaycheckCalculatorusaría un objeto de acceso a datos para extraer sus valores. Esto permite que las personas adecuadas cambien el comportamiento del sistema sin necesidad de reescribir el código.

Berin Loritsch
fuente