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 OvertimeFactor
cambios 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.
fuente
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_principleEntonces, 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.
fuente
En el ejemplo que ha publicado, el factor de tiempo extra debe ser una variable o una constante. * (Ejemplo de Java)
O
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.
fuente
Realmente no veo su ejemplo como una gran representación de OCP. Creo que lo que realmente significa la regla es esto:
Una mala implementación a continuación. Cada vez que agregue un juego, deberá modificar la clase GamePlayer.
La clase GamePlayer nunca debería necesitar ser modificada
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
Game
clase 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.
fuente
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.
Digamos que tenemos el
PaycheckCalculator
. LoOvertimeFactor
má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
PaycheckCalculator
clase 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. SuHourlyPaycheckCalculator
entonces poner en práctica elgetOvertimeFactor
método y volver 1.5 o 2.0 como su caso. SuStraightTimePaycheckCalculator
implementaría elgetOvertimeFactor
para devolver 1.0. Finalmente, una tercera implementación sería unaNoOvertimePaycheckCalculator
que implementaría elgetOvertimeFactor
para 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
getOvertimeFactor
cables 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
OvertimeFactor
en 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, yPaycheckCalculator
usarí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.fuente