design - sintaxis - ¿Cómo se diseña una clase para la herencia?
sintaxis para declarar una clase en c# (5)
A veces, al crear una clase base, no estoy seguro de si debo exponer a ciertos miembros a subclases, por ejemplo, los objetos que uso para la sincronización. Por lo general, termino haciéndolos todos privados y los cambio a protegidos cuando surge la necesidad.
He oído decir que es "difícil" "diseñar para la herencia", pero nunca he encontrado que sea el caso. ¿Alguien (y cualquiera, me refiero a Jon Skeet) puede explicar por qué esto es supuestamente difícil, cuáles son las trampas / obstáculos / problemas, y por qué los simples programadores mortales no deberían intentarlo y simplemente sellar sus clases para proteger a los inocentes?
ok, yo bromeo sobre esto último, pero tengo curiosidad por ver si alguien (incluido Jon) realmente tiene dificultades para "diseñar para la herencia". Realmente nunca lo he considerado un problema, pero tal vez estoy pasando por alto algo que doy por hecho ... ¡o arruinar algo sin darme cuenta!
EDIT: gracias por todas las excelentes respuestas hasta ahora. Creo que el consenso es que para las clases de aplicaciones típicas (subclases de WinForm, clases de utilidad única, etc.) no es necesario considerar la reutilización de ningún tipo, ni mucho menos la reutilización por herencia, mientras que para las clases de biblioteca es fundamental considerar la reutilización a través de la herencia en el diseño.
Realmente no pienso en, digamos, una clase de WinForm para implementar un diálogo de GUI, como una clase que alguien podría reutilizar, en cierto modo lo considero un objeto único. Pero técnicamente es una clase y alguien podría heredar de ella, pero no es muy probable.
Mucho del desarrollo a gran escala que he hecho han sido las bibliotecas de clases para bibliotecas base y frameworks, por lo que el diseño para la reutilización por herencia fue fundamental; nunca lo consideré "difícil", simplemente lo era . ;-)
Pero tampoco lo consideré en contraste con las clases "únicas" para tareas de aplicación comunes como WinForms et al.
Más consejos y trampas de diseño para la herencia son bienvenidos, por supuesto; Trataré de arrojar algo también.
Creo que el punto principal está en el penúltimo párrafo de la respuesta de Jon: la mayoría de la gente piensa en "diseñar para la herencia" en términos de diseño de aplicaciones, es decir, solo quieren que funcione, y si no lo hace, puede ser arreglado
Pero es una bestia completamente diferente diseñar una clase heredable como una API para que otras personas trabajen con ella, porque entonces los campos protegidos y una gran cantidad de detalles de implementación se convierten implícitamente en parte del contrato de la API que las personas que usan la API deben entender, y que no se puede cambiar sin romper el código usando la API. Si comete un error en el diseño o la implementación, es probable que no pueda solucionarlo sin romper el código que depende de él.
Otro problema posible: cuando llama al método "virtual" desde el constructor en la clase base, y la subclase anula este método, la subclase puede usar datos unificados.
El código es mejor:
class Base {
Base() {
method();
}
void method() {
// subclass may override this method
}
}
class Sub extends Base {
private Data data;
Sub(Data value) {
super();
this.data = value;
}
@Override
void method() {
// prints null (!), when called from Base''s constructor
System.out.println(this.data);
}
}
Esto se debe a que el constructor de la clase base siempre debe terminar antes del constructor de la subclase.
Resumen: no llame a métodos invalidables desde el constructor
No creo que haya diseñado una clase para la herencia. Escribo el menor código posible, y cuando ya es hora de copiar y pegar para crear otra clase, pongo ese método en una súper clase (donde tiene más sentido que la composición). Así que dejo que el principio de No repetirme (DRY) dicte mi diseño de clase.
En cuanto a los consumidores, depende de ellos actuar con cordura. Intento no dictar cómo alguien puede usar mis clases, aunque trato de documentar cómo pretendía que fueran utilizadas.
En lugar de repetirlo demasiado, simplemente responderé que deberías echar un vistazo a los problemas que tuve cuando subclasificamos java.util.Properties . Tiendo a evitar muchos de los problemas de diseño de herencia al hacerlo tan raramente como sea posible. Aquí hay algunas ideas de los problemas:
Es un dolor (o potencialmente imposible) implementar la igualdad de manera adecuada, a menos que limites a "ambos objetos deben tener exactamente el mismo tipo". La dificultad viene con el requisito simétrico de que
a.Equals(b)
implicab.Equals(a)
. Sia
yb
son "un cuadrado de 10x10" y "un cuadrado rojo de 10x10" respectivamente, entoncesa
podría pensar queb
es igual a eso, y eso puede ser todo lo que realmente quiere probar, en muchos casos. Perob
sabe que tiene un color ya
no ...Cada vez que llamas a un método virtual en tu implementación, debes documentar esa relación y nunca, nunca, cambiarla. La persona que deriva de la clase también necesita leer esa documentación; de lo contrario, podría implementar la llamada al revés, lo que provocaría un desbordamiento de la pila de X que llama a Y que llama a X que llama a Y. Este es mi principal problema: en muchos En los casos, debe documentar su implementación, lo que conduce a una falta de flexibilidad. Se mitiga significativamente si nunca llama a un método virtual desde otro, pero aún tiene que documentar cualquier otra llamada a métodos virtuales, incluso de los no virtuales, y nunca cambie la implementación en ese sentido.
La seguridad del subproceso es difícil de lograr incluso sin que algún código desconocido sea parte de su clase de tiempo de ejecución. No solo necesita documentar el modelo de subprocesamiento de su clase, sino que también puede tener que exponer bloqueos (etc) a las clases derivadas para que puedan protegerse contra subprocesos de la misma manera.
Considere qué tipo de especialización es válida mientras se mantiene dentro del contrato de la clase base original. ¿De qué maneras se pueden anular los métodos de modo que una persona que llama no deba saber acerca de la especialización, solo que funciona? java.util.Properties es un gran ejemplo de mala especialización aquí: las personas que llaman no pueden simplemente tratarlo como una Hashtable normal, ya que solo debería tener cadenas para las claves y los valores.
Si un tipo está destinado a ser inmutable, no debería permitir la herencia, porque una subclase puede ser fácilmente mutable. Las rarezas podrían abundan.
¿Deberías implementar algún tipo de habilidad de clonación? Si no lo hace, puede hacer que sea más difícil para las subclases clonar correctamente. A veces, un clon de miembro es lo suficientemente bueno, pero otras veces puede no tener sentido.
Si no proporciona ningún miembro virtual, puede ser razonablemente seguro, pero en ese momento las subclases brindan funcionalidad adicional y diferente en lugar de especializar la funcionalidad existente. Eso no es necesariamente algo malo, pero no se siente como el propósito original de la herencia.
Muchos de estos son un problema mucho menor para los creadores de aplicaciones que los diseñadores de bibliotecas de clase: si sabes que tú y tus colegas van a ser los únicos que se derivarán de tu tipo, entonces puedes salirte con la tuya mucho menos. diseño frontal: puede corregir las subclases si lo necesita en una fecha posterior.
Por cierto, estos son puntos improvisados. Puedo pensar en más si lo pienso por suficiente tiempo. Josh Bloch lo dice todo muy bien en Effective Java 2, por cierto.