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.