Por que as classes não deveriam ser projetadas para serem "abertas"?

44

Ao ler várias perguntas do Stack Overflow e o código de outras pessoas, o consenso geral de como projetar classes é fechado. Isso significa que, por padrão em Java e C #, tudo é privado, os campos são finais, alguns métodos são finais e, às vezes, as classes são até finais .

A idéia por trás disso é ocultar os detalhes da implementação, o que é uma boa razão. No entanto, com a existência protectedna maioria das linguagens e polimorfismo OOP, isso não funciona.

Toda vez que desejo adicionar ou alterar a funcionalidade de uma classe, geralmente sou prejudicada por particulares e finais colocados em todos os lugares. Aqui os detalhes da implementação são importantes: você está pegando a extensão e estendendo-a, sabendo muito bem quais são as consequências. No entanto, como não consigo acessar campos e métodos particulares e finais, tenho três opções:

  • Não estenda a classe, apenas contorne o problema que leva ao código mais complexo
  • Copie e cole toda a classe, eliminando a reutilização do código
  • Bifurcar o projeto

Essas não são boas opções. Por que não é protectedusado em projetos escritos em idiomas que o suportam? Por que alguns projetos proíbem explicitamente a herança de suas classes?

TheLQ
fonte
1
Eu concordo, eu tive esse problema com os componentes Java Swing, é muito ruim.
Jonas
3
Há uma boa chance de que, se você está tendo esse problema, as classes foram mal projetadas - ou talvez você esteja tentando usá-las incorretamente. Você não pede informações a uma classe, pede que faça algo por você - portanto, geralmente não deve precisar dos dados. Embora isso nem sempre seja verdade, se você achar que precisa acessar os dados de uma classe, é muito provável que algo dê errado.
Bill K
2
"a maioria dos idiomas OOP?" Eu sei muito mais onde as aulas não podem ser fechadas. Você deixou a quarta opção: alterar idiomas.
Kevin cline
@Jonas Um dos principais problemas do Swing é o fato de expor muita implementação. Isso realmente mata o desenvolvimento.
Tom Hawtin - tackline

Respostas:

55

Projetar classes para funcionar corretamente quando estendidas, especialmente quando o programador que estende a extensão não entende completamente como a classe deve funcionar, exige um esforço extra considerável. Você não pode simplesmente pegar tudo o que é privado e torná-lo público (ou protegido) e chamar isso de "aberto". Se você permitir que outra pessoa altere o valor de uma variável, você deve considerar como todos os valores possíveis afetarão a classe. (E se eles definirem a variável como nula? Uma matriz vazia? Um número negativo?) O mesmo se aplica ao permitir que outras pessoas chamam um método. É preciso um pensamento cuidadoso.

Portanto, não é tanto que as aulas não devam ser abertas, mas às vezes não vale a pena o esforço para torná-las abertas.

Obviamente, também é possível que os autores da biblioteca estejam apenas sendo preguiçosos. Depende de qual biblioteca você está falando. :-)

Aaron
fonte
13
Sim, é por isso que as classes são seladas por padrão. Como você não pode prever as várias maneiras que seus clientes podem estender a classe, é mais seguro selá-la do que se comprometer a oferecer suporte ao número potencialmente ilimitado de maneiras pelas quais a classe pode ser estendida.
Robert Harvey
18
Você não precisa prever como sua classe será substituída, basta assumir que as pessoas que estendem sua classe sabem o que estão fazendo. Se eles estenderem a classe e configurarem algo como nulo quando não deve ser nulo, a culpa é deles, não sua. O único lugar que esse argumento faz sentido é em aplicativos super-críticos, nos quais algo dará terrivelmente errado se um campo for nulo.
TheLQ
5
@TheLQ: Essa é uma suposição bastante grande.
Robert Harvey
9
@TheLQ De quem é a culpa é uma questão altamente subjetiva. Se eles definirem uma variável com um valor aparentemente razoável, e você não documentou o fato de que isso não era permitido, e seu código não gerou uma ArgumentException (ou equivalente), mas faz com que o servidor fique offline ou leva para uma exploração de segurança, então eu diria que a culpa é sua e deles. E se você documentou os valores válidos e implementou uma verificação de argumento, aplicou exatamente o tipo de esforço que estou falando, e você deve se perguntar se esse esforço foi realmente um bom uso do seu tempo.
Aaron
24

Tornar tudo privado por padrão parece duro, mas olhe para o outro lado: quando tudo é privado por padrão, tornar público algo (ou protegido, que é quase a mesma coisa) é uma escolha consciente; é o contrato do autor da classe com você, consumidor, sobre como usar a classe. Esta é uma conveniência para vocês dois: o autor é livre para modificar o funcionamento interno da classe, desde que a interface permaneça inalterada; e você sabe exatamente em quais partes da turma você pode confiar e quais estão sujeitas a alterações.

A idéia subjacente é 'acoplamento flexível' (também conhecido como 'interfaces estreitas'); seu valor reside em manter a complexidade baixa. Ao reduzir o número de maneiras pelas quais os componentes podem interagir, a quantidade de dependência cruzada entre eles também é reduzida; e a dependência cruzada é um dos piores tipos de complexidade quando se trata de manutenção e gerenciamento de mudanças.

Em bibliotecas bem projetadas, as classes que valem a pena estender por herança protegeram e publicaram os membros nos lugares certos e ocultaram todo o resto.

tdammers
fonte
Isso faz algum sentido. Ainda existe o problema quando você precisa de algo muito semelhante, mas com 1 ou 2 coisas mudaram na implementação (funcionamento interno da classe). Também estou lutando para entender que, se você está substituindo a classe, você já não depende muito da implementação?
TheLQ
5
+1 para os benefícios do acoplamento solto. @ TheLQ, é a interface em que você deve depender muito, não a implementação.
Karl Bielefeldt
19

Tudo o que não é privado deve existir mais ou menos com comportamento inalterado em todas as versões futuras da classe. De fato, pode ser considerado parte da API, documentada ou não. Portanto, expor muitos detalhes provavelmente está causando problemas de compatibilidade posteriormente.

Quanto resp "final". classes "seladas", um caso típico são classes imutáveis. Muitas partes da estrutura dependem da imutabilidade das strings. Se a classe String não fosse final, seria fácil (até tentador) criar uma subclasse String mutável que causasse todos os tipos de erros em muitas outras classes quando usada em vez da classe String imutável.

user281377
fonte
4
Esta é a verdadeira resposta. Outras respostas abordam coisas importantes, mas a parte relevante é a seguinte: tudo o que não é finale / ou privateestá bloqueado e nunca pode ser alterado .
21411 Konrad Rudolph
1
@ Konrad: Até os desenvolvedores decidirem reformular tudo e não serem compatíveis com versões anteriores.
1113 JAB
Isso é verdade, mas vale a pena notar que é um requisito muito mais fraco do que para os publicmembros; protectedos membros formam um contrato apenas entre a classe que os expõe e suas classes derivadas imediatas; por outro lado, publicmembros de contratos com todos os consumidores em nome de todas as classes herdadas presentes e futuras possíveis.
Supercat
14

No OO, existem duas maneiras de adicionar funcionalidade ao código existente.

O primeiro é por herança: você pega uma aula e deriva dela. No entanto, a herança deve ser usada com cuidado. Você deve usar a herança pública principalmente quando tiver um relacionamento isA entre a base e a classe derivada (por exemplo, um retângulo é uma forma). Em vez disso, você deve evitar a herança pública para reutilizar uma implementação existente. Basicamente, a herança pública é usada para garantir que a classe derivada tenha a mesma interface que a classe base (ou uma maior), para que você possa aplicar o princípio de substituição de Liskov .

O outro método para adicionar funcionalidade é usar delegação ou composição. Você escreve sua própria classe que usa a classe existente como um objeto interno e delega parte do trabalho de implementação.

Não sei qual foi a ideia por trás de todas as finais da biblioteca que você está tentando usar. Provavelmente, os programadores queriam ter certeza de que você não herdaria uma nova classe da classe deles, porque essa não é a melhor maneira de estender seu código.

knulp
fonte
5
Ótimo ponto com composição vs. herança.
Zsolt Török
+1, mas nunca gostei do exemplo "é uma forma" de herança. Um retângulo e um círculo podem ser duas coisas muito diferentes. Eu gosto mais de usar, um quadrado é um retângulo que é restrito. Retângulos podem ser usados ​​como Formas.
22611 Tylermac
@tylermac: de fato. O caso Quadrado / Retângulo é, na verdade, um dos contra-exemplos do LSP. Provavelmente, eu deveria começar a pensar em um exemplo mais eficaz.
knulp
3
Quadrado / retângulo é apenas um contra-exemplo se você permitir a modificação.
starblue
11

A maioria das respostas tem razão: as classes devem ser seladas (final, NotOverridable, etc) quando o objeto não for projetado ou pretendido para ser estendido.

No entanto, eu não consideraria simplesmente fechar tudo o design de código adequado. O "O" no SOLID é para "Princípio Aberto-Fechado", afirmando que uma classe deve ser "fechada" para modificação, mas "aberta" para extensão. A ideia é que você tenha código em um objeto. Funciona muito bem. Adicionar funcionalidade não deve exigir a abertura desse código e a realização de alterações cirúrgicas que podem interromper o comportamento anterior do trabalho. Em vez disso, deve-se usar a injeção de herança ou dependência para "estender" a classe para fazer outras coisas.

Por exemplo, uma classe "ConsoleWriter" pode obter texto e enviá-lo para o console. Isso faz bem. No entanto, em um determinado caso, você TAMBÉM precisa da mesma saída gravada em um arquivo. Abrir o código do ConsoleWriter, alterar sua interface externa para adicionar um parâmetro "WriteToFile" às ​​funções principais e colocar o código de gravação de arquivo adicional ao lado do código de gravação do console geralmente seria considerado uma coisa ruim.

Em vez disso, você pode fazer uma de duas coisas: você pode derivar do ConsoleWriter para formar ConsoleAndFileWriter e estender o método Write () para chamar primeiro a implementação base e, em seguida, também gravar em um arquivo. Ou, você pode extrair uma interface IWriter do ConsoleWriter e reimplementar essa interface para criar duas novas classes; um FileWriter e um "MultiWriter", que podem receber outros IWriters e transmitirão qualquer chamada feita aos seus métodos a todos os gravadores fornecidos. Qual deles você escolhe depende do que você precisará; A derivação simples é, bem, mais simples, mas se você tiver uma previsão fraca de eventualmente precisar enviar a mensagem para um cliente de rede, três arquivos, dois pipes nomeados e o console, vá em frente e se esforce para extrair a interface e criar o "Adaptador Y"; isto'

Agora, se a função Write () nunca foi declarada virtual (ou foi selada), agora você está com problemas se não controlar o código-fonte. Às vezes, mesmo que você faça. Geralmente, essa é uma posição ruim para a empresa e frustra os usuários de APIs de código fechado ou de fonte limitada sem fim quando isso acontece. Mas, há razões legítimas pelas quais Write () ou toda a classe ConsoleWriter são seladas; pode haver informações confidenciais em um campo protegido (substituído por sua vez a partir de uma classe base para fornecer essas informações). Pode haver validação insuficiente; ao escrever uma API, você deve assumir que os programadores que consomem seu código não são mais inteligentes ou benevolentes que o "usuário final" médio do sistema final; Dessa forma, você nunca fica desapontado.

KeithS
fonte
Bom ponto. A herança pode ser boa ou ruim, por isso é errado dizer que toda classe deve ser selada. Realmente depende do problema em questão.
knulp
2

Eu acho que é provavelmente de todas as pessoas que usam uma linguagem oo para programação c. Eu estou olhando para você, java. Se o seu martelo tiver a forma de um objeto, mas você desejar um sistema processual e / ou realmente não entender as classes, seu projeto precisará ser bloqueado.

Basicamente, se você vai escrever em um idioma oo, seu projeto precisa seguir o paradigma oo, que inclui extensão. Obviamente, se alguém escreveu uma biblioteca em um paradigma diferente, talvez você não deva estendê-la de qualquer maneira.

Spencer Rathbun
fonte
7
BTW, OO e processual são enfaticamente NÃO mutuamente exclusivos. Você não pode ter OO sem procedimentos. Além disso, a composição é tanto um conceito de OO quanto extensão.
Michael K
@ Michael, claro que não. Afirmei que você pode querer um sistema processual , ou seja, um sem conceitos de OO. Vi pessoas usarem Java para escrever código puramente processual e não funcionará se você tentar interagir com ele de maneira OO.
Spencer Rathbun
1
Isso poderia ser resolvido se o Java tivesse funções de primeira classe. Meh.
Michael K
É difícil ensinar Java ou DOTNet Languages ​​para novos alunos. Normalmente, ensino Procedural com Pascal procedural ou "C simples" e, posteriormente, alterno para OO com Pascal de objeto ou "C ++". E deixe o Java para mais tarde. Ensinar programação com programas procedura parece que um único objeto (singleton) funciona bem.
umlcat
@ Michael K: No OOP, uma função de primeira classe é apenas um objeto com exatamente um método.
Giorgio
2

Porque eles não sabem melhor.

Os autores originais provavelmente estão apegados a um mal-entendido dos princípios do SOLID que se originaram em um mundo C ++ confuso e complicado.

Espero que você note que os mundos ruby, python e perl não têm os problemas que as respostas aqui alegam ser o motivo da vedação. Observe que sua digitação ortogonal à dinâmica. Os modificadores de acesso são fáceis de trabalhar na maioria dos idiomas (todos?). Os campos C ++ podem ser modificados ao transmitir para outro tipo (C ++ é mais fraco). Java e C # podem usar reflexão. Os modificadores de acesso tornam as coisas difíceis o suficiente para impedir que você faça isso, a menos que você realmente queira.

O selamento de classes e a marcação de qualquer membro como particular viola explicitamente o princípio de que coisas simples devem ser simples e coisas difíceis devem ser possíveis. De repente, coisas que deveriam ser simples, não são.

Eu o encorajo a tentar entender o ponto de vista dos autores originais. Muito disso vem de uma idéia acadêmica de encapsulamento que nunca demonstrou sucesso absoluto no mundo real. Eu nunca vi uma estrutura ou biblioteca em que algum desenvolvedor em algum lugar não desejasse que funcionasse um pouco diferente e não tivesse boas razões para alterá-lo. Há duas possibilidades que podem ter atormentado os desenvolvedores de software originais que selaram e tornaram os membros privados.

  1. Arrogância - eles realmente acreditavam que estavam abertos para extensão e fechados para modificação
  2. Complacência - eles sabiam que poderia haver outros casos de uso, mas decidiram não escrever para esses casos de uso

Penso que, na estrutura corporativa, o nº 2 é provavelmente o caso. Essas estruturas C ++, Java e .NET precisam ser "concluídas" e precisam seguir certas diretrizes. Essas diretrizes geralmente significam tipos fechados, a menos que o tipo tenha sido explicitamente projetado como parte de uma hierarquia de tipos e membros privados para muitas coisas que podem ser úteis para outras pessoas usarem. Mas, como eles não se relacionam diretamente a essa classe, eles não são expostos . Extrair um novo tipo seria muito caro para suportar, documentar, etc ...

A idéia por trás dos modificadores de acesso é que os programadores devem ser protegidos de si mesmos. "A programação C é ruim porque permite que você atire no próprio pé." Não é uma filosofia com a qual concordo como programador.

Eu prefiro muito a abordagem confusa do nome do python. Você pode facilmente (muito mais fácil do que refletir) substituir substitutos, se necessário. Uma excelente descrição está disponível aqui: http://bytebaker.com/2009/03/31/python-properties-vs-java-access-modifiers/

O modificador privado de Ruby é mais como protegido em C # e não tem um privado como modificador de C #. Protegido é um pouco diferente. Há uma grande explicação aqui: http://www.ruby-lang.org/en/documentation/ruby-from-other-languages/

Lembre-se de que sua linguagem estática não precisa estar em conformidade com os estilos antiquados do código escrito naquele idioma.

jrwren
fonte
5
"o encapsulamento nunca demonstrou sucesso absoluto". Eu teria pensado que todos concordariam que a falta de encapsulamento causou muitos problemas.
11138 Joh
5
-1. Tolos correm para lugares onde anjos temem pisar. Celebrar a capacidade de alterar variáveis ​​privadas mostra uma falha grave no seu estilo de codificação.
Rjalk
2
Além de ser discutível e provavelmente errado, essa publicação também é bastante arrogante e ofensiva. Bem feito.
21911 Konrad Rudolph
1
Existem duas escolas de pensamentos cujos defensores parecem não se dar bem. O pessoal de digitação estática quer que seja fácil raciocinar sobre o software, o pessoal dinâmico quer que seja fácil adicionar funcionalidade. Qual é o melhor depende basicamente da vida útil esperada do projeto.
Joh
1

EDIT: Eu acredito que as aulas devem ser projetadas para serem abertas. Com algumas restrições, mas não deve ser fechado para herança.

Por que: "Aulas finais" ou "aulas seladas" me parecem estranhas. Nunca preciso marcar uma de minhas próprias classes como "final" ("selada"), porque talvez precise herdar essas classes mais tarde.

Comprei / baixei bibliotecas de terceiros com classes (controles mais visuais) e realmente grato que nenhuma dessas bibliotecas tenha usado "final", mesmo se suportada pela linguagem de programação, na qual elas foram codificadas, porque eu acabei de estender essas classes, mesmo se eu compilei bibliotecas sem o código fonte.

Às vezes, eu tenho que lidar com algumas classes "seladas" ocasionais e acabei fazendo um novo "wrapper" de classe contendo a classe dada, que tem membros semelhantes.

class MyBaseWrapper {
  protected FinalClass _FinalObject;

  public virtual DoSomething()
  {
    // these method cannot be overriden,
    // its from a "final" class
    // the wrapper method can  be open and virtual:
    FinalObject.DoSomething();
  }
} // class MyBaseWrapper


class MyBaseWrapper: MyBaseWrapper {

  public override DoSomething()
  {
    // the wrapper method allows "overriding",
    // a method from a "final" class:
    DoSomethingNewBefore();
    FinalObject.DoSomething();
    DoSomethingNewAfter();
  }
} // class MyBaseWrapper

Os classificadores de escopo permitem "selar" alguns recursos sem restringir a herança.

Quando mudei do Structured Progr. para OOP, comecei a usar membros da classe privada , mas, depois de muitos projetos, acabei usando o protected e, eventualmente, promovi essas propriedades ou métodos para o público , se necessário.

PS: Eu não gosto de "final" como palavra-chave em C #; prefiro usar "selado" como Java, ou "estéril", seguindo a metáfora da herança. Além disso, "final" é uma palavra ambígua usada em vários contextos.

umlcat
fonte
-1: Você declarou que gosta de designs abertos, mas isso não responde à pergunta, razão pela qual as classes não devem ser projetadas para serem abertas.
Joh
@ Oh, desculpe, eu não me expliquei bem, tentei expressar a opinião oposta, não deveria ser selada. Obrigado por descrever por que você discorda.
umlcat