Por que as classes não devem ser projetadas para serem “abertas”?

44

Ao ler várias perguntas sobre estouro de pilha e o código de outras pessoas, o consenso geral sobre como criar 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 aulas são finais .

A ideia por trás disso é ocultar os detalhes da implementação, o que é um bom motivo. No entanto, com a existência de protected na maioria das linguagens OOP e polimorfismo, isso não funciona.

Sempre que desejo adicionar ou alterar funcionalidades a uma aula, geralmente sou prejudicado por colocações particulares e finais em todos os lugares. Aqui detalhes de implementação são importantes: você está tomando a implementação e estendendo-a, sabendo muito bem quais são as consequências. No entanto, como não consigo acessar os campos e métodos privados e finais, tenho três opções:

  • Não prolongue a aula, apenas contorne o problema que leva a códigos mais complexos
  • Copie e cole toda a turma, matando a reutilização de código
  • bifurque o projeto

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

    
por TheLQ 12.07.2011 / 20:01
fonte

8 respostas

55

Projetar classes para funcionar corretamente quando estendido, especialmente quando o programador que faz a extensão não entende totalmente 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 chamem um método. É preciso pensar cuidadosamente.

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

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

    
por 12.07.2011 / 21:40
fonte
24

Tornar tudo privado por padrão parece rude, mas olhe para ele do outro lado: quando tudo é privado por padrão, tornar algo público (ou protegido, que é quase a mesma coisa) é uma escolha consciente; é o contrato do autor da aula com você, o consumidor, sobre como usar a classe. Essa é 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 ideia subjacente é 'acoplamento solto' (também chamado de 'interfaces estreitas'); seu valor está 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 protegem e publicam os membros nos lugares certos e ocultam todo o resto.

    
por 12.07.2011 / 21:44
fonte
19

Tudo o que é não privado é mais ou menos suposto existir com o 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.

Em relação a "final" resp. "selado" classes, um caso típico são classes imutáveis. Muitas partes do framework dependem de strings serem imutáveis. Se a classe String não fosse final, seria fácil (mesmo 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.

    
por 13.07.2011 / 01:07
fonte
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 a ela.

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 dos seus, porque essa não é a melhor maneira de estender o código deles.

    
por 12.07.2011 / 22:35
fonte
11

A maioria das respostas está correta: as classes devem ser seladas (final, NotOverridable, etc) quando o objeto não é projetado ou se destina a ser estendido.

No entanto, eu não consideraria simplesmente fechar todo o design de código adequado. O "O" em 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. Adicionando funcionalidade não deve exigir a abertura desse código e fazer alterações cirúrgicas que podem quebrar o comportamento anterior de trabalho. Em vez disso, deve-se poder usar a injeção de herança ou dependência para "estender" a classe para fazer coisas adicionais.

Por exemplo, uma classe "ConsoleWriter" pode obter texto e gerar a saída 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 adicional de gravação de arquivo próximo ao código de gravação do console geralmente seria considerado uma coisa ruim.

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

Agora, se a função Write () nunca tivesse sido declarada virtual (ou estava selada), agora você está com problemas se não controlar o código-fonte. Às vezes até mesmo se você fizer. Em geral, essa é uma posição ruim para se estar, e isso frustra os usuários de APIs de código-fonte fechado ou de código-fonte limitado quando isso acontece. Mas, há razão legítima por que o Write (), ou a classe inteira ConsoleWriter, são selados; pode haver informações confidenciais em um campo protegido (substituído, por sua vez, de uma classe base para fornecer a referida informação). Pode haver validação insuficiente; quando você escreve uma API, você tem que assumir que os programadores que consomem seu código não são mais inteligentes ou benevolentes do que o "usuário final" médio do sistema final; Dessa forma, você nunca está desapontado. Você aproveita o tempo para validar o que seu consumidor pode fazer para estender sua API ou bloqueia a API como o Fort Knox, para que eles possam fazer exatamente o que você espera.

    
por 13.07.2011 / 01:27
fonte
2

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

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

    
por 12.07.2011 / 20:09
fonte
2

Porque eles não sabem nada melhor.

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

Espero que você perceba que os mundos de rubi, python e perl não têm os problemas que as respostas aqui afirmam ser o motivo da vedação. Observe que é ortogonal à digitação dinâmica. Os modificadores de acesso são fáceis de trabalhar na maioria dos idiomas. C ++ campos podem ser descartados por conversão para algum 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.

Classes de selamento e marcação de quaisquer membros privados violam explicitamente o princípio de que coisas simples devem ser simples e difíceis, coisas que devem ser possíveis. De repente, coisas que deveriam ser simples, não são.

Eu encorajaria você a tentar entender o ponto de vista dos autores originais. Muito disso é 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 de maneira um pouco diferente e não tivesse uma boa razão para alterá-lo. Existem 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

Eu acho que no quadro corporativo, # 2 é provavelmente o caso. Esses frameworks C ++, Java e .NET precisam ser "feitos" e eles precisam seguir certas diretrizes. Essas diretrizes geralmente significam tipos selados, a menos que o tipo tenha sido explicitamente projetado como parte de uma hierarquia de tipos e membros privados para muitas coisas que possam ser úteis para outros usarem .. mas como eles não se relacionam diretamente com essa classe, eles não são expostos . Extrair um novo tipo seria muito caro para suportar, documentar, etc ...

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

Eu prefiro muito mais a abordagem mangling do nome do Python. Você pode facilmente (muito mais fácil que reflexão) substituir privates se você precisar. Um ótimo writeup sobre ele está disponível aqui: link

O modificador privado de Ruby é na verdade mais parecido com protegido em C # e não possui um modificador privado como C #. Protegido é um pouco diferente. Há uma grande explicação aqui: link

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

    
por 13.07.2011 / 04:11
fonte
1

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

Por quê: "Aulas finais" ou "aulas seladas" me parecem estranhas. Eu nunca tenho que marcar uma das minhas aulas como "final" ("selado"), porque eu posso ter que herdar essas classes, mais tarde.

Eu tenho comprado / baixado bibliotecas de terceiros com classes, (a maioria dos controles visuais), e realmente grato nenhuma dessas bibliotecas usou "final", mesmo se suportado pela linguagem de programação, onde eles foram codificados, porque acabei de estender essas classes, mesmo que eu tenha compilado bibliotecas sem o código fonte.

Às vezes, eu tenho que lidar com algumas classes "fechadas" ocasionais, e acabei criando uma nova classe "wrapper" contendo a classe dada, que tem membros similares.

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 clasificadores de escopo permitem "selar" alguns recursos sem restringir a herança.

Quando eu mudei de Progr. Estruturado. para OOP, comecei a usar membros da classe private , mas, depois de muitos projetos, acabei usando protected e, eventualmente, promovi essas propriedades ou métodos para public , se necessário.

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

    
por 13.07.2011 / 00:53
fonte

Tags