Como aderir ao princípio aberto-fechado na prática

14

Entendo a intenção do princípio aberto-fechado. Ele visa reduzir o risco de quebrar algo que já funciona ao modificá-lo, solicitando que você tente estender sem modificar.

No entanto, tive alguns problemas para entender como esse princípio é aplicado na prática. No meu entender, existem duas maneiras de aplicá-lo. Antes e depois de uma possível mudança:

  1. Antes: programe para abstrações e 'preveja o futuro' o máximo que puder. Por exemplo, um método drive(Car car)precisará ser alterado se Motorcycles forem adicionados ao sistema no futuro, portanto, provavelmente viola o OCP. Mas drive(MotorVehicle vehicle)é menos provável que o método precise mudar no futuro, por isso adere ao OCP.

    No entanto, é bastante difícil prever o futuro e saber com antecedência quais mudanças serão feitas no sistema.

  2. Depois: quando uma alteração for necessária, estenda uma classe em vez de modificar o código atual.

A prática nº 1 não é difícil de entender. No entanto, é a prática número 2 que estou tendo problemas para entender como me inscrever.

Por exemplo (a tirei de um vídeo no YouTube): vamos dizer que temos um método em uma classe que aceita CreditCardobjetos: makePayment(CraditCard card). Um dia Vouchers são adicionados ao sistema. Este método não os suporta, portanto, ele deve ser modificado.

Ao implementar o método em primeiro lugar, falhamos em prever o futuro e o programa em termos mais abstratos (por exemplo makePayment(Payment pay), agora precisamos alterar o código existente.

A prática nº 2 diz que devemos adicionar a funcionalidade estendendo ao invés de modificar. O que isso significa? Devo subclassificar a classe existente em vez de simplesmente alterar o código existente? Devo fazer algum tipo de wrapper para evitar a reescrita do código?

Ou o princípio nem mesmo se refere a 'como modificar / adicionar corretamente a funcionalidade', mas se refere a 'como evitar a necessidade de fazer alterações em primeiro lugar (isto é, programa para abstrações)?

Aviv Cohn
fonte
1
O Princípio Aberto / Fechado não determina o mecanismo que você usa. Herança é geralmente a escolha errada. Além disso, é impossível se proteger contra todas as mudanças futuras. É melhor não tentar prever o futuro, mas uma vez que uma mudança seja necessária, modifique o design para que futuras mudanças do mesmo tipo possam ser acomodadas.
Doval

Respostas:

14

Os princípios de design sempre precisam ser equilibrados entre si. Você não pode prever o futuro, e a maioria dos programadores faz isso horrivelmente quando tenta. É por isso que temos a regra dos três , que trata principalmente da duplicação, mas também se aplica à refatoração para quaisquer outros princípios de design.

Quando você tem apenas uma implementação de uma interface, não precisa se preocupar muito com o OCP, a menos que esteja claro onde as extensões ocorreriam. De fato, você freqüentemente perde a clareza ao tentar projetar demais nessa situação. Quando você o estende uma vez, refatora para torná-lo amigável ao OCP, se essa é a maneira mais fácil e clara de fazê-lo. Ao estendê-lo para uma terceira implementação, certifique-se de refatorá-lo levando em conta o OCP, mesmo que exija um pouco mais de esforço.

Na prática, quando você tem apenas duas implementações, refatorar quando você adiciona uma terceira geralmente não é muito difícil. É quando você deixa crescer depois desse ponto que fica difícil de manter.

Karl Bielefeldt
fonte
1
Obrigado por responder. Deixe-me ver se entendo o que você está dizendo: o que você está dizendo é que eu deveria me preocupar com o OCP principalmente depois que fui forçado a fazer alterações em uma classe. Significado: ao implementar uma classe pela primeira vez, não devo me preocupar muito com o OCP, pois é difícil prever o futuro. Quando preciso estendê-lo / modificá-lo pela primeira vez, talvez seja uma boa idéia refatorar um pouco para ser mais flexível no futuro (mais OCP). E na terceira vez que preciso estender / modificar a classe, é hora de refatorar para torná-la mais aderente ao OCP. É isto que você quer dizer?
Aviv Cohn
1
Essa é a ideia.
Karl Bielefeldt
2

Eu acho que você está olhando muito longe no futuro. Resolva o problema atual de uma maneira flexível que adere ao aberto / fechado.

Digamos que você precise implementar um drive(Car car)método. Dependendo do seu idioma, você tem algumas opções.

  • Para idiomas que suportam sobrecarga (C ++), use apenas drive(const Car& car)

    Em algum momento depois, você pode precisar drive(const Motorcycle& motorcycle), mas isso não interferirá drive(const Car& car). Sem problemas!

  • Para os idiomas que não suportam a sobrecarga (Objective C), em seguida, incluir o nome do tipo no método -driveCar:(Car *)car.

    Em algum momento depois, você pode precisar -driveMotorcycle:(Motorcycle *)motorcycle, mas, novamente, isso não interferirá.

Isso permite drive(Car car)que você seja fechado para modificação, mas está aberto a se estender a outros tipos de veículos. Esse planejamento futuro minimalista, que permite que você faça o trabalho hoje, mas evita que você se bloqueie no futuro.

Tentar imaginar os tipos mais básicos necessários pode levar a uma regressão infinita. O que acontece quando você deseja dirigir um Segue, uma bicicleta ou um jato Jumbo. Como você constrói um único tipo abstrato genérico que pode ser responsável por todos os dispositivos que as pessoas acessam e usam para mobilidade?

Jeffery Thomas
fonte
Modificar uma classe para adicionar seu novo método viola o Princípio Aberto-Fechado. Sua sugestão também elimina a capacidade de aplicar o Princípio de Substituição de Liskov a todos os veículos que podem dirigir, o que elimina essencialmente uma das partes mais fortes do OO.
Dunk
@ Dunk Baseei minha resposta no princípio aberto / fechado polimórfico, não no princípio estrito aberto / fechado de Meyer. É permitido atualizar classes para suportar novas interfaces. Neste exemplo, a interface do carro é mantida separada da interface da motocicleta. Elas poderiam ser formalizadas como classes abstratas de direção para automóveis e motocicletas, às quais a classe de implementação poderia apoiar.
Jeffery Thomas
@ Dunk O Princípio da Substituição de Liskov é útil, mas não é de graça. Se a especificação original exigir apenas um carro, a criação de um veículo mais genérico pode não valer o custo extra de dinheiro, tempo e complexidade. Além disso, é improvável que um veículo mais genérico seja perfeitamente adequado para lidar com subclasses não planejadas. A interface de uma motocicleta precisará ser encaixada na interface do veículo (que foi projetada para lidar apenas com o carro) ou você precisará modificar o veículo para lidar com a motocicleta (uma violação real da abertura / fechamento).
Jeffery Thomas
O princípio de Liskov-Substituição não vem de graça, mas também não tem muito custo. E geralmente ele paga muito mais do que custa muitas vezes, mesmo que outra subclasse nunca seja herdada dela no aplicativo principal. A aplicação do LSP facilita muito os testes automatizados, o que já é uma vitória. Além disso, embora você certamente não deva ficar louco e presumir que tudo vai precisar de LSP, se você estiver criando um aplicativo e não tiver uma boa noção do que provavelmente precisará dele em uma futura rev, você não precisará conheça o suficiente sobre seu aplicativo ou seu domínio.
Dunk
1
No que diz respeito à definição de OCP. Podem ser os setores em que trabalhei, que tendem a exigir níveis mais altos de verificação do que apenas uma empresa comercial comum, mas de um modo geral, se um arquivo / classe mudar, então você não apenas precisará testar novamente o arquivo / classe, mas tudo o que for necessário. faz uso desse arquivo / classe em seu teste de regressão. Portanto, não importa se alguém diz que abrir / fechar polimórfico é bom, alterar a interface tem consequências abrangentes, por isso não é tão bom assim.
Dunk
2

Entendo a intenção do princípio aberto-fechado. Ele visa reduzir o risco de quebrar algo que já funciona ao modificá-lo, solicitando que você tente estender sem modificar.

Trata-se também de não quebrar todos os objetos que dependem desse método, não alterando o comportamento de objetos já existentes. Depois que um objeto anuncia a mudança de comportamento, é arriscado, pois você altera o comportamento conhecido e esperado do objeto sem saber exatamente o que outros objetos esperam desse comportamento.

O que isso significa? Devo subclassificar a classe existente em vez de simplesmente alterar o código existente?

Sim.

"Somente aceita cartões de crédito" é definido como parte do comportamento dessa classe, por meio de sua interface pública. O programador declarou ao mundo que o método desse objeto aceita apenas cartões de crédito. Ela fez isso usando um nome meio não claro, mas está pronto. O resto do sistema está contando com isso.

Isso pode ter feito sentido na época, mas agora, se precisar mudar, você deve criar uma nova classe que aceite outras coisas que não cartões de crédito.

Novo comportamento = nova classe

Como um aparte - Uma boa maneira de prever o futuro é pensar no nome que você deu a um método. Você forneceu um nome de método de som realmente geral, como makePayment, a um método com regras específicas no método sobre exatamente qual pagamento ele pode fazer? Isso é um cheiro de código. Se você tiver regras específicas, elas deverão ficar claras no nome do método - makePayment deve ser makeCreditCardPayment. Faça isso quando estiver escrevendo o objeto pela primeira vez e outros programadores agradecerão por isso.

Cormac Mulhall
fonte