Principio de Open Closed en los patrones de diseño

7

Estoy un poco confundido acerca de cómo se puede aplicar el principio de Open Closed en la vida real. Requisito en cualquier cambio de negocio a lo largo del tiempo. De acuerdo con el principio Open-Closed, debe ampliar la clase en lugar de modificar la clase existente. Para mí, cada vez que extiende una clase no parece ser práctico para cumplir con el requisito. Déjame dar un ejemplo con el sistema de reservas de trenes.

En el sistema de reservas de trenes, habrá un objeto Boleto. Podría haber diferentes tipos de boletos Boleto Regular, Boleto de Concesión, etc. El boleto es de clase abstracta y el Boleto Regular y el Boleto de Concesión son clases concretas. Dado que todos los tickets tendrán el método PrintTicket que es común, por lo tanto, está escrito en Ticket, que es la clase abstracta básica. Digamos que esto funcionó bien durante unos meses. Ahora llega un nuevo requisito, que establece cambiar el formato del ticket. Es posible que se agreguen algunos campos más en el boleto impreso o que se cambie el formato. Para cumplir con este requisito tengo las siguientes opciones

  1. Modifique el método PrintTicket () en la clase abstracta del ticket. Pero esto violará el principio Open-Closed.
  2. Anule el método PrintTicket () en clases secundarias, pero esto duplicará la lógica de impresión que es una violación del principio DRY (no se repita)

Entonces las preguntas son

  1. ¿Cómo puedo satisfacer los requisitos comerciales anteriores sin violar el principio de Abierto / Cerrado?
  2. ¿Cuándo se supone que la clase está cerrada por modificación? ¿Cuáles son los criterios para considerar que la clase está cerrada por modificación? Es después de la implementación inicial de la clase o puede ser después del primer despliegue en producción o puede ser otra cosa.
pregunta parag 14.04.2016 - 16:13

5 respuestas

5
  

Tengo las siguientes opciones

     
  • Modifique el método PrintTicket() en la clase abstracta del ticket. Pero esto violará el principio Open-Closed.
  •   
  • Anule el método PrintTicket() en las clases secundarias, pero esto duplicará la lógica de impresión, lo cual es una violación del principio DRY (No se repita).
  •   

Esto depende de la forma en que se implementa el PrintTicket . Si el método necesita tener en cuenta la información de las subclases, debe proporcionar una forma para que proporcionen información adicional.

Además, puede anular sin repetir su código también. Por ejemplo, si llama a su método de clase base desde la implementación, evita la repetición:

class ConcessionTicket : Ticket {
    public override string PrintTicket() {
        return $"{base.PrintTicket()} (concession)"
    }
}
  

¿Cómo puedo satisfacer los requisitos comerciales anteriores sin violar el principio de apertura / cierre?

El patrón

Método de plantilla proporciona una tercera opción: implementar PrintTicket en la clase base y confíe en las clases derivadas para proporcionar detalles adicionales según sea necesario.

Aquí hay un ejemplo usando tu jerarquía de clases:

abstract class Ticket {
    public string Name {get;}
    public string Train {get;}
    protected virtual string AdditionalDetails() {
        return "";
    }
    public string PrintTicket() {
        return $"{Name} : {Train}{AdditionalDetails()}";
    }
}

class RegularTicket : Ticket {
    ... // Uses the default implementation of AdditionalDetails()
}


class ConcessionTicket : Ticket {
    protected override string AdditionalDetails() {
        return " (concession)";
    }
}
  

¿Cuáles son los criterios para considerar que la clase está cerrada por modificación?

No es la clase la que debe cerrarse a la modificación, sino la interfaz de esa clase (me refiero a "interfaz" en sentido amplio, es decir, una colección de métodos y propiedades y su comportamiento, no la construcción del lenguaje). La clase oculta su implementación, por lo que los propietarios de la clase pueden modificarla en cualquier momento, siempre que su comportamiento visible externamente permanezca sin cambios.

  

¿Es después de la implementación inicial de la clase o puede ser después del primer despliegue en producción o puede ser otra cosa?

La interfaz de la clase debe permanecer cerrada después de la primera vez que la publique para uso externo. Las clases de uso interno permanecen abiertas para refactorizar para siempre, porque puedes encontrar y reparar todos sus usos.

Aparte de los casos más simples, no es práctico esperar que su jerarquía de clases cubra todos los posibles escenarios de uso después de un cierto número de iteraciones de refactorización. Los requisitos adicionales que requieren métodos completamente nuevos en la clase base aparecen regularmente, por lo que su clase permanece abierta a modificaciones por usted para siempre.

    
respondido por el dasblinkenlight 14.04.2016 - 16:25
2

Comencemos con una línea simple de principio Abierto-cerrado - "Open for extension Closed for modification" . Considere su problema. Diseñaría las clases para que el Boleto base sea responsable de imprimir una información común del Boleto y otros tipos de Boletos sean responsables de imprimir su propio formato en la parte superior de la impresión base.

abstract class Ticket
{
    public string Print()
    {
        // Print base properties here. and return 
        return PrintBasicInfo() + AddionalPrint();
    }

    protected abstract string AddionalPrint();

    private string PrintBasicInfo()
    {
        // print base ticket info
    }
}

class ConcessionTicket : Ticket
{
    protected override string AddionalPrint()
    {
        // Specific to Conecession ticket printing
    }
}

class RegularTicket : Ticket
{
    protected override string AddionalPrint()
    {
        // Specific to Regular ticket printing
    }
}

De esta manera, está aplicando cualquier nuevo tipo de ticket que se introduciría en el sistema implementará su propia función de impresión sobre la información básica de impresión de tickets. El ticket base ahora está cerrado por modificación, pero abierto por extensión al proporcionar un método adicional para sus tipos derivados.

    
respondido por el vendettamit 14.04.2016 - 16:41
2

El problema no es que esté violando el principio de apertura / cierre, el problema es que está violando el principio de responsabilidad única.

Esto es, literalmente, un ejemplo de libro escolar de un problema de SRP, o como wikipedia indica:

  

Como ejemplo, considere un módulo que compila e imprime un informe. Imagina que un módulo de este tipo se puede cambiar por dos razones. Primero, el contenido del informe podría cambiar. En segundo lugar, el formato del informe podría cambiar. Estas dos cosas cambian por causas muy diferentes; Una sustantiva, y una cosmética. El principio de responsabilidad única dice que estos dos aspectos del problema son en realidad dos responsabilidades separadas y, por lo tanto, deberían estar en clases o módulos separados. Sería un mal diseño juntar dos cosas que cambian por diferentes razones en diferentes momentos.

Las diferentes clases de Entradas pueden cambiar porque les agrega nueva información. La impresión del ticket puede cambiar porque necesita un nuevo diseño, por lo tanto, debe tener una clase TicketPrinter que acepte tickets como entrada.

Sus clases de Ticket pueden implementar diferentes interfaces para proporcionar diferentes tipos de datos o proporcionar algún tipo de plantilla de datos.

    
respondido por el Bjorn 23.05.2016 - 21:58
1
  

Modifique el método PrintTicket () en la clase abstracta del ticket. Pero esto violará el principio de Open-Closed.

Esto no viola el principal abierto-cerrado. El código cambia todo el tiempo, por eso SOLID es importante, por lo que el código se mantiene, es flexible y se puede cambiar. No hay nada de malo en cambiar el código.

OCP es más acerca de las clases externas que no pueden manipular la funcionalidad deseada de una clase.

Por ejemplo, tengo una clase A que toma una cadena en el constructor, y verifica que esta cadena tiene un cierto formato al llamar a un método para verificar la cadena. Este método de verificación también debe ser utilizado por los clientes de A , por lo que en la implementación ingenua, se realiza public .

Ahora puedo "romper" esta clase heredando de ella y anulando el método de verificación para devolver siempre true . Debido al polimorfismo, todavía puedo usar mi subclase en cualquier lugar donde pueda usar A , posiblemente creando un comportamiento no deseado en mi programa.

Esto parece ser una cosa maliciosa silenciosa, pero en una base de código más compleja, podría hacerse como un 'error honesto'. Para cumplir con el OCP, la clase A debe diseñarse de tal manera que no sea posible cometer este error.

Podría hacer esto, por ejemplo, haciendo que el método de verificación final .

    
respondido por el Jorn Vernee 14.04.2016 - 16:42
0

La herencia no es el único mecanismo que se puede usar para cumplir los requisitos de OCP, y en la mayoría de los casos diría que rara vez es el mejor, por lo general hay mejores formas, siempre y cuando se planifique con anticipación. para considerar el tipo de cambios que es probable que necesite.

En este caso, argumentaría que si espera obtener cambios frecuentes de formato en su sistema de impresión de tickets (y eso me parece una apuesta bastante segura), utilizar un sistema de plantillas tendría mucho sentido. Ahora, no necesitamos tocar el código en absoluto: todo lo que tenemos que hacer para cumplir con esta solicitud es cambiar una plantilla (siempre que su sistema de plantillas pueda acceder a todos los datos que se requieren, de todos modos).

En realidad, un sistema nunca puede cerrarse por completo contra modificaciones, e incluso lograrlo requiere una gran cantidad de esfuerzo desperdiciado, por lo que debe hacer juicios sobre qué tipo de cambios son posibles, y estructurar su código en consecuencia .

    
respondido por el Jules 20.05.2016 - 01:46

Lea otras preguntas en las etiquetas