Por que muitos desenvolvedores de software violam o princípio aberto / fechado?


74

Por que muitos desenvolvedores de software violam o princípio de aberto / fechado , modificando muitas coisas, como renomear funções, que interromperão o aplicativo após a atualização?

Essa pergunta me vem à cabeça depois das versões rápida e contínua na biblioteca React .

A cada curto período, noto muitas mudanças na sintaxe, nos nomes dos componentes, etc.

Exemplo na próxima versão do React :

Novos avisos de descontinuação

A maior mudança é que extraímos React.PropTypes e React.createClass em seus próprios pacotes. Ambos ainda podem ser acessados ​​pelo objeto React principal, mas o uso de ambos registrará um aviso de descontinuação único no console quando estiver no modo de desenvolvimento. Isso permitirá futuras otimizações de tamanho de código.

Esses avisos não afetarão o comportamento do seu aplicativo. No entanto, percebemos que eles podem causar alguma frustração, principalmente se você usar uma estrutura de teste que trate console.error como uma falha.


  • Essas mudanças são consideradas uma violação desse princípio?
  • Como iniciante em algo como React , como o aprendo com essas rápidas mudanças na biblioteca (é tão frustrante)?

6
Este é claramente um exemplo de observação , e sua alegação de "tantos" não tem fundamento. Os projetos Lucene e RichFaces são exemplos notórios e a API da porta COMM do Windows, mas não consigo pensar em nenhum outro de antemão. E o React é realmente um 'grande desenvolvedor de software'?
usar o seguinte comando

62
Como qualquer princípio, o OCP tem seu valor. Mas isso requer que os desenvolvedores tenham uma visão infinita. No mundo real, as pessoas geralmente entendem errado o primeiro design. À medida que o tempo passa, alguns preferem solucionar seus antigos erros por uma questão de compatibilidade, outros preferem, eventualmente, limpá-los para ter uma base de código compacta e sem ônus.
Theodoros Chatzigiannakis

1
Quando foi a última vez que você viu uma linguagem orientada a objetos "como originalmente pretendida"? O princípio central era um sistema de mensagens que significava que cada parte do sistema é infinitamente extensível por qualquer pessoa. Agora compare isso com sua linguagem típica de OOP - quantas permitem estender um método existente de fora? Quantos facilitam o suficiente para serem úteis?
Luaan

Legado é uma merda. 30 anos de experiência mostraram que você deve despejar completamente o legado e começar do zero sempre. Hoje todo mundo tem conexão em todos os lugares o tempo todo, então o legado é totalmente irrelevante hoje. o exemplo final foi "Windows versus Mac". A Microsoft tradicionalmente tentava "dar suporte ao legado"; você vê isso de várias maneiras. A Apple sempre disse "F- - - Você" para usuários legados. (Isso se aplica a tudo, de idiomas a dispositivos e sistemas operacionais.) Na verdade, a Apple estava totalmente correta e o MSFT estava totalmente errado, claro e simples.
Fattie

4
Porque existem exatamente zero "princípios" e "padrões de design" que funcionam 100% do tempo na vida real.
Matti Virkkunen

Respostas:


148

A resposta do IMHO JacquesB, embora contenha muita verdade, mostra um mal-entendido fundamental do OCP. Para ser justo, sua pergunta também expressa esse mal-entendido - renomear funções quebra a compatibilidade com versões anteriores , mas não o OCP. Se a quebra de compatibilidade parecer necessária (ou a manutenção de duas versões do mesmo componente para não quebrar a compatibilidade), o OCP já estava quebrado antes!

Como Jörg W Mittag já mencionou em seus comentários, o princípio não diz "você não pode modificar o comportamento de um componente" - diz: deve-se tentar projetar componentes de uma maneira que eles estejam abertos para serem reutilizados (ou estendidos) de várias maneiras, sem a necessidade de modificação. Isso pode ser feito fornecendo os "pontos de extensão" corretos ou, como mencionado por @AntP, "decompondo uma estrutura de classe / função para o ponto em que todos os pontos de extensão naturais estão por padrão". O IMHO após o OCP não tem nada em comum com "manter a versão antiga inalterada para compatibilidade com versões anteriores" ! Ou, citando o comentário de @ DerekElkin abaixo:

O OCP é um conselho sobre como escrever um módulo, [...] não sobre a implementação de um processo de gerenciamento de mudanças que nunca permite a mudança de módulos.

Bons programadores usam sua experiência para projetar componentes com os pontos de extensão "certos" em mente (ou - melhor ainda - de maneira que nenhum ponto de extensão artificial seja necessário). No entanto, para fazer isso corretamente e sem uma engenharia desnecessária, é necessário saber de antemão como serão os futuros casos de uso do seu componente. Mesmo programadores experientes não podem olhar para o futuro e conhecer todos os requisitos futuros com antecedência. E é por isso que às vezes a compatibilidade com versões anteriores precisa ser violada - não importa quantos pontos de extensão seu componente tenha, ou quão bem ele siga o OCP em relação a certos tipos de requisitos, sempre haverá um requisito que não pode ser implementado facilmente sem modificar o componente.


14
A IMO, a maior razão para "violar" o OCP, é que é preciso muito esforço para se adequar a ele corretamente. Eric Lippert tem uma excelente postagem no blog sobre por que muitas das classes do .NET framework parecem violar o OCP.
BJ Myers

2
@ BJMyers: obrigado pelo link. Jon Skeet tem um excelente post sobre o OCP como sendo muito semelhante à idéia de variação protegida.
Doc Brown)

8
ESTA! O OCP diz que você deve escrever um código que possa ser alterado sem ser tocado! Por quê? Então você só precisa testar, revisar e compilar uma vez. Novo comportamento deve vir do novo código. Não estragando o código antigo e comprovado. E a refatoração? Bem, refatoração é uma clara violação do OCP! É por isso que é um pecado escrever código pensando que você apenas o refatorará se suas suposições mudarem. Não! Coloque cada suposição em sua própria caixinha. Quando estiver errado, não conserte a caixa. Escreva um novo. Por quê? Porque você pode precisar voltar para o antigo. Quando você faz, seria bom se ainda funcionasse.
candied_orange

7
@ CandiedOrange: obrigado pelo seu comentário. Não vejo a refatoração e o OCP tão contrários à descrição. Escrever componentes que seguem o OCP requer frequentemente vários ciclos de refatoração. O objetivo deve ser um componente que não precise de modificações para resolver toda uma "família" de requisitos. No entanto, não se deve adicionar pontos de extensão arbitrários a um componente "apenas por precaução", que leva muito facilmente à superengenharia. Confiar na possibilidade de refatoração pode ser a melhor alternativa para isso em muitos casos.
Doc Brown

4
Esta resposta faz um bom trabalho ao apontar os erros na resposta (atualmente) principal - acho que a principal coisa de ser bem-sucedido com aberto / fechado é parar de pensar em termos de "pontos de extensão" e começar a pensar em decompor seu estrutura de classe / função até o ponto em que todo ponto de extensão natural existe por padrão. Programar "fora para dentro" é uma maneira muito boa de conseguir isso, onde todo cenário que seu método / função atual atende é enviado para uma interface externa, que forma um ponto de extensão natural para decoradores, adaptadores etc.
Ant P

67

O princípio aberto / fechado tem benefícios, mas também apresenta algumas desvantagens sérias.

Em teoria, o princípio resolve o problema de compatibilidade com versões anteriores, criando código "aberto para extensão, mas fechado para modificação". Se uma classe tem alguns novos requisitos, você nunca modifica o código fonte da própria classe, mas cria uma subclasse que substitui apenas os membros apropriados necessários para alterar o comportamento. Todo o código gravado na versão original da classe não é afetado, portanto, você pode ter certeza de que sua alteração não quebrou o código existente.

Na realidade, você acaba facilmente com o inchaço do código e uma confusão confusa de classes obsoletas. Se não for possível modificar algum comportamento de um componente por meio de extensão, você deverá fornecer uma nova variante do componente com o comportamento desejado e manter a versão antiga inalterada para compatibilidade com versões anteriores.

Digamos que você descubra uma falha fundamental de design em uma classe base da qual muitas classes herdam. Digamos que o erro seja devido a um campo privado do tipo errado. Você não pode corrigir isso substituindo um membro. Basicamente, você precisa substituir toda a classe, o que significa que você acaba estendendo Objectpara fornecer uma classe base alternativa - e agora você também precisa fornecer alternativas para todas as subclasses, terminando com uma hierarquia de objetos duplicada, uma hierarquia defeituosa e uma melhorada . Mas você não pode remover a hierarquia defeituosa (já que a exclusão do código é uma modificação), todos os futuros clientes serão expostos às duas hierarquias.

Agora, a resposta teórica para esse problema é "apenas projete-o corretamente da primeira vez". Se o código for perfeitamente decomposto, sem falhas ou erros e projetado com pontos de extensão preparados para todas as possíveis alterações futuras de requisitos, evite a confusão. Mas, na realidade, todo mundo comete erros e ninguém pode prever o futuro perfeitamente.

Pegue algo como o framework .NET - ele ainda carrega o conjunto de classes de coleção que foram projetadas antes da introdução dos genéricos há mais de uma década. Isso certamente é um benefício para a compatibilidade com versões anteriores (você pode atualizar a estrutura sem precisar reescrever nada), mas também incha a estrutura e apresenta aos desenvolvedores um grande conjunto de opções, onde muitas são simplesmente obsoletas.

Aparentemente, os desenvolvedores do React sentiram que não valia a pena o custo em complexidade e inchaço do código para seguir rigorosamente o princípio de abrir / fechar.

A alternativa pragmática ao aberto / fechado é a depreciação controlada. Em vez de quebrar a compatibilidade com versões anteriores em um único release, os componentes antigos são mantidos por um ciclo de release, mas os clientes são informados por meio de avisos do compilador de que a abordagem antiga será removida em um release posterior. Isso dá aos clientes tempo para modificar o código. Essa parece ser a abordagem do React neste caso.

(Minha interpretação do princípio é baseada no Princípio Aberto-Fechado de Robert C. Martin)


37
"O princípio basicamente diz que você não pode modificar o comportamento de um componente. Em vez disso, é necessário fornecer uma nova variante do componente com o comportamento desejado e manter a versão antiga inalterada para compatibilidade com versões anteriores". - Eu discordo disso. O princípio diz que você deve projetar componentes de tal maneira que não seja necessário alterar seu comportamento, porque você pode estendê-lo para fazer o que quiser. O problema é que ainda não descobrimos como fazer isso, especialmente com os idiomas atualmente em uso generalizado. O problema da expressão é uma parte do…
Jörg W Mittag

8
... isso, por exemplo. Nem Java nem C♯ têm uma solução para a Expressão. Haskell e Scala, mas sua base de usuários é muito menor.
Jörg W Mittag

1
@ Giorgio: Em Haskell, a solução são classes de tipo. Em Scala, a solução é implícita e objetos. No momento, não tenho os links em mãos. Sim, os multimétodos (na verdade, eles nem precisam ser "multi", é a natureza "aberta" dos métodos do Lisp que é necessária) também são uma solução possível. Observe que existem várias frases do problema de expressão, porque normalmente os artigos são escritos de tal maneira que o autor adiciona uma restrição ao problema de expressão, o que resulta no fato de que todas as soluções existentes no momento se tornam inválidas e mostra como as suas…
Jörg W Mittag

1
... a linguagem pode até resolver esta versão "mais difícil". Por exemplo, o Wadler originalmente formulou o problema da expressão não apenas para extensão modular, mas também para extensão modular estaticamente segura. Os multimétodos comuns do Lisp, no entanto, não são estaticamente seguros, apenas dinamicamente seguros. Odersky então reforçou ainda mais isso dizendo que deveria ser modular estaticamente seguro, ou seja, a segurança deve ser estaticamente verificável sem observar o programa inteiro, apenas observando o módulo de extensão. Na verdade, isso não pode ser feito com as classes do tipo Haskell, mas pode ser feito com o Scala. E no…
Jörg W Mittag

2
@Giorgio: Exatamente. O que faz com que os métodos múltiplos do Common Lisp resolvam o EP não é, na verdade, despacho múltiplo. É o fato de que os métodos estão abertos. No FP típico (ou programação processual), a discriminação de tipo está ligada às funções. Em OO típico, os métodos estão vinculados aos tipos. Os métodos comuns do Lisp são abertos , eles podem ser adicionados às classes após o fato e em um módulo diferente. Essa é a característica que os torna úteis para resolver o EP. Por exemplo, os protocolos de Clojure são de envio único, mas também resolvem o EP (desde que você não insista em segurança estática).
Jörg W Mittag

20

Eu chamaria o princípio aberto / fechado de ideal. Como todos os ideais, ele dá pouca consideração às realidades do desenvolvimento de software. Também como todos os ideais, é impossível alcançá-lo na prática - apenas se esforça para abordar esse ideal da melhor maneira possível.

O outro lado da história é conhecido como Algemas de Ouro. Algemas de Ouro são o que você ganha quando se escrava demais do princípio de abrir / fechar. Algemas douradas são o que ocorre quando o produto que nunca quebra a compatibilidade com versões anteriores não pode crescer porque muitos erros do passado foram cometidos.

Um exemplo famoso disso é encontrado no gerenciador de memória do Windows 95. Como parte do marketing do Windows 95, foi declarado que todos os aplicativos do Windows 3.1 funcionariam no Windows 95. A Microsoft adquiriu licenças para milhares de programas para testá-los no Windows 95. Um dos casos de problemas foi o Sim City. Na verdade, o Sim City tinha um bug que fazia com que ele gravasse na memória não alocada. No Windows 3.1, sem um gerenciador de memória "adequado", esse foi um pequeno passo em falso. No entanto, no Windows 95, o gerenciador de memória detectaria isso e causaria uma falha de segmentação. A solução? No Windows 95, se o nome do seu aplicativo for simcity.exe, o sistema operacional relaxará as restrições do gerenciador de memória para evitar a falha de segmentação!

A verdadeira questão por trás desse ideal são os conceitos simplificados de produtos e serviços. Ninguém realmente faz um ou o outro. Tudo se alinha em algum lugar na região cinzenta entre os dois. Se você pensa em uma abordagem orientada ao produto, abrir / fechar soa como um ótimo ideal. Seus produtos são confiáveis. No entanto, quando se trata de serviços, a história muda. É fácil mostrar que, com o princípio de aberto / fechado, a quantidade de funcionalidade que sua equipe deve suportar deve abordar assintoticamente o infinito, porque você nunca pode limpar as funcionalidades antigas. Isso significa que sua equipe de desenvolvimento deve oferecer suporte a mais e mais códigos a cada ano. Eventualmente, você chega a um ponto de ruptura.

Atualmente, a maioria dos softwares, especialmente de código aberto, segue uma versão relaxada comum do princípio de aberto / fechado. É muito comum ver aberto / fechado seguido servilmente para lançamentos menores, mas abandonado para lançamentos principais. Por exemplo, o Python 2.7 contém muitas "más escolhas" dos dias Python 2.0 e 2.1, mas o Python 3.0 varreu todas elas. (Além disso, a mudança da base de código do Windows 95 para a base de código do Windows NT quando lançou o Windows 2000 quebrou todos os tipos de coisas, mas que significa que nunca tem que lidar com um gerenciador de memória verificando o nome do aplicativo para decidir comportamento!)


Essa é uma ótima história sobre o SimCity. Você tem uma fonte?
BJ Myers

5
@BJMyers É uma história antiga, Joel Spoleky menciona isso no final deste artigo . Eu o li originalmente como parte de um livro sobre o desenvolvimento de videogames anos atrás.
precisa saber é o seguinte

1
@BJMyers: Tenho certeza de que eles tinham "hacks" de compatibilidade semelhantes para dezenas de aplicativos populares.
Doc Brown)

3
@BJMyers, existem muitas coisas assim, se você quiser uma boa leitura, vá para o blog The Old New Thing, de Raymond Chen , navegue pela tag History ou procure por "compatibilidade". Há uma lembrança de muitos contos, incluindo algo conspicuamente próximo do caso SimCity acima mencionado - Addentum: Chen não gosta de chamar nomes para culpar.
Theraot

2
Pouquíssimas coisas quebraram mesmo na transição 95-> NT. O SimCity original para Windows ainda funciona muito bem no Windows 10 (32 bits). Até jogos do DOS ainda funcionam perfeitamente, desde que você desative o som ou use algo como o VDMSound para permitir que o subsistema do console manipule o áudio corretamente. A Microsoft leva a compatibilidade com versões anteriores muito a sério, e eles também não estão levando nenhum atalho do tipo "vamos colocar em uma máquina virtual". Às vezes, precisa de uma solução alternativa, mas isso ainda é bastante impressionante, especialmente em termos relativos.
Luaan

11

A resposta do Doc Brown é a mais exata possível, as outras respostas ilustram mal-entendidos do Princípio Aberto Fechado.

Articular explicitamente o mal-entendido, parece haver uma crença de que o OCP significa que você não deve fazer alterações para trás incompatíveis (ou mesmo quaisquer alterações ou algo nesse sentido.) O OCP é sobre a criação de componentes de modo que você não precisa de faça alterações para estender sua funcionalidade, independentemente de essas alterações serem compatíveis com versões anteriores ou não. Há muitas outras razões além da adição de funcionalidades, que você pode fazer alterações em um componente, seja elas compatíveis com versões anteriores (por exemplo, refatoração ou otimização) ou incompatíveis com versões anteriores (por exemplo, descontinuando e removendo a funcionalidade). O fato de você poder fazer essas alterações não significa que seu componente violou o OCP (e definitivamente não significa que você estão violando o OCP).

Realmente, não se trata de código fonte. Uma declaração mais abstrata e relevante do OCP é: "um componente deve permitir a extensão sem a necessidade de violar seus limites de abstração". Eu iria além e diria que uma versão mais moderna é: "um componente deve impor seus limites de abstração, mas permitir a extensão". Mesmo no artigo sobre OCP de Bob Martin, enquanto ele "descreve" "fechado para modificação" como "o código-fonte é inviolável", mais tarde ele começa a falar sobre encapsulamento que nada tem a ver com a modificação do código-fonte e tudo a ver com abstração limites.

Portanto, a premissa defeituosa na pergunta é que o OCP é (pretendido como) uma diretriz sobre evoluções de uma base de código. O OCP é tipicamente slogan como "um componente deve ser aberto a extensões e fechado a modificações pelos consumidores". Basicamente, se um consumidor de um componente desejar adicionar funcionalidade ao componente, ele poderá estender o componente antigo para um novo com a funcionalidade adicional, mas não poderá alterar o componente antigo.

O OCP não diz nada sobre o criador de um componente alterando ou removendo a funcionalidade. O OCP não está defendendo a manutenção da compatibilidade de bugs para sempre. Você, como criador, não está violando o OCP alterando ou mesmo removendo um componente. Você, ou melhor, os componentes que você escreveu, está violando o OCP se a única maneira de os consumidores adicionarem funcionalidade aos seus componentes é alterando-a, por exemplo, através de patches de macacosou ter acesso ao código fonte e recompilar. Em muitos casos, nenhuma dessas opções é para o consumidor, o que significa que, se seu componente não estiver "aberto para extensão", eles terão azar. Eles simplesmente não podem usar seu componente para as necessidades deles. O OCP argumenta para não colocar os consumidores da sua biblioteca nessa posição, pelo menos no que diz respeito a alguma classe identificável de "extensões". Mesmo quando modificações podem ser feitas no código-fonte ou até mesmo na cópia principal do código-fonte, é melhor "fingir" que você não pode modificá-lo, pois há muitas conseqüências negativas em potencial.

Portanto, para responder às suas perguntas: Não, essas não são violações do OCP. Nenhuma mudança feita por um autor pode ser uma violação do OCP, porque o OCP não é uma série de mudanças. As alterações, no entanto, podem criar violações do OCP e podem ser motivadas por falhas do OCP em versões anteriores da base de código. O OCP é uma propriedade de um pedaço de código específico, não a história evolutiva de uma base de código.

Por outro lado, a compatibilidade com versões anteriores é uma propriedade de uma alteração de código. Não faz sentido dizer que algum código é ou não é compatível com versões anteriores. Só faz sentido falar sobre a compatibilidade com versões anteriores de algum código com relação a algum código mais antigo. Portanto, nunca faz sentido falar sobre o primeiro corte de algum código ser compatível com versões anteriores ou não. O primeiro corte de código pode satisfazer ou deixar de satisfazer o OCP e, em geral, podemos determinar se algum código satisfaz o OCP sem se referir a nenhuma versão histórica do código.

Quanto à sua última pergunta, é indiscutivelmente fora de tópico para o StackExchange, em geral, ser principalmente baseado em opiniões, mas a falta dela é bem-vinda à tecnologia e, principalmente, ao JavaScript, onde nos últimos anos o fenômeno que você descreve foi chamado fadiga do JavaScript . (Fique à vontade no Google para encontrar uma variedade de outros artigos, alguns satíricos, falando sobre isso de várias perspectivas.)


3
"Você, como criador, não está violando o OCP alterando ou mesmo removendo um componente." - você pode fornecer uma referência para isso? Nenhuma das definições do princípio que vi afirma que "o criador" (o que quer que isso signifique) está isento do princípio. A remoção de um componente publicado é claramente uma alteração de ruptura.
JacquesB

1
@JacquesB As pessoas e até as alterações de código não violam o OCP, componentes (ou seja, partes reais do código) o fazem. (E, para ser perfeitamente claro, isso significa que o componente falha em atender ao próprio OCP, não que viole o OCP de outro componente.) O ponto principal da minha resposta é que o OCP não está falando sobre alterações de código , quebrando ou não. Um componente é aberto à extensão e fechado à modificação, ou não é, assim como um método pode ser privateou não. Se um autor faz um privatemétodo publicmais tarde, isso não significa que eles violaram o controle de acesso, (1/2)
Derek Elkins #

2
... nem significa que o método não era realmente privateantes. "A remoção de um componente publicado é claramente uma alteração", não é uma sequência. Os componentes da nova versão satisfazem o OCP ou não, não é necessário o histórico da base de código para determinar isso. Pela sua lógica, eu nunca poderia escrever código que satisfaça o OCP. Você está conflitando com a compatibilidade com versões anteriores, uma propriedade do código muda, com o OCP, uma propriedade do código. Seu comentário faz tanto sentido quanto dizer que o quicksort não é compatível com versões anteriores. (2/2)
Derek Elkins

3
@ JacquesB Primeiro, observe novamente que se trata de um módulo em conformidade com o OCP. O OCP é um conselho sobre como escrever um módulo para que, dada a restrição de que o código fonte não possa ser alterado, o módulo possa ser estendido. No início do artigo, ele fala sobre o design de módulos que nunca mudam, nem sobre a implementação de um processo de gerenciamento de mudanças que nunca permite que os módulos mudem. Referindo-se à edição da sua resposta, você não "quebra o OCP" modificando o código do módulo. Em vez disso, se "estender" o módulo exigir que você modifique o código-fonte, (1/3)
Derek Elkins

2
"O OCP é uma propriedade de um pedaço de código específico, não a história evolutiva de uma base de código". - excelente!
Doc Brown
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.