Devo sempre encapsular inteiramente uma estrutura de dados interna?

11

Por favor, considere esta classe:

class ClassA{

    private Thing[] things; // stores data

    // stuff omitted

    public Thing[] getThings(){
        return things;
    }

}

Essa classe expõe a matriz usada para armazenar dados, para qualquer código de cliente interessado.

Eu fiz isso em um aplicativo no qual estou trabalhando. Eu tive uma ChordProgressionclasse que armazena uma sequência de Chords (e faz algumas outras coisas). Ele tinha um Chord[] getChords()método que retornava a matriz de acordes. Quando a estrutura de dados precisou ser alterada (de uma matriz para uma ArrayList), todo o código do cliente foi quebrado.

Isso me fez pensar - talvez a seguinte abordagem seja melhor:

class ClassA{

    private Thing[] things; // stores data

    // stuff omitted

    public Thing[] getThing(int index){
        return things[index];
    }

    public int getDataSize(){
        return things.length;
    }

    public void setThing(int index, Thing thing){
        things[index] = thing;
    }

}

Em vez de expor a própria estrutura de dados, agora todas as operações oferecidas pela estrutura de dados são oferecidas diretamente pela classe que a encerra, usando métodos públicos que delegam na estrutura de dados.

Quando a estrutura de dados muda, apenas esses métodos precisam ser alterados - mas depois disso, todo o código do cliente ainda funciona.

Observe que coleções mais complexas que matrizes podem exigir que a classe envolvente implemente ainda mais de três métodos apenas para acessar a estrutura de dados interna.


Essa abordagem é comum? O que você acha disso? Que desvantagens tem outras? É razoável que a classe envolvente implemente pelo menos três métodos públicos apenas para delegar à estrutura de dados interna?

Aviv Cohn
fonte

Respostas:

14

Código como:

   public Thing[] getThings(){
        return things;
    }

Não faz muito sentido, pois seu método de acesso não está fazendo nada além de retornar diretamente a estrutura de dados interna. Você também pode simplesmente declarar Thing[] thingsser public. A idéia por trás de um método de acesso é criar uma interface que isola os clientes de alterações internas e os impede de manipular a estrutura de dados real, exceto de maneiras discretas, conforme permitido pela interface. Como você descobriu quando todo o código do seu cliente quebrou, seu método de acesso não fez isso - é apenas um código desperdiçado. Eu acho que muitos programadores tendem a escrever código assim porque aprenderam em algum lugar que tudo precisa ser encapsulado com métodos de acesso - mas foi por isso que expliquei. Fazer isso apenas para "seguir a forma" quando o método de acesso não serve a nenhum propósito é apenas ruído.

Definitivamente, eu recomendaria sua solução proposta, que cumpre alguns dos objetivos mais importantes do encapsulamento: Oferecer aos clientes uma interface robusta e discreta que os isole dos detalhes de implementação interna da sua classe e não permita que eles toquem na estrutura de dados interna espere da maneira que você decidir ser apropriada - "a lei do privilégio menos necessário". Se você observar as grandes estruturas populares de OOP, como o CLR, o STL, o VCL, o padrão que você propôs é generalizado, exatamente por esse motivo.

Você sempre deve fazer isso? Não necessariamente. Por exemplo, se você tem classes auxiliares ou amigas que são essencialmente um componente da sua classe principal de trabalhadores e não são "voltadas para a frente", isso não é necessário - é um exagero que adicionará muito código desnecessário. E, nesse caso, eu não usaria um método de acesso - não faz sentido, conforme explicado. Apenas declare a estrutura de dados de uma maneira que tenha como escopo apenas a classe principal que a usa - a maioria dos idiomas suporta maneiras de fazer isso - friendou declare-a no mesmo arquivo da classe principal de trabalho, etc.

A única desvantagem que vejo na sua proposta é que é mais trabalhoso codificar (e agora você terá que re-codificar suas classes de consumidor - mas você precisa / teve que fazer isso de qualquer maneira.) Mas isso não é realmente uma desvantagem - você precisa fazer o que é certo, e às vezes isso exige mais trabalho.

Uma das coisas que torna um bom programador bom é que eles sabem quando o trabalho extra vale a pena e quando não vale. A longo prazo, colocar o extra agora renderá grandes dividendos no futuro - se não neste projeto, então em outros. Aprenda a codificar da maneira certa e use sua mente para isso, não apenas siga roboticamente os formulários prescritos.

Observe que coleções mais complexas que matrizes podem exigir que a classe envolvente implemente ainda mais de três métodos apenas para acessar a estrutura de dados interna.

Se você está expondo uma estrutura de dados inteira por meio de uma classe que contém, na IMO, é necessário pensar no porquê dessa classe ser encapsulada, se não for apenas para fornecer uma interface mais segura - uma "classe de wrapper". Você está dizendo que a classe que contém não existe para esse fim - então talvez haja algo errado no seu design. Considere dividir suas aulas em módulos mais discretos e colocá-los em camadas.

Uma classe deve ter um objetivo claro e discreto e fornecer uma interface para suportar essa funcionalidade - nada mais. Você pode estar tentando juntar coisas que não pertencem uma à outra. Quando você faz isso, as coisas vão quebrar toda vez que você precisa implementar uma mudança. Quanto menores e mais discretas forem suas aulas, mais fácil será mudar as coisas: pense em LEGO.

Vetor
fonte
1
Obrigado por responder. Uma pergunta: e se a estrutura de dados interna tiver, talvez, cinco métodos públicos - que todos precisam ser apresentados pela interface pública da minha classe? Por exemplo, um Java ArrayList tem os seguintes métodos: get(index), add(), size(), remove(index), e remove(Object). Usando a técnica proposta, a classe que contém esse ArrayList deve ter cinco métodos públicos apenas para delegar à coleção interna. E o objetivo dessa classe no programa provavelmente não está encapsulando esse ArrayList, mas fazendo outra coisa. O ArrayList é apenas um detalhe. [...]
Aviv Cohn
A estrutura de dados interna é apenas um membro comum, que, usando a técnica acima - requer que ela contenha classe para apresentar cinco métodos públicos adicionais. Na sua opinião - isso é razoável? E também - isso é comum?
Aviv Cohn
@Prog - E se a estrutura de dados interna tiver, talvez, 5 métodos públicos ... IMO se você achar que precisa agrupar uma classe auxiliar inteira dentro da classe principal e expô-la dessa maneira, é necessário repensar que design - sua classe pública está fazendo muito / ou não apresentando a interface apropriada. Uma classe deve ter um papel muito discreto e claramente definido, e sua interface deve suportar esse papel e somente esse papel. Pense em terminar e estratificar suas aulas. Uma classe não deve ser uma "pia de cozinha" que contenha todos os tipos de objetos em nome do encapsulamento.
Vector
Se você está expondo uma estrutura de dados inteira por meio de uma classe de wrapper, na IMO, é necessário pensar no porquê dessa classe ser encapsulada, se não for apenas para fornecer uma interface mais segura. Você está dizendo que a classe que contém não existe para esse fim - então há algo errado nesse design.
Vector
1
@ Phoshi - Somente leitura é a palavra - chave - posso concordar com isso. Mas o OP não está falando apenas de leitura. por exemplo, removenão é somente leitura. Meu entendimento é que o OP quer tornar tudo público - como no código original antes da alteração proposta. public Thing[] getThings(){return things;}É disso que eu não gosto.
Vector
2

Você perguntou: Devo sempre encapsular inteiramente uma estrutura de dados interna?

Resposta breve: Sim, na maioria das vezes, mas nem sempre .

Resposta longa: Eu acho que as aulas seguem as seguintes categorias:

  1. Classes que encapsulam dados simples. Exemplo: ponto 2D. É fácil criar funções públicas que ofereçam a capacidade de obter / definir as coordenadas X e Y, mas você pode ocultar os dados internos facilmente sem grandes problemas. Para essas classes, não é necessário expor os detalhes da estrutura de dados internos.

  2. Classes de contêiner que encapsulam coleções. STL tem as classes de contêiner clássicas. Eu considero std::stringe std::wstringentre aqueles também. Eles fornecem uma interface rica para lidar com as abstrações, mas std::vector, std::stringe std::wstringtambém fornecem a capacidade de obter acesso aos dados brutos. Eu não teria pressa em chamá-los de aulas mal planejadas. Não sei a justificativa para essas classes que expõem seus dados brutos. No entanto, no meu trabalho, achei necessário expor os dados brutos ao lidar com milhões de nós de malha e dados nesses nós de malha por razões de desempenho.

    O importante sobre a exposição da estrutura interna de uma classe é que você precisa pensar muito antes de emitir um sinal verde. Se a interface for interna de um projeto, será caro alterá-la no futuro, mas não impossível. Se a interface for externa ao projeto (como quando você estiver desenvolvendo uma biblioteca que será usada por outros desenvolvedores de aplicativos), pode ser impossível alterar a interface sem perder seus clientes.

  3. Classes que são principalmente de natureza funcional. Exemplos: std::istream, std::ostream, iterators dos recipientes STL. É tolice expor os detalhes internos dessas classes.

  4. Classes híbridas. Essas são classes que encapsulam alguma estrutura de dados, mas também fornecem funcionalidade algorítmica. Pessoalmente, acho que isso é resultado de um design mal pensado. No entanto, se você os encontrar, precisará decidir se faz sentido expor seus dados internos caso a caso.

Conclusão: A única vez em que achei necessário expor a estrutura de dados interna de uma classe é quando ela se tornou um gargalo de desempenho.

R Sahu
fonte
Eu acho que a razão mais importante pela qual o STL expõe seus dados internos é a compatibilidade com todas as funções que esperam ponteiros, que são muitas.
Siyuan Ren
0

Em vez de retornar os dados brutos diretamente, tente algo como isto

class ClassA {
  private Things[] things;
  ...
  public Things[] asArray() { return things; }
  public List<Thing> asList() { ... }
  ...
}

Portanto, você está fornecendo essencialmente uma coleção personalizada que apresenta qualquer rosto que o mundo deseje. Na sua nova implementação, então,

class ClassA {
  private List<Thing> things;
  ...
  public Things[] asArray() { return things.asArray(); }
  public List<Thing> asList() { return things; }
  ...
}

Agora você tem o encapsulamento adequado, oculta os detalhes da implementação e fornece compatibilidade com versões anteriores (a um custo).

BobDalgleish
fonte
Idéia inteligente para compatibilidade com versões anteriores. Mas: agora você tem o encapsulamento adequado, oculte os detalhes da implementação - na verdade não. Os clientes ainda precisam lidar com as nuances de List. Os métodos de acesso que simplesmente retornam membros de dados, mesmo com uma conversão para tornar as coisas mais robustas, não são realmente um bom IMO de encapsulamento. A classe de trabalhadores deve lidar com tudo isso, não o cliente. Quanto mais burro um cliente tiver, mais robusto ele será. Como um aparte, não tenho certeza de que você respondeu à pergunta ...
Vector
1
@ Vector - você está correto. A estrutura de dados retornada ainda é mutável e os efeitos colaterais matam a ocultação de informações.
27414 BobDalgleish
A estrutura de dados retornada ainda é mutável e os efeitos colaterais matam a ocultação de informações - sim, também - é perigoso. Eu estava simplesmente pensando em termos do que é exigido do cliente, que era o foco da pergunta.
Vector
@BobDalgleish: por que não retornar uma cópia da coleção original?
Giorgio
1
@BobDalgleish: A menos que haja boas razões de desempenho, eu consideraria retornar uma referência a estruturas de dados internas para permitir que seus usuários as alterassem como uma péssima decisão de design. O estado interno de um objeto só deve ser alterado através de métodos públicos apropriados.
Giorgio
0

Você deve usar interfaces para essas coisas. Não ajudaria no seu caso, já que a matriz do Java não implementa essas interfaces, mas você deve fazê-lo a partir de agora:

class ClassA{

    public ClassA(){
        things = new ArrayList<Thing>();
    }

    private List<Thing> things; // stores data

    // stuff omitted

    public List<Thing> getThings(){
        return things;
    }

}

Dessa forma, você pode mudar ArrayListpara LinkedListou qualquer outra coisa e não quebra nenhum código, pois provavelmente todas as coleções Java (além das matrizes) que têm acesso aleatório (pseudo?) Serão implementadas List.

Você também pode usar Collection, que oferecem menos métodos do que Listmas podem suportar coleções sem acesso aleatório, ou Iterableque podem até suportar fluxos, mas não oferecem muito em termos de métodos de acesso ...

Idan Arye
fonte
-1 - comprometimento ruim e IMO não particularmente seguro: você está expondo a estrutura de dados interna ao cliente, mascarando-a e esperando o melhor porque "Coleções Java ... provavelmente implementarão a Lista". Se sua solução fosse realmente baseada em polimórficos / herança - que todas as coleções invariavelmente implementam Listcomo classes derivadas, faria mais sentido, mas apenas "esperar pelo melhor" não é uma boa idéia. "Um bom programador olha nos dois sentidos em uma rua de mão única".
Vector
@Vector Sim, estou assumindo que futuras coleções Java serão implementadas List(ou Collection, ou pelo menos Iterable). Esse é o objetivo dessas interfaces, e é uma pena que as Java Arrays não as implementem, mas elas são uma interface oficial para coleções em Java, portanto, não é tão difícil supor que qualquer coleção Java as implemente - a menos que essa coleção seja mais antiga do que List, e, nesse caso, é muito fácil para envolvê-lo com AbstractList .
Idan Arye
Você está dizendo que sua suposição é praticamente garantida, então tudo bem - eu removerei o voto negativo (quando for permitido) porque você era decente o suficiente para explicar e eu não sou um cara Java exceto por osmose. Ainda assim, não apoio essa ideia de expor a estrutura de dados interna, independentemente de como isso é feito, e você não respondeu diretamente à pergunta do OP, que é realmente sobre encapsulamento. isto é, limitar o acesso à estrutura interna de dados.
Vector
1
@Vector Sim, os usuários podem converter o arquivo Listem ArrayList, mas não é como se a implementação pudesse ser 100% protegida - você sempre pode usar a reflexão para acessar campos privados. Fazer isso é desaprovado, mas o elenco também é desaprovado (embora não muito). O objetivo do encapsulamento não é impedir a invasão mal-intencionada, mas impedir que os usuários dependam dos detalhes da implementação que você pode querer alterar. O uso da Listinterface faz exatamente isso - os usuários da classe podem depender da Listinterface, em vez da ArrayListclasse concreta que pode mudar.
Idan Arye
você sempre pode usar a reflexão para acessar campos privados com certeza; se alguém quiser escrever um código incorreto e subverter um design, poderá fazê-lo. pelo contrário, é para impedir os usuários ... - esse é um motivo para o encapsulamento. Outra é garantir a integridade e consistência do estado interno da sua classe. O problema não é "hackers maliciosos", mas uma organização deficiente que leva a erros desagradáveis. "Lei do privilégio menos necessário" - dê ao consumidor da sua classe apenas o que é obrigatório - nada mais. Se for obrigatório que você torne pública uma estrutura de dados interna inteira, você terá um problema de design.
Vector
-2

Isso é bastante comum para ocultar sua estrutura de dados interna do mundo exterior. Às vezes é um exagero, especialmente no DTO. Eu recomendo isso para o modelo de domínio. Se for necessário expor, devolva a cópia imutável. Junto com isso, sugiro criar uma interface com esses métodos, como obter, definir, remover etc.

VGaur
fonte
1
este não parece oferecer nada substancial ao longo de 3 respostas anteriores
mosquito