Princípio Aberto Fechado nos padrões de design

8

Estou um pouco confuso sobre como o princípio Aberto Fechado pode ser aplicado na vida real. Exigência em qualquer negócio muda ao longo do tempo. De acordo com o princípio Aberto-Fechado, você deve estender a classe, em vez de modificar a classe existente. Para mim, toda vez que estender uma aula não parece prático para cumprir o requisito. Deixe-me dar um exemplo com o sistema de reservas de trens.

No sistema de reservas de trens, haverá um objeto Ticket. Pode haver diferentes tipos de bilhetes Ticket Regular, Ticket de Concessão etc. Ticket é da classe abstrata e RegularTicket e ConcessionTickets são classes concretas. Como todos os tickets terão o método PrintTicket, que é comum, portanto, ele é escrito em Ticket, que é a classe abstrata base. Digamos que isso funcionou bem por alguns meses. Agora chega um novo requisito, que declara alterar o formato do ticket. Podem ser adicionados mais alguns campos ao ticket impresso ou o formato pode ser alterado. Para cumprir este requisito, tenho as seguintes opções

  1. Modifique o método PrintTicket () na classe abstrata Ticket. Mas isso violará o princípio Aberto-Fechado.
  2. Substitua o método PrintTicket () nas classes filho, mas isso duplicará a lógica de impressão, que é uma violação do princípio DRY (não se repita).

Então as perguntas são

  1. Como posso satisfazer os requisitos comerciais acima sem violar o princípio Aberto / Fechado.
  2. Quando a classe deve ser fechada para modificação? Quais são os critérios para considerar que a classe está fechada para modificação? É após a implementação inicial da classe ou pode ser após a primeira implantação na produção ou pode ser outra coisa.
parag
fonte
1
Acho que o problema aqui é que a sua classe de ticket não deve saber como se imprimir. Uma classe TicketPrinter deve receber um ticket e saber como imprimi-lo. Nesse caso, você pode usar o padrão do decotador para incrementar sua classe de impressora ou substituí-la para alterá-la completamente.
1
O decorador também enfrentará o mesmo problema de aberto / fechado. O requisito pode mudar muitas vezes no futuro. Nesse caso, você mudará o decorador ou estenderá o decorador. A extensão do decorador toda vez não parece prático e a modificação do decorador violará o princípio de abrir / fechar.
parag
1
É muito difícil projetar com o OCP para alterações arbitrárias no design. É uma aposta escolher a dimensão que você espera variar em relação à parte que é estável.
Fuhrmanator

Respostas:

5

Eu tenho as seguintes opções

  • Modifique o PrintTicket()método na classe abstrata Ticket. Mas isso violará o princípio Aberto-Fechado.
  • Substitua o PrintTicket()método nas classes filho, mas isso duplicará a lógica de impressão, que é uma violação do princípio DRY (Não se repita).

Isso depende da maneira como o PrintTicketé implementado. Se o método precisar considerar informações de subclasses, precisará fornecer uma maneira de fornecer informações adicionais.

Além disso, você pode substituir sem repetir seu código também. Por exemplo, se você chamar seu método de classe base a partir da implementação, evite repetições:

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

Como posso satisfazer os requisitos comerciais acima sem violar o princípio Aberto / Fechado?

O padrão Template Method fornece uma terceira opção: implementarPrintTicketna classe base e confiar nas classes derivadas para fornecer detalhes adicionais conforme necessário.

Aqui está um exemplo usando sua hierarquia de classes:

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)";
    }
}

Quais são os critérios para considerar que a classe está fechada para modificação?

Não é a classe que deve ser fechada para modificação, mas a interface dessa classe (quero dizer "interface" no sentido amplo, isto é, uma coleção de métodos e propriedades e seu comportamento, não a construção da linguagem). A classe oculta sua implementação, para que seus proprietários possam modificá-la a qualquer momento, desde que seu comportamento visível externamente permaneça inalterado.

É após a implementação inicial da classe ou pode ser após a primeira implantação na produção ou pode ser outra coisa?

A interface da classe precisa permanecer fechada após a primeira vez que você a publica para uso externo. As classes de uso interno permanecem abertas à refatoração para sempre, porque você pode encontrar e corrigir todos os seus usos.

Além dos casos mais simples, não é prático esperar que sua hierarquia de classes cubra todos os cenários de uso possíveis após um certo número de iterações de refatoração. Os requisitos adicionais que exigem métodos inteiramente novos na classe base surgem regularmente, para que sua classe fique sempre aberta a modificações por você.

dasblinkenlight
fonte
Sim, neste caso em particular, o modelo funcionará. Mas este é apenas um exemplo para colocar o problema. Pode haver muitos cenários / problemas de negócios nos quais o método de modelo / plugin pode não ser aplicável.
parag
1
O seu ponto na interface da classe é, na minha opinião, a melhor resolução para esse cenário em particular. Para focar na questão específica: é possível ter um método PrintTicket com uma saída especificada definida na interface para a classe Ticket (junto com entradas definidas), desde que a saída do ticket seja sempre a mesma, não importaria como classes subjacentes alteradas. - Além disso, se sempre se espera que as propriedades de um ticket sejam alteradas, por que não projetar sua classe com isso como um requisito comercial conhecido. Ticket deve conter uma coleção de um objeto ticketProperty de algum tipo
user3908435
Também pode criar uma TicketPrinterclasse que é passada para o construtor.
Zymus 19/05
2

Vamos começar com uma linha simples de princípio Aberto-fechado - "Open for extension Closed for modification". Considere o seu problema: eu projetaria as classes para que o Ticket base fosse responsável por imprimir uma informação comum do Ticket e outros tipos de Ticket fossem responsáveis ​​pela impressão de seu próprio formato na parte superior da impressão 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
    }
}

Dessa forma, você está aplicando qualquer novo tipo de ticket que seria introduzido no sistema implementará seu próprio recurso de impressão sobre as informações básicas de impressão de tickets. Agora, o ticket base está fechado para modificação, mas aberto para extensão, fornecendo um método adicional para os tipos derivados.

vendettamit
fonte
2

O problema não é que você está violando o princípio de Aberto / Fechado, o problema é que você está violando o Princípio de Responsabilidade Única.

Este é literalmente um exemplo de livro escolar de um problema de SRP, ou como a Wikipedia diz:

Como exemplo, considere um módulo que compila e imprime um relatório. Imagine que esse módulo possa ser alterado por dois motivos. Primeiro, o conteúdo do relatório pode mudar. Segundo, o formato do relatório pode mudar. Essas duas coisas mudam por causas muito diferentes; um substantivo e um cosmético. O princípio da responsabilidade única diz que esses dois aspectos do problema são realmente duas responsabilidades separadas e, portanto, devem estar em classes ou módulos separados. Seria um péssimo design juntar duas coisas que mudam por razões diferentes em momentos diferentes.

As diferentes classes de ticket podem mudar porque você adiciona novas informações a elas. A impressão do ticket pode mudar porque você precisa de um novo layout, portanto, você deve ter uma classe TicketPrinter que aceite tickets como entrada.

Suas classes de ticket podem implementar interfaces diferentes para fornecer diferentes tipos de dados ou fornecer algum tipo de modelo de dados.

Bjorn
fonte
1

Modifique o método PrintTicket () na classe abstrata Ticket. Mas isso violará o princípio Aberto-Fechado.

Isso não viola o principal aberto-fechado. O código muda o tempo todo, é por isso que o SOLID é importante, para que o código permaneça sustentável, flexível e mutável. Não há nada errado em alterar o código.


OCP é mais sobre classes externas que não podem violar a funcionalidade pretendida de uma classe.

Por exemplo, eu tenho uma classe Aque pega uma String no construtor e verifica se essa String tem um determinado formato chamando um método para verificar a string. Esse método de verificação também precisa ser utilizável pelos clientes A, portanto, na implementação ingênua, é feito public.

Agora eu posso 'quebrar' essa classe herdando dela e substituindo o método de verificação para sempre retornar sempre true. Devido ao polimorfismo, ainda posso usar minha subclasse em qualquer lugar em que possa usá-lo A, possivelmente criando um comportamento indesejado no meu programa.

Parece uma coisa maliciosa silenciosa, mas em uma base de código mais complexa, isso pode ser feito como um "erro honesto". Para estar em conformidade com o OCP, a classe Adeve ser projetada de forma que não seja possível cometer esse erro.

Eu poderia fazer isso, por exemplo, criando o método de verificação final.

Jorn Vernee
fonte
Nunca vi uma descrição do OCP que afirmasse que isso é uma violação. LSP, definitivamente, mas não OCP.
Jules
@Jules Descobri mais tarde que a definição da Wikipedia é realmente muito diferente disso. Este foi um dos exemplos usados ​​para me ensinar OCP (embora possa ter sido um pouco diferente). O ponto é que não se tratava de se proibir de alterar seu código. Também não faz sentido para mim. Se você escrever um código e o ambiente mudar para que seu código não caiba mais, altere-o ou melhor, jogue-o fora e comece novamente. Por que manter algo que está quebrado ...
Jorn Vernee
0

A herança não é o único mecanismo que pode ser usado para atender aos requisitos do OCP e, na maioria dos casos, eu argumentaria que raramente é o melhor - geralmente existem maneiras melhores, desde que você planeje um pouco para considerar o tipo de alterações que você provavelmente precisará.

Nesse caso, eu argumentaria que, se você espera obter alterações frequentes de formato no seu sistema de impressão de ingressos (e isso me parece uma aposta bastante segura), o uso de um sistema de modelos faria muito sentido. Agora, não precisamos tocar no código: tudo o que precisamos fazer para atender a essa solicitação é alterar um modelo (desde que seu sistema de modelos possa acessar todos os dados necessários).

Na realidade, um sistema nunca pode ser totalmente fechado contra modificações, e até mesmo chegar perto disso exigiria uma enorme quantidade de esforço desperdiçado; portanto, você deve fazer julgamentos sobre que tipo de mudanças são prováveis ​​e estruturar seu código de acordo.

Jules
fonte