Principio abierto cerrado en patrones de diseño

8

Estoy un poco confundido acerca de cómo se puede aplicar el principio de Open Closed en la vida real. Requisito en cualquier negocio cambia con el tiempo. De acuerdo con el principio Open-Closed, debe extender la clase modificando la clase existente. Para mí, cada vez que extender una clase no parece ser práctico para cumplir con el requisito. Permítanme dar un ejemplo con el sistema de reserva de trenes.

En el sistema de reserva de trenes habrá un objeto Boleto. Podría haber diferentes tipos de boletos Boleto regular, Boleto de concesión, etc. Boleto es clase abstracta y Boletos regulares y Boletos de concesión son clases concretas. Como 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 base. Digamos que esto funcionó bien durante unos meses. Ahora entra un nuevo requisito, que establece cambiar el formato del ticket. Es posible que se agreguen algunos campos más en el ticket impreso o que se cambie el formato. Para cumplir este requisito, tengo las siguientes opciones

  1. Modifique el método PrintTicket () en la clase abstracta Ticket. Pero esto violará el principio de Abierto-Cerrado.
  2. Anule el método PrintTicket () en las 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 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 de la primera implementación en producción o puede ser otra cosa.
parag
fuente
1
Supongo que el problema aquí es que su clase de ticket no debería saber cómo imprimirse. Una clase TicketPrinter debe recibir un ticket y saber cómo imprimirlo. En este caso, puede usar el decotator patern para incrementar su clase de impresora o anularlo para cambiarlo por completo.
1
Decorador también enfrentará el mismo problema de Abrir / Cerrado. El requisito puede cambiar muchas veces en el futuro. En ese caso, cambiará el decorador o extenderá el decorador. Ampliar el decorador cada vez no parece ser práctico y modificar el decorador violará el principio abierto / cerrado.
parag
1
Es muy difícil diseñar con OCP para cambios arbitrarios en el diseño. Es una apuesta para elegir la dimensión que espera variar con respecto a la parte que es estable.
Fuhrmanator

Respuestas:

5

Tengo las siguientes opciones

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

Esto depende de la forma en que PrintTicketse implementa. Si el método necesita considerar 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 Abierto / Cerrado?

El patrón de Método de plantilla proporciona una tercera opción: implementarPrintTicketen la clase base y confiar en clases derivadas para proporcionar detalles adicionales según sea necesario.

Aquí hay un ejemplo usando su 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 más bien 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 y cuando su comportamiento externo visible permanezca sin cambios.

¿Es después de la implementación inicial de la clase o puede ser después de la primera implementación en producción o puede ser algo más?

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

Además de los casos más simples, no es práctico esperar que su jerarquía de clases cubra todos los escenarios de uso posibles 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 siempre.

dasblinkenlight
fuente
Sí, en este caso particular, la plantilla funcionará. Pero este es solo un ejemplo para poner el problema. Podría haber muchos escenarios / problemas comerciales en los que el método de plantillas / complementos puede no ser aplicable.
parag
1
Su punto en la interfaz de clase es, en mi opinión, la mejor resolución para este escenario particular. Para enfocarlo en la pregunta específica: uno podría tener un método PrintTicket con una salida especificada definida en la interfaz para la clase Ticket (junto con entradas definidas) y siempre que la salida del ticket sea siempre la misma, no importaría cómo las clases subyacentes cambiaron. - Además, si siempre se espera que las propiedades de un boleto cambien, ¿por qué no diseñar su clase con eso como un requisito comercial conocido? El ticket debe contener una colección de un objeto ticketProperty de algún tipo
user3908435 el
También podría hacer una TicketPrinterclase que se pasa al constructor.
Zymus
2

Comencemos con una línea simple de principio abierto-cerrado "Open for extension Closed for modification". Considere su problema. Yo diseñaría las clases para que el Ticket base sea responsable de imprimir una información de Ticket común y otros tipos de Ticket sean responsables de imprimir su propio formato sobre 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, si aplica cualquier tipo de boleto nuevo que se introduciría en el sistema, implementará su propia función de impresión además de la información básica de impresión del boleto. Base Ticket ahora está cerrado para modificación pero abierto para extensión al proporcionar un método adicional para sus tipos derivados.

vendettamit
fuente
2

El problema no es que esté violando el principio Abierto / Cerrado, sino que está violando el Principio de responsabilidad única.

Este es literalmente un ejemplo de un libro escolar de un problema de SRP, o como dice Wikipedia :

Como ejemplo, considere un módulo que compila e imprime un informe. Imagine que dicho módulo 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; uno sustantivo y otro cosmético. El principio de responsabilidad única dice que estos dos aspectos del problema son realmente dos responsabilidades separadas y, por lo tanto, deben estar en clases o módulos separados. Sería un mal diseño unir dos cosas que cambian por diferentes razones en diferentes momentos.

Las diferentes clases de Ticket 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 Ticket pueden implementar diferentes interfaces para proporcionar diferentes tipos de datos o proporcionar algún tipo de plantilla de datos.

Bjorn
fuente
1

Modifique el método PrintTicket () en la clase abstracta Ticket. Pero esto violará el principio de Abierto-Cerrado.

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 sigue siendo mantenible, flexible y cambiante. No hay nada de malo en cambiar el código.


OCP trata más sobre las clases externas que no pueden alterar la funcionalidad prevista de una clase.

Por ejemplo, tengo una clase Aque toma una Cadena en el constructor y verifica que esta Cadena tenga un cierto formato llamando 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, está hecho public.

Ahora puedo 'romper' esta clase heredando de ella y anulando el método de verificación para que siempre regrese 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 silencioso, algo malicioso que hacer, pero en una base de código más compleja, podría hacerse como un "error honesto". Para cumplir con OCP, la clase Adebe diseñarse de tal manera que no sea posible cometer este error.

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

Jorn Vernee
fuente
Nunca he visto una descripción de OCP que indique que esto es una violación de la misma. LSP, definitivamente, pero no OCP.
Julio
@Jules Más tarde descubrí que la definición de wikipedia es muy diferente de esta. Sin embargo, este fue uno de los ejemplos utilizados para enseñarme OCP (aunque podría haber sido ligeramente diferente). El punto es que no se trataba de prohibirte cambiar tu código. No tiene sentido para mí tampoco. Si escribe código y el entorno cambia para que su código ya no se ajuste, cámbielo o, mejor aún, deséchelo y comience de nuevo. ¿Por qué mantener algo roto? ...
Jorn Vernee
0

La herencia no es el único mecanismo que se puede utilizar para cumplir con 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 que planifique con anticipación un poco para considerar el tipo de cambios que probablemente 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), usar 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 cerrarlo requeriría una gran cantidad de esfuerzo desperdiciado, por lo que debe hacer juicios sobre qué tipo de cambios son probables y estructurar su código en consecuencia.

Jules
fuente