¿Cuál sería la desventaja de definir una clase como una subclase de una lista de sí misma?

48

En un proyecto reciente mío, definí una clase con el siguiente encabezado:

public class Node extends ArrayList<Node> {
    ...
}

Sin embargo, después de discutir con mi profesor de CS, afirmó que la clase sería "horrible para la memoria" y "mala práctica". No he encontrado que el primero sea particularmente cierto y el segundo que sea subjetivo.

Mi razonamiento para este uso es que tuve una idea para un objeto que debía definirse como algo que podría tener una profundidad arbitraria, donde el comportamiento de una instancia podría definirse mediante una implementación personalizada o mediante el comportamiento de varios. como objetos que interactúan. Esto permitiría la abstracción de objetos cuya implementación física se compondría de muchos subcomponentes interactuando.

Por otro lado, veo cómo esto podría ser una mala práctica. La idea de definir algo como una lista de sí misma no es simple ni físicamente implementable.

¿Hay alguna razón válida por la que no debería usar esto en mi código, teniendo en cuenta mi uso para él?

¹ Si necesito explicarlo más, me complacería; Solo intento mantener esta pregunta concisa.

    
pregunta Addison Crump 02.11.2016 - 21:27

6 respuestas

108

Francamente, no veo la necesidad de herencia aquí. No tiene sentido; Node es un ArrayList de Node ?

Si esto es solo una estructura de datos recursiva, simplemente escribirías algo como:

public class Node {
    public String item;
    public List<Node> children;
}

Qué hace tiene sentido; nodo tiene una lista de nodos secundarios o descendientes.

    
respondido por el Robert Harvey 02.11.2016 - 21:39
57

El argumento "horrible para la memoria" es totalmente erróneo, pero es una "mala práctica" objetivamente . Cuando hereda de una clase, no solo hereda los campos y métodos que le interesan. En cambio, hereda todo . Cada método que declara, incluso si no es útil para usted. Y lo más importante, también hereda todos los contratos y garantías que proporciona la clase.

El acrónimo SOLID proporciona algunas heurísticas para un buen diseño orientado a objetos. Aquí, el I principio de segregación de interfaz (ISP) y el L iskov Sustitución de precios (LSP) tienen algo para decir.

El ISP nos dice que mantengamos nuestras interfaces lo más pequeñas posible. Pero al heredar de ArrayList , obtienes muchos, muchos métodos. ¿Es significativo para get() , remove() , set() (reemplazar) o add() (insertar) un nodo secundario en un índice particular? ¿Es sensible a ensureCapacity() de la lista subyacente? ¿Qué significa sort() a Node? ¿Se supone que los usuarios de su clase realmente obtienen un subList() ? Como no puede ocultar los métodos que no desea, la única solución es tener la ArrayList como variable miembro y reenviar todos los métodos que realmente desee:

private final ArrayList<Node> children = new ArrayList();
public void add(Node child) { children.add(child); }
public Iterator<Node> iterator() { return children.iterator(); }

Si realmente desea todos los métodos que ve en la documentación, podemos pasar al LSP. El LSP nos dice que debemos poder usar la subclase donde se espera que la clase primaria. Si una función toma un parámetro ArrayList y pasamos nuestro Node en su lugar, se supone que nada debe cambiar.

La compatibilidad de las subclases comienza con cosas simples como las firmas de tipo. Cuando reemplaza un método, no puede hacer que los tipos de parámetros sean más estrictos, ya que eso podría excluir los usos que eran legales con la clase principal. Pero eso es algo que el compilador nos comprueba en Java.

Pero el LSP es mucho más profundo: tenemos que mantener la compatibilidad con todo lo que promete la documentación de todas las clases e interfaces principales. En su respuesta , Lynn encontró un caso en el que la interfaz List (que usted heredó a través de ArrayList ) garantiza cómo deben funcionar los métodos equals() y hashCode() . Para hashCode() incluso se le da un algoritmo particular que debe implementarse exactamente. Supongamos que ha escrito este Node :

public class Node extends ArrayList<Node> {
  public final int value;

  public Node(int value, Node... children) {
    this.value = Value;
    for (Node child : children)
      add(child);
  }

  ...

}

Esto requiere que el value no pueda contribuir al hashCode() y no pueda influir en el equals() . La interfaz List , que prometes honrar heredando de ella, requiere que new Node(0).equals(new Node(123)) sea verdadera.

Debido a que la herencia de las clases hace que sea demasiado fácil romper la promesa que hizo una clase padre y porque generalmente expone más métodos de los que pretendía, generalmente se sugiere que prefiera la composición sobre la herencia . Si debe heredar algo, se sugiere que solo herede las interfaces. Si desea reutilizar el comportamiento de una clase en particular, puede mantenerlo como un objeto separado en una variable de instancia, de esa manera todas sus promesas y requisitos no le serán forzados.

A veces, nuestro lenguaje natural sugiere una relación de herencia: un automóvil es un vehículo. Una motocicleta es un vehículo. ¿Debo definir las clases Car y Motorcycle que heredan de una clase Vehicle ? El diseño orientado a objetos no se trata de reflejar el mundo real exactamente en nuestro código. No podemos codificar fácilmente las ricas taxonomías del mundo real en nuestro código fuente.

Uno de estos ejemplos es el problema de modelado empleado-jefe. Tenemos varios Person s, cada uno con un nombre y una dirección. Un Employee es un Person y tiene un Boss . Un Boss también es un Person . Entonces, ¿debo crear una clase Person que sea heredada por Boss y Employee ? Ahora tengo un problema: el jefe también es un empleado y tiene otro superior. Así que parece que Boss debería extender Employee . Pero el CompanyOwner es un Boss pero ¿no es un Employee ? Cualquier tipo de gráfico de herencia de alguna manera se descompondrá aquí.

OOP no se trata de jerarquías, herencia y reutilización de clases existentes, se trata de comportamiento de generalización . OOP se trata de "Tengo un montón de objetos y quiero que hagan algo en particular, y no me importa cómo". Para eso están las interfaces . Si implementas la interfaz Iterable para tu Node porque quieres que sea iterable, está perfectamente bien. Si implementa la interfaz Collection porque desea agregar / eliminar nodos secundarios, etc., está bien. Pero heredar de otra clase porque le da todo lo que no es, o al menos no, a menos que lo haya pensado cuidadosamente como se describe anteriormente.

    
respondido por el amon 03.11.2016 - 15:51
17

La extensión de un contenedor por sí misma generalmente se acepta como una mala práctica. Realmente hay muy pocas razones para extender un contenedor en lugar de tener solo uno. Extender un contenedor de ti mismo lo hace muy extraño.

    
respondido por el DeadMG 02.11.2016 - 21:43
14

Además de lo que se ha dicho, existe una razón un tanto específica de Java para evitar este tipo de estructuras.

El contrato del método equals para listas requiere que una lista se considere igual a otro objeto

  

si y solo si el objeto especificado también es una lista, ambas listas tienen el mismo tamaño, y todos los pares de elementos correspondientes en las dos listas son iguales .

Fuente: enlace

Específicamente, esto significa que tener una clase diseñada como una lista de sí misma puede hacer que las comparaciones de igualdad sean caras (y los cálculos de hash también si las listas son mutables), y si la clase tiene algunos campos de instancia, bueno, estos deben se ignorará en la comparación de igualdad.

    
respondido por el Lynn 03.11.2016 - 12:26
3

Con respecto a la memoria:

Yo diría que esto es una cuestión de perfeccionismo. El constructor predeterminado de ArrayList se ve así:

public ArrayList(int initialCapacity) {
     super();

     if (initialCapacity < 0)
         throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity);

     this.elementData = new Object[initialCapacity];
 }

public ArrayList() {
     this(10);
}

Fuente . Este constructor también se utiliza en Oracle-JDK.

Ahora considere la posibilidad de construir una Lista de enlaces individuales con su código. Acaba de inflar con éxito el consumo de memoria con el factor 10x (para ser precisos, incluso un poco más alto). Para los árboles, esto también puede empeorar fácilmente sin ningún requisito especial en la estructura del árbol. Usar un LinkedList u otra clase como alternativa debería resolver este problema.

Para ser honesto, en la mayoría de los casos esto no es más que un mero perfeccionismo, incluso si ignoramos la cantidad de memoria disponible. Un LinkedList ralentizaría un poco el código como alternativa, por lo que es una compensación entre el rendimiento y el consumo de memoria de cualquier manera. Aún así, mi opinión personal sobre esto sería no desperdiciar tanta memoria de manera que se pueda sortear tan fácilmente como esta.

EDIT: aclaración sobre los comentarios (@amon para ser precisos). Esta sección de la respuesta no trata no el problema de la herencia. La comparación del uso de la memoria se realiza contra una lista de enlaces individuales y con el mejor uso de la memoria (en implementaciones reales, el factor puede cambiar un poco, pero aún es lo suficientemente grande como para sumar un poco de memoria desperdiciada).

Con respecto a la "mala práctica":

Definitivamente. Esta no es la forma estándar de implementar una gráfica por una simple razón: un Nodo Gráfico tiene nodos secundarios, no es como lista de nodos secundarios. Expresar con precisión lo que quiere decir en el código es una habilidad importante. Ya sea en nombres de variables o expresando estructuras como esta. Punto siguiente: mantener las interfaces mínimas: ha puesto todos los métodos de ArrayList disponibles para el usuario de la clase a través de la herencia. No hay forma de alterar esto sin romper toda la estructura del código. En su lugar, almacene List como variable interna y haga que los métodos requeridos estén disponibles a través de un método de adaptador. De esta manera, puede agregar y eliminar fácilmente la funcionalidad de la clase sin desordenar todo.

    
respondido por el Paul 04.11.2016 - 00:00
-2

Esto parece parecerse a la semántica del patrón compuesto . Se utiliza para modelar estructuras de datos recursivas.

    
respondido por el oopexpert 02.11.2016 - 22:07

Lea otras preguntas en las etiquetas