¿Debo encapsular siempre una estructura de datos interna por completo?

11

Por favor considere esta clase:

class ClassA{

    private Thing[] things; // stores data

    // stuff omitted

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

}

Esta clase expone la matriz que usa para almacenar datos, a cualquier código de cliente interesado.

Hice esto en una aplicación en la que estoy trabajando. Tuve una ChordProgressionclase que almacena una secuencia de Chords (y hace algunas otras cosas). Tenía un Chord[] getChords()método que devolvía el conjunto de acordes. Cuando la estructura de datos tuvo que cambiar (de una matriz a una ArrayList), todo el código del cliente se rompió.

Esto me hizo pensar, quizás el siguiente enfoque es mejor:

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;
    }

}

En lugar de exponer la estructura de datos en sí, todas las operaciones ofrecidas por la estructura de datos ahora son ofrecidas directamente por la clase que la incluye, utilizando métodos públicos que delegan a la estructura de datos.

Cuando la estructura de datos cambia, solo estos métodos tienen que cambiar, pero después de que lo hacen, todo el código del cliente aún funciona.

Tenga en cuenta que las colecciones más complejas que las matrices pueden requerir que la clase adjunta implemente incluso más de tres métodos solo para acceder a la estructura de datos interna.


¿Es común este enfoque? ¿Qué piensas de esto? ¿Qué inconvenientes tiene otro? ¿Es razonable que la clase adjunta implemente al menos tres métodos públicos solo para delegar en la estructura de datos interna?

Aviv Cohn
fuente

Respuestas:

14

Código como:

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

No tiene mucho sentido, ya que su método de acceso no está haciendo nada más que devolver directamente la estructura de datos interna. Es lo mismo que simplemente declara Thing[] thingsser public. La idea detrás de un método de acceso es crear una interfaz que aísle a los clientes de los cambios internos y les impida manipular la estructura de datos real, excepto en formas discretas, según lo permita la interfaz. Como descubrió cuando se rompió todo el código de su cliente, su método de acceso no lo hizo, es solo código desperdiciado. Creo que muchos programadores tienden a escribir código como ese porque aprendieron en alguna parte que todo debe encapsularse con métodos de acceso, pero eso es por las razones que expliqué. Hacerlo solo para "seguir la forma" cuando el método de acceso no sirve para nada es solo ruido.

Definitivamente recomendaría su solución propuesta, que cumple algunos de los objetivos más importantes de la encapsulación: brindar a los clientes una interfaz sólida y discreta que los aísle de los detalles de implementación internos de su clase y no les permita tocar la estructura de datos interna espere en las formas que usted decida que son apropiadas: "la ley del privilegio menos necesario". Si observa los grandes marcos de OOP populares, como el CLR, el STL, el VCL, el patrón que ha propuesto está muy extendido, exactamente por esa razón.

¿Deberías hacer eso siempre ? No necesariamente. Por ejemplo, si tiene clases auxiliares o de amigos que son esencialmente un componente de su clase principal de trabajo y no están "orientadas al frente", no es necesario, es una exageración que agregará una gran cantidad de código innecesario. Y en ese caso, no usaría ningún método de acceso, no tiene sentido, como se explicó. Simplemente declare la estructura de datos de una manera que esté limitada solo a la clase principal que la usa, la mayoría de los idiomas admiten formas de hacerlo friend, o declarándola en el mismo archivo que la clase de trabajador principal, etc.

El único inconveniente que puedo ver en su propuesta es que es más trabajo codificar (y ahora tendrá que volver a codificar sus clases de consumidor, pero debe / debe hacerlo de todos modos). Pero eso no es realmente un inconveniente - necesitas hacerlo bien, y a veces eso requiere más trabajo.

Una de las cosas que hace que un buen programador sea bueno es que saben cuándo vale la pena el trabajo extra y cuándo no. A la larga, poner el extra ahora dará sus frutos con grandes dividendos en el futuro, si no en este proyecto, en otros. Aprenda a codificar de la manera correcta y use su mente al respecto, no solo siga los formularios prescritos robóticamente.

Tenga en cuenta que las colecciones más complejas que las matrices pueden requerir que la clase adjunta implemente incluso más de tres métodos solo para acceder a la estructura de datos interna.

Si está exponiendo una estructura de datos completa a través de una clase que contiene, en mi opinión, debe pensar por qué esa clase está encapsulada, si no es simplemente para proporcionar una interfaz más segura: una "clase de envoltura". Estás diciendo que la clase que contiene no existe para ese propósito, así que tal vez hay algo que no está bien en tu diseño. Considere dividir sus clases en módulos más discretos y superponerlos.

Una clase debe tener un propósito claro y discreto, y proporcionar una interfaz para admitir esa funcionalidad, nada más. Puede que estés tratando de agrupar cosas que no estén juntas. Cuando haces eso, las cosas se romperán cada vez que tengas que implementar un cambio. Cuanto más pequeñas y discretas sean sus clases, más fácil será cambiar las cosas: piense en LEGO.

Vector
fuente
1
Gracias por responder. Una pregunta: ¿Qué pasa si la estructura de datos interna tiene, quizás, 5 métodos públicos, que todos necesitan ser presentados por la interfaz pública de mi clase? Por ejemplo, un ArrayList Java tiene los siguientes métodos: get(index), add(), size(), remove(index), y remove(Object). Usando la técnica propuesta, la clase que contiene esta ArrayList debe tener cinco métodos públicos solo para delegar en la colección interna. Y el propósito de esta clase en el programa probablemente no sea encapsular esta ArrayList, sino más bien hacer otra cosa. ArrayList es solo un detalle. [...]
Aviv Cohn
La estructura de datos interna es solo un miembro ordinario que, utilizando la técnica anterior, requiere que contenga la clase para presentar cinco métodos públicos adicionales. En su opinión, ¿es esto razonable? Y también, ¿es esto común?
Aviv Cohn
@Prog: ¿qué pasa si la estructura de datos interna tiene, tal vez, 5 métodos públicos ... OMI si encuentra que necesita envolver una clase auxiliar completa dentro de su clase principal y exponerla de esa manera, necesita repensar que diseño: su clase pública está haciendo demasiado y / o no presenta la interfaz adecuada. Una clase debe tener un rol muy discreto y claramente definido, y su interfaz debe admitir ese rol y solo ese rol. Piensa en dividir y superponer tus clases. Una clase no debe ser un "fregadero de cocina" que contenga todo tipo de objetos en nombre de la encapsulación.
Vector
Si está exponiendo una estructura de datos completa a través de una clase contenedora, en mi opinión, debe pensar por qué esa clase está encapsulada si no es simplemente para proporcionar una interfaz más segura. Estás diciendo que la clase que contiene no existe para ese propósito, por lo que hay algo que no está bien en este diseño.
Vector
1
@Phoshi: la palabra clave es de solo lectura , estoy de acuerdo con eso. Pero el OP no está hablando de solo lectura. por ejemplo removeno es solo de lectura. Entiendo que el OP quiere hacer público todo, como en el código original antes del cambio propuesto. public Thing[] getThings(){return things;}Eso es lo que no me gusta.
Vector
2

Usted preguntó: ¿Debería encapsular siempre una estructura de datos interna por completo?

Breve respuesta: Sí, la mayoría de las veces pero no siempre .

Respuesta larga: Creo que las clases siguen las siguientes categorías:

  1. Clases que encapsulan datos simples. Ejemplo: punto 2D. Es fácil crear funciones públicas que brinden la capacidad de obtener / establecer las coordenadas X e Y, pero puede ocultar los datos internos fácilmente sin demasiados problemas. Para tales clases, no es necesario exponer los detalles internos de la estructura de datos.

  2. Clases de contenedores que encapsulan colecciones. STL tiene las clases clásicas de contenedores. Lo considero std::stringy std::wstringentre esos también. Proporcionan una interfaz rica para hacer frente a las abstracciones, sino std::vector, std::stringy std::wstringtambién proporcionan la capacidad de obtener acceso a los datos en bruto. No me apresuraría a llamarlas clases mal diseñadas. No sé la justificación para estas clases que exponen sus datos en bruto. Sin embargo, en mi trabajo, he encontrado que es necesario exponer los datos sin procesar cuando se trata con millones de nodos de malla y datos en esos nodos de malla por razones de rendimiento.

    Lo importante de exponer la estructura interna de una clase es que tienes que pensar mucho antes de darle una señal verde. Si la interfaz es interna a un proyecto, será costoso cambiarla en el futuro pero no imposible. Si la interfaz es externa al proyecto (como cuando está desarrollando una biblioteca que será utilizada por otros desarrolladores de aplicaciones), puede ser imposible cambiar la interfaz sin perder sus clientes.

  3. Clases que son en su mayoría funcionales por naturaleza. Ejemplos: std::istream, std::ostream, iteradores de los contenedores STL. Es completamente tonto exponer los detalles internos de estas clases.

  4. Clases híbridas Estas son clases que encapsulan cierta estructura de datos pero también proporcionan funcionalidad algorítmica. Personalmente, creo que estos son el resultado de un diseño mal pensado. Sin embargo, si los encuentra, debe decidir si tiene sentido exponer sus datos internos caso por caso.

En conclusión: la única vez que he encontrado necesario exponer la estructura interna de datos de una clase es cuando se convirtió en un cuello de botella en el rendimiento.

R Sahu
fuente
Creo que la razón más importante por la que STL expone sus datos internos es la compatibilidad con todas las funciones que esperan punteros, que son muchas.
Siyuan Ren
0

En lugar de devolver los datos sin procesar directamente, intente algo como esto

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

Por lo tanto, básicamente está proporcionando una colección personalizada que presenta cualquier cara al mundo que se desee. En su nueva implementación, entonces,

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

Ahora tiene la encapsulación adecuada, oculta los detalles de implementación y proporciona compatibilidad con versiones anteriores (a un costo).

BobDalgleish
fuente
Idea inteligente para la compatibilidad con versiones anteriores. Pero: ahora que tiene la encapsulación adecuada, oculte los detalles de implementación , en realidad no. Los clientes todavía tienen que lidiar con los matices de List. Los métodos de acceso que simplemente devuelven miembros de datos, incluso con un reparto para hacer las cosas más robustas, no son realmente buenas encapsulaciones IMO. La clase trabajadora debería manejar todo eso, no el cliente. Cuanto más "tonto" sea un cliente, más robusto será. Como comentario aparte, no estoy seguro de que hayas respondido la pregunta ...
Vector
1
@Vector: tienes razón. La estructura de datos devuelta sigue siendo mutable y los efectos secundarios matarán la información oculta.
BobDalgleish
La estructura de datos devuelta sigue siendo mutable y los efectos secundarios matarán la ocultación de la información , sí, eso también, es peligroso. Simplemente estaba pensando en términos de lo que se requiere del cliente, que era el foco de la pregunta.
Vector
@BobDalgleish: ¿por qué no devolver una copia de la colección original?
Giorgio
1
@BobDalgleish: a menos que haya buenas razones de rendimiento, consideraría devolver una referencia a las estructuras de datos internas para permitir a sus usuarios cambiarlas como una muy mala decisión de diseño. El estado interno de un objeto solo debe cambiarse a través de métodos públicos apropiados.
Giorgio
0

Deberías usar interfaces para esas cosas. No ayudaría en su caso, ya que la matriz de Java no implementa esas interfaces, pero debe hacerlo a partir de ahora:

class ClassA{

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

    private List<Thing> things; // stores data

    // stuff omitted

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

}

De esa manera usted puede cambiar ArrayLista LinkedListo cualquier otra cosa, y no se va a romper cualquier código dado que todas las colecciones de Java (al lado de arrays) que tienen (pseudo?) De acceso aleatorio probablemente implemento List.

También puede usar Collection, que ofrece menos métodos que, Listpero puede admitir colecciones sin acceso aleatorio, o Iterableincluso puede admitir transmisiones, pero no ofrece mucho en términos de métodos de acceso ...

Idan Arye
fuente
-1: compromiso deficiente y una OMI no particularmente segura: está exponiendo la estructura de datos interna al cliente, simplemente enmascarándola y esperando lo mejor porque "las colecciones de Java ... probablemente implementarán List". Si su solución se basara realmente en la herencia polimórfica, que todas las colecciones se implementan invariablemente Listcomo clases derivadas, tendría más sentido, pero simplemente "esperar lo mejor" no es una buena idea. "Un buen programador mira en ambos sentidos en una calle de sentido único".
Vector
@ Vector Sí, supongo que las futuras colecciones de Java se implementarán List(o Collection, o al menos Iterable). Ese es el objetivo de estas interfaces, y es una pena que las matrices de Java no las implementen, pero son una interfaz oficial para colecciones en Java, por lo que no es descabellado asumir que cualquier colección de Java las implementará, a menos que esa colección sea más antigua que List, y en ese caso, es muy fácil envolverlo con AbstractList .
Idan Arye
Estás diciendo que tu suposición está virtualmente garantizada, así que está bien, voy a eliminar el voto negativo (cuando se me permita) porque fuiste lo suficientemente decente para explicar, y no soy un tipo Java excepto por osmosis. Aún así, no apoyo esta idea de exponer la estructura de datos interna, independientemente de cómo se haga, y no ha respondido directamente a la pregunta del OP, que realmente se trata de la encapsulación. es decir, limitar el acceso a la estructura de datos interna.
Vector
1
@Vector Sí, los usuarios pueden emitir el Listen ArrayList, pero no es igual que la implementación puede ser protegido 100% - siempre se puede utilizar la reflexión para acceder a los campos privados. Hacerlo está mal visto, pero el lanzamiento también está mal visto (aunque no tanto). El objetivo de la encapsulación no es evitar el pirateo malintencionado, sino evitar que los usuarios dependan de los detalles de implementación que desee cambiar. El uso de la Listinterfaz hace exactamente eso: los usuarios de la clase pueden depender de la Listinterfaz en lugar de la ArrayListclase concreta que podría cambiar.
Idan Arye
siempre se puede usar la reflexión para acceder a los campos privados , si alguien quiere escribir código incorrecto y subvertir un diseño, puede hacerlo. más bien, es para evitar que los usuarios ... esa sea una razón para la encapsulación. Otra es garantizar la integridad y la coherencia del estado interno de su clase. El problema no es "piratería maliciosa", sino una organización deficiente que genera errores desagradables. "Ley del privilegio menos necesario": dé al consumidor de su clase solo lo que es obligatorio, no más. Si es obligatorio que haga pública una estructura de datos interna completa, tiene un problema de diseño.
Vector
-2

Esto es bastante común para ocultar su estructura de datos internos del mundo exterior. En algún momento es excesivo especialmente en DTO. Recomiendo esto para el modelo de dominio. Si es necesario exponer, devuelva la copia inmutable. Junto con esto, sugiero crear una interfaz que tenga estos métodos como obtener, establecer, eliminar, etc.

VGaur
fuente
1
esto no parece ofrecer nada sustancial en 3 respuestas anteriores
mosquito