Qual é a maneira comum de lidar com a visibilidade nas bibliotecas?

12

Essa pergunta sobre quando usar o privado e quando usar o protegido nas aulas me fez pensar. (Vou estender essa pergunta também para as classes e métodos finais, pois estão relacionados. Estou programando em Java, mas acho que isso é relevante para todas as linguagens OOP)

A resposta aceita sais:

Uma boa regra geral é: torne tudo o mais privado possível.

E um outro:

  1. Torne todas as aulas finais, a menos que você precise subclassificá-las imediatamente.
  2. Torne todos os métodos finais, a menos que você precise subclassificar e substituí-los imediatamente.
  3. Torne todos os parâmetros do método final, a menos que você precise alterá-los no corpo do método, o que é meio estranho na maioria das vezes.

Isso é bem direto e claro, mas e se eu estiver escrevendo principalmente bibliotecas (código-fonte aberto no GitHub) em vez de aplicativos?

Eu poderia citar muitas bibliotecas e situações em que

  • Uma biblioteca foi estendida de uma maneira que os desenvolvedores nunca pensariam em
  • Isso tinha que ser feito com a "magia do carregador de classes" e outros hacks devido a restrições de visibilidade
  • As bibliotecas foram usadas de uma maneira para a qual não foram criadas e a maneira necessária de funcionalidade "invadida" em
  • As bibliotecas não puderam ser usadas devido a um pequeno problema (erro, falta de funcionalidade, comportamento "errado") que não pôde ser alterado devido à visibilidade reduzida
  • Um problema que não pôde ser corrigido levou a soluções alternativas enormes, feias e com erros, em que substituir uma função simples (privada ou final) poderia ter ajudado

Na verdade, comecei a nomear esses nomes até que a pergunta ficasse muito longa e resolvi removê-los.

Eu gosto da ideia de não ter mais código do que o necessário, mais visibilidade do que o necessário, mais abstração do que o necessário. E isso pode funcionar ao escrever um aplicativo para o usuário final, onde o código é usado apenas por quem o escreve. Mas como isso acontece se o código for usado por outros desenvolvedores, onde é improvável que o desenvolvedor original tenha pensado em todos os casos de uso possíveis com antecedência e que mudanças / refatores sejam difíceis / impossíveis de realizar?

Como as grandes bibliotecas de código aberto não são novidade, qual é a maneira mais comum de lidar com a visibilidade nesses projetos com linguagens orientadas a objetos?

piegames
fonte
dado que você pergunta sobre código aberto, faz ainda menos sentido alterar os princípios de codificação adequados para resolver os problemas listados por você do que o código fechado, simplesmente porque é possível contribuir com as correções necessárias diretamente no código da biblioteca ou bifurcá-lo e criar sua própria versão com as correções que eles querem
mosquito
2
meu argumento não é sobre isso, mas sobre sua referência ao código aberto que não faz sentido nesse contexto. Posso imaginar como as necessidades pragmáticas podem justificar o desvio de princípios estritos em alguns casos (também conhecidos como acumulação de dívida técnica ), mas dessa perspectiva, não importa se o código é fechado ou de código aberto. Ou, mais precisamente, importa na direção oposta do que um que você imaginou aqui porque o código sendo de fonte aberta pode pode fazer essas necessidades menos urgentes do que fechada porque oferece opções adicionais para enfrentar estes
mosquito
1
@piegames: Eu concordo plenamente em gnat aqui, os problemas que você detectou são muito mais prováveis ​​de ocorrer em bibliotecas de código fechado - se for uma biblioteca de SO com uma licença permissiva, se os mantenedores ignorarem uma solicitação de mudança, pode-se bifurcar a lib e altere a visibilidade por si mesmo, se necessário.
Doc Brown
1
@piegames: Eu não entendo sua pergunta. "Java" é uma linguagem, não uma biblioteca. E se "sua pequena biblioteca de código-fonte aberto" tiver visibilidade muito estrita, estender a visibilidade depois normalmente não prejudica a compatibilidade com versões anteriores. Apenas o contrário.
Doc Brown

Respostas:

15

A infeliz verdade é que muitas bibliotecas são escritas , não projetadas . Isso é triste, porque um pouco de pensamento prévio pode evitar muitos problemas no caminho.

Se pretendermos projetar uma biblioteca, haverá um conjunto de casos de uso antecipados. A biblioteca pode não satisfazer todos os casos de uso diretamente, mas pode servir como parte de uma solução. Portanto, a biblioteca precisa ser flexível o suficiente para se adaptar.

A restrição é que geralmente não é uma boa ideia pegar o código-fonte da biblioteca e modificá-lo para lidar com o novo caso de uso. Para bibliotecas proprietárias, a fonte pode não estar disponível e, para bibliotecas de código aberto, pode ser indesejável manter uma versão bifurcada. Pode não ser possível mesclar adaptações altamente específicas no projeto upstream.

É aqui que entra o princípio aberto-fechado: a biblioteca deve ser aberta para extensão sem modificar o código-fonte. Isso não vem naturalmente. Esse deve ser um objetivo de design intencional. Há várias técnicas que podem ajudar aqui, os padrões clássicos de design de POO são algumas delas. Em geral, especificamos ganchos nos quais o código do usuário pode ser conectado com segurança à biblioteca e adicionar funcionalidade.

Apenas tornar público todo método ou permitir que cada classe seja subclassificada não é suficiente para alcançar a extensibilidade. Primeiro de tudo, é realmente difícil estender a biblioteca se não estiver claro onde o usuário pode se conectar à biblioteca. Por exemplo, substituir a maioria dos métodos não é seguro porque o método da classe base foi escrito com suposições implícitas. Você realmente precisa projetar para extensibilidade.

Mais importante, quando algo faz parte da API pública, você não pode recuperá-lo. Você não pode refatorá-lo sem quebrar o código downstream. A abertura prematura limita a biblioteca a um design abaixo do ideal. Por outro lado, tornar as coisas internas privadas, mas adicionar ganchos, se mais tarde houver necessidade delas, é uma abordagem mais segura. Embora essa seja uma maneira sensata de lidar com a evolução a longo prazo de uma biblioteca, isso não é satisfatório para usuários que precisam usar a biblioteca no momento .

Então, o que acontece? Se houver um problema significativo com o estado atual da biblioteca, os desenvolvedores poderão obter todo o conhecimento sobre os casos de uso reais acumulados ao longo do tempo e escrever uma Versão 2 da biblioteca. Vai ser ótimo! Ele irá corrigir todos os erros por design! Também levará mais tempo do que o esperado, em muitos casos fracassando. E se a nova versão for muito diferente da versão antiga, pode ser difícil incentivar os usuários a migrar. Você fica mantendo duas versões incompatíveis.

amon
fonte
Então, eu preciso adicionar ganchos para extensão, porque apenas torná-lo público / substituível não é suficiente. E também preciso pensar em quando lançar alterações / nova API devido à compatibilidade com versões anteriores. Mas e a visibilidade dos métodos em especial?
piegames 24/08
@piegames Com visibilidade, você decide quais partes são públicas (parte de sua API estável) e quais são privadas (sujeitas a alterações). Se alguém contornar isso com a reflexão, é problema deles quando esse recurso é interrompido no futuro. A propósito, os pontos de extensão geralmente estão na forma de um método que pode ser substituído. Mas há uma diferença entre um método que pode ser substituído, e um método que está destinado a ser substituído (ver também o padrão Template Method).
amon
8

Cada classe / método público e extensível faz parte da sua API e deve ser suportado. Limitar esse conjunto a um subconjunto razoável da biblioteca permite maior estabilidade e limita o número de coisas que podem dar errado. É uma decisão de gerenciamento (e até mesmo os projetos OSS são gerenciados até certo ponto) com base no que você pode razoavelmente oferecer suporte.

A diferença entre o OSS e o código-fonte fechado é que a maioria das pessoas está tentando criar e aumentar uma comunidade em torno do código, para que mais de uma pessoa mantenha a biblioteca. Dito isto, existem várias ferramentas de gerenciamento disponíveis:

  • As listas de discussão discutem as necessidades do usuário e como implementar as coisas
  • Os sistemas de rastreamento de problemas (problemas no JIRA ou Git etc.) rastreiam bugs e solicitações de recursos
  • O controle de versão gerencia o código fonte.

Em projetos maduros, o que você verá é algo nesse sentido:

  1. Alguém quer fazer algo com a biblioteca que não foi originalmente projetada para fazer
  2. Eles adicionam um ticket ao rastreamento de problemas
  3. A equipe pode discutir o assunto na lista de discussão ou nos comentários, e o solicitante é sempre convidado a participar da discussão.
  4. A alteração da API é aceita e priorizada ou rejeitada por algum motivo

Nesse ponto, se a alteração foi aceita, mas o usuário deseja acelerar a correção, ele pode fazer o trabalho e enviar uma solicitação de recebimento ou um patch (dependendo da ferramenta de controle de versão).

Nenhuma API é estática. No entanto, seu crescimento precisa ser moldado de alguma forma. Ao manter tudo fechado até que haja uma necessidade demonstrada de abrir as coisas, você evita obter a reputação de uma biblioteca de bugs ou instável.

Berin Loritsch
fonte
1
Concordo plenamente e pratiquei com êxito o processo de solicitação de mudança que você buscou nas bibliotecas de código-fonte fechado de terceiros, bem como nas bibliotecas de código-fonte aberto.
Doc Brown
Na minha experiência, mesmo pequenas alterações em bibliotecas pequenas são muito trabalhosas (mesmo que seja apenas para convencer as outras) e podem levar algum tempo (para aguardar o próximo lançamento, se você não puder usar um instantâneo até então). Então isso claramente não é uma opção para mim. Eu estaria interessado: existem bibliotecas maiores (no GitHub) que realmente usam esse conceito?
piegames 24/08
É sempre muito trabalho. Quase todos os projetos nos quais contribuí têm um processo semelhante a esse. Nos meus dias Apache, poderíamos discutir algo por dias, porque éramos apaixonados pelo que criamos. Sabíamos que muitas pessoas iam usar as bibliotecas, então tivemos que discutir coisas como se a alteração proposta iria quebrar a API. O recurso proposto vale a pena? Quando devemos fazer isso? etc.
Berin Loritsch
0

Vou reformular minha resposta, uma vez que parece que isso afetou algumas pessoas.

A visibilidade da propriedade / método da classe não tem nada a ver com segurança nem abertura da fonte.

A razão pela qual a visibilidade existe é porque os objetos são frágeis para 4 problemas específicos:

  1. simultaneidade

Se você construir seu módulo sem o encapsulamento, seus usuários se acostumarão a alterar diretamente o estado do módulo. Isso funciona bem em um único ambiente de thread, mas quando você pensa em adicionar threads; você será forçado a tornar o estado privado e usar bloqueios / monitores junto com getters e setters que fazem outros threads aguardarem pelos recursos, em vez de correrem neles. Isso significa que os programas dos usuários não funcionarão mais porque as variáveis ​​privadas não podem ser acessadas de maneira convencional. Isso pode significar que você precisa de muitas reescritas.

A verdade é que é muito mais fácil codificar com um único tempo de execução em thread, e a palavra-chave privada permite que você adicione a palavra-chave sincronizada ou alguns bloqueios, e o código dos usuários não será quebrado se você o encapsular desde o início .

  1. Ajude a impedir que os usuários se atiram no pé / simplifiquem o uso da interface. Em essência, ajuda a controlar os invariantes do objeto.

Todo objeto tem várias coisas necessárias para ser verdade para estar em um estado consistente. Infelizmente, essas coisas vivem no espaço visível do cliente, porque é caro mover cada objeto para seu próprio processo e conversar com ele por meio de mensagens. Isso significa que é muito fácil para um objeto travar o programa inteiro se o usuário tiver total visibilidade.

Isso é inevitável, mas você pode evitar colocar acidentalmente um objeto em um estado inconsistente, fechando uma interface sobre seus serviços que evitam falhas acidentais, permitindo apenas que o usuário interaja com o estado do objeto por meio de uma interface cuidadosamente criada que torna o programa muito mais robusto . Isso não significa que o usuário não possa intencionalmente corromper os invariantes, mas se o fizerem, é o cliente que trava, tudo o que eles precisam fazer é reiniciar o programa (os dados que você deseja proteger não devem ser armazenados no lado do cliente )

Outro bom exemplo em que você pode melhorar a usabilidade de seus módulos é tornar o construtor privado; porque se o construtor lançar uma exceção, ele matará o programa. Uma abordagem preguiçosa para resolver isso é fazer com que o construtor gere um erro de tempo de compilação que você não pode construí-lo, a menos que esteja em um bloco de tentativa / captura. Tornando o construtor privado e adicionando um método público estático de criação, você pode fazer com que o método create retorne nulo se ele não for construído, ou use uma função de retorno de chamada para lidar com o erro, tornando o programa mais amigável.

  1. Poluição do escopo

Muitas classes têm muitos estados e métodos e é fácil ficar sobrecarregado tentando rolar por elas; Muitos desses métodos são apenas ruídos visuais, como funções auxiliares, estado. tornar as variáveis ​​e os métodos privados ajuda a reduzir a poluição do escopo e facilita o usuário a encontrar os serviços que está procurando.

Em essência, permite que você tenha funções auxiliares dentro da classe e não fora da classe; sem controle de visibilidade sem distrair o usuário com vários serviços que o usuário nunca deve usar, para que você possa dividir métodos em vários métodos auxiliares (embora ele ainda polua seu escopo, mas não o usuário).

  1. estar vinculado a dependências

Uma interface bem criada pode ocultar seus bancos de dados / janelas / imagens internos dos quais depende para fazer seu trabalho e, se você quiser mudar para outro banco de dados / outro sistema de janelas / outra biblioteca de imagens, poderá manter a interface e os usuários não vai perceber.

Por outro lado, se você não fizer isso, poderá facilmente tornar impossível alterar suas dependências, porque elas estão expostas e o código depende disso. Com um sistema grande o suficiente, o custo da migração pode se tornar inacessível, enquanto um encapsulamento pode proteger os usuários do cliente que se comportam bem contra decisões futuras de troca de dependências.

Dmitry
fonte
1
"Não adianta esconder nada" - então por que pensar em encapsulamento? Em muitos contextos, a reflexão requer privilégios especiais.
23417 Frank Hileman
Você pensa no encapsulamento porque ele oferece espaço para respirar durante o desenvolvimento do seu módulo e reduz a probabilidade de uso indevido. Por exemplo, se você tiver 4 threads modificando diretamente o estado interno de uma classe, isso facilmente causará problemas, enquanto tornar a variável privada encoraja o usuário a usar os métodos públicos para manipular o estado mundial, que pode usar monitores / bloqueios para evitar problemas . Esse é o único benefício real do encapsulamento.
Dmitry
Ocultar coisas por questões de segurança é uma maneira fácil de criar um design em que você acaba tendo que ter buracos na sua API. Um bom exemplo disso são os aplicativos para vários documentos, onde você tem muitas caixas de ferramentas e muitas janelas com sub-janelas. Se você enlouquecer com o encapsulamento, acabará tendo uma situação em que desenhar algo em um documento, terá que pedir à janela para solicitar ao documento interno para solicitar ao documento interno para solicitar ao documento interno que faça um pedido para desenhar algo e invalidar seu contexto. Se o lado do cliente quiser jogar com o cliente, você não poderá impedi-lo.
Dmitry
OK, isso faz mais sentido, embora a segurança possa ser alcançada por meio do controle de acesso, se o ambiente suportar isso, e esse era um dos objetivos originais no design da linguagem OO. Além disso, você está promovendo o encapsulamento e dizendo para não usá-lo ao mesmo tempo; um pouco confuso.
24717 Frank Hileman
Eu nunca quis não usá-lo; Eu quis dizer não usá-lo por uma questão de segurança; use-o estrategicamente para melhorar a experiência de seus usuários e proporcionar um ambiente de desenvolvimento mais suave. O que quero dizer é que não tem nada a ver com segurança nem abertura da fonte. Os objetos do lado do cliente são, por definição, vulneráveis ​​à introspecção, e movê-los para fora do espaço de processo do usuário torna as coisas não encapsuladas igualmente inacessíveis como encapsuladas.
Dmitry