Tengo una clase base, Base
. Tiene dos subclases Sub1
y Sub2
. Cada subclase tiene algunos métodos adicionales. Por ejemplo, Sub1
tiene Sandwich makeASandwich(Ingredients... ingredients)
y Sub2
tiene boolean contactAliens(Frequency onFrequency)
.
Dado que estos métodos toman parámetros diferentes y hacen cosas completamente diferentes, son completamente incompatibles, y no puedo usar el polimorfismo para resolver este problema.
Base
proporciona la mayor parte de la funcionalidad, y tengo una gran colección de Base
objetos. Sin embargo, todos los Base
objetos son a Sub1
o a Sub2
, y a veces necesito saber cuáles son.
Parece una mala idea hacer lo siguiente:
for (Base base : bases) {
if (base instanceof Sub1) {
((Sub1) base).makeASandwich(getRandomIngredients());
// ... etc.
} else { // must be Sub2
((Sub2) base).contactAliens(getFrequency());
// ... etc.
}
}
Así que se me ocurrió una estrategia para evitar esto sin lanzar. Base
ahora tiene estos métodos:
boolean isSub1();
Sub1 asSub1();
Sub2 asSub2();
Y, por supuesto, Sub1
implementa estos métodos como
boolean isSub1() { return true; }
Sub1 asSub1(); { return this; }
Sub2 asSub2(); { throw new IllegalStateException(); }
Y los Sub2
implementa de la manera opuesta.
Lamentablemente, ahora Sub1
y Sub2
tienen estos métodos en su propia API. Entonces puedo hacer esto, por ejemplo, en Sub1
.
/** no need to use this if object is known to be Sub1 */
@Deprecated
boolean isSub1() { return true; }
/** no need to use this if object is known to be Sub1 */
@Deprecated
Sub1 asSub1(); { return this; }
/** no need to use this if object is known to be Sub1 */
@Deprecated
Sub2 asSub2(); { throw new IllegalStateException(); }
De esta manera, si se sabe que el objeto es solo a Base
, estos métodos no están en desuso y se pueden usar para "lanzarse" a un tipo diferente para que yo pueda invocar los métodos de la subclase. Esto me parece elegante de alguna manera, pero por otro lado, estoy abusando de las anotaciones obsoletas como una forma de "eliminar" métodos de una clase.
Dado que una Sub1
instancia realmente es una Base, tiene sentido usar la herencia en lugar de la encapsulación. ¿Lo que estoy haciendo bien? ¿Hay una mejor manera de resolver este problema?
fuente
instanceof
, de una manera que requiere mucho tipeo, es propenso a errores y dificulta agregar más subclases.Sub1
ySub2
no se pueden usar indistintamente, ¿por qué los trata como tal? ¿Por qué no realizar un seguimiento de sus 'fabricantes de sándwiches' y 'contactores alienígenas' por separado?Respuestas:
No siempre tiene sentido agregar funciones a la clase base, como se sugiere en algunas de las otras respuestas. Agregar demasiadas funciones de casos especiales puede dar como resultado la unión de componentes que de otro modo no estarían relacionados.
Por ejemplo, podría tener una
Animal
clase, conCat
yDog
componentes. Si quiero ser capaz de imprimir, o los mostrará en la interfaz gráfica de usuario que puede ser excesiva para que añadarenderToGUI(...)
ysendToPrinter(...)
de la clase base.El enfoque que está utilizando, utilizando verificaciones de tipo y modelos, es frágil, pero al menos mantiene las preocupaciones separadas.
Sin embargo, si te encuentras haciendo este tipo de verificaciones / lanzamientos con frecuencia, entonces una opción es implementar el patrón de visitante / doble despacho para ello. Se parece a esto:
Ahora tu código se convierte
El principal beneficio es que si agrega una subclase, obtendrá errores de compilación en lugar de casos que faltan silenciosamente. La nueva clase de visitante también se convierte en un buen objetivo para incorporar funciones. Por ejemplo, podría tener sentido para moverse
getRandomIngredients()
enActOnBase
.También puede extraer la lógica de bucle: por ejemplo, el fragmento anterior podría convertirse
Un poco más de masajes y el uso de lambdas y transmisión de Java 8 le permitirán llegar a
Qué IMO tiene el aspecto más prolijo y conciso posible.
Aquí hay un ejemplo más completo de Java 8:
fuente
Sub3
. ¿No tiene el visitante exactamente el mismo problema queinstanceof
? Ahora debe agregaronSub3
al visitante y se rompe si lo olvida.onSub3
a la interfaz de visitante, obtiene un error de compilación en cada ubicación donde crea un nuevo visitante que aún no se ha actualizado. Por el contrario, la activación del tipo generará, en el mejor de los casos, un error de tiempo de ejecución, que puede ser más difícil de activar.Desde mi perspectiva: tu diseño está equivocado .
Traducido al lenguaje natural, estás diciendo lo siguiente:
Dado que tenemos
animals
, haycats
yfish
.animals
tener propiedades, que son comunes acats
yfish
. Pero eso no es suficiente: hay algunas propiedades que hacen diferentescat
a partirfish
, por lo tanto, que necesita subclase.Ahora tiene el problema, que olvidó modelar el movimiento . Bueno. Eso es relativamente fácil:
Pero ese es un diseño incorrecto. La forma correcta sería:
Donde
move
se compartiría el comportamiento implementado de manera diferente por cada animal.Esto significa: su diseño está roto.
Mi recomendación: refactor
Base
,Sub1
ySub2
.fuente
.move()
o.attack()
y debidamente abstractathat
parte - en lugar de tener.swim()
,.fly()
,.slide()
,.hop()
,.jump()
,.squirm()
,.phone_home()
, etc. ¿Cómo se refactoriza correctamente? Desconocido sin mejores ejemplos ... pero en general sigue siendo la respuesta correcta, a menos que el código de ejemplo y más detalles sugieran lo contrario.move
vsfly
/run
/ etc, eso se puede explicar en una oración como: Debería decirle al objeto qué hacer, no cómo hacerlo. En términos de accesores, como usted menciona es más como el caso real en un comentario aquí, debe hacer preguntas al objeto, no inspeccionar su estado.Es un poco difícil imaginar una circunstancia en la que tienes un grupo de cosas y quieres que hagan un sándwich o contacten a extraterrestres. En la mayoría de los casos en los que encuentre este tipo de conversión, operará con un tipo; por ejemplo, en clang, filtrará un conjunto de nodos para las declaraciones donde getAsFunction devuelve no nulo, en lugar de hacer algo diferente para cada nodo en la lista.
Puede ser que necesite realizar una secuencia de acción y en realidad no es relevante que los objetos que realizan la acción estén relacionados.
Entonces, en lugar de una lista de
Base
, trabaje en la lista de accionesdónde
Puede, si corresponde, hacer que Base implemente un método para devolver la acción, o cualquier otro medio que necesite para seleccionar la acción de las instancias en su lista base (ya que dice que no puede usar el polimorfismo, entonces, presumiblemente, la acción a tomar no es una función de la clase, sino alguna otra propiedad de las bases; de lo contrario, solo le daría a Base el método act (Context)
fuente
Context
objeto solo empeora las cosas. También podría estar pasandoObject
s a los métodos, lanzándolos y esperando que sean del tipo correcto.Context
no es necesario que sea una nueva clase, o incluso una interfaz. Por lo general, el contexto es solo la persona que llama, por lo que las llamadas son en formaaction.act(this)
. SigetFrequency()
ygetRandomIngredients()
son métodos estáticos, es posible que ni siquiera necesite un contexto. Así es como implementaría, digamos, una cola de trabajo, a la que se parece mucho su problema.¿Qué tal si sus subclases implementan una o más interfaces que definen lo que pueden hacer? Algo como esto:
Entonces tus clases se verán así:
Y su bucle for se convertirá en:
Dos ventajas principales de este enfoque:
PD: Perdón por los nombres de las interfaces, no se me ocurrió nada más genial en ese momento en particular: D.
fuente
El enfoque puede ser una buena en los casos en que casi cualquier tipo dentro de una familia , ya sea directamente utilizable como una implementación de alguna de las interfaces que cumple algún criterio o se puede utilizar para crear una implementación de la interfaz. Los tipos de colección integrados en mi humilde opinión se habrían beneficiado de este patrón, pero como no lo hacen, por ejemplo, inventaré una interfaz de colección
BunchOfThings<T>
.Algunas implementaciones de
BunchOfThings
son mutables; algunos no lo son. En muchos casos, un objeto que Fred podría querer contener algo que pueda usar comoBunchOfThings
y sepa que nada más que Fred podrá modificarlo. Este requisito puede cumplirse de dos maneras:Fred sabe que tiene las únicas referencias a eso
BunchOfThings
, y no existe ninguna referencia externa a eso enBunchOfThings
sus partes internas en ningún lugar del universo. Si nadie más tiene una referencia aBunchOfThings
sus componentes internos, nadie más podrá modificarlo, por lo que se cumplirá la restricción.Ni el
BunchOfThings
, ni ninguno de sus componentes internos a los que existen referencias externas, pueden modificarse por cualquier medio. Si absolutamente nadie puede modificar aBunchOfThings
, entonces la restricción será satisfecha.Una forma de satisfacer la restricción sería copiar incondicionalmente cualquier objeto recibido (procesando recursivamente cualquier componente anidado). Otra sería probar si un objeto recibido promete inmutabilidad y, si no, hacer una copia del mismo, y hacer lo mismo con cualquier componente anidado. Una alternativa, que es más limpia que la segunda y más rápida que la primera, es ofrecer un
AsImmutable
método que le pida a un objeto que haga una copia inmutable de sí mismo (utilizandoAsImmutable
cualquier componente anidado que lo admita).También se pueden proporcionar métodos relacionados
asDetached
(para usar cuando el código recibe un objeto y no sabe si querrá mutarlo, en cuyo caso un objeto mutable debe reemplazarse por un nuevo objeto mutable, pero un objeto inmutable puede mantenerse como está),asMutable
(en los casos en que un objeto sabe que contendrá un objeto devuelto anteriormenteasDetached
, es decir, una referencia no compartida a un objeto mutable o una referencia que se puede compartir a un objeto mutable), yasNewMutable
(en los casos en que el código recibe un exterior referencia y sabe que va a querer mutar una copia de los datos allí; si los datos entrantes son mutables, no hay razón para comenzar haciendo una copia inmutable que se usará inmediatamente para crear una copia mutable y luego se abandonará).Tenga en cuenta que, si bien los
asXX
métodos pueden devolver tipos ligeramente diferentes, su función real es garantizar que los objetos devueltos satisfagan las necesidades del programa.fuente
Ignorando la cuestión de si tiene un buen diseño o no, y suponiendo que sea bueno o al menos aceptable, me gustaría considerar las capacidades de las subclases, no el tipo.
Por lo tanto, ya sea:
Transfiera algún conocimiento de la existencia de sándwiches y extraterrestres a la clase base, aunque sepa que algunas instancias de la clase base no pueden hacerlo. Impleméntelo en la clase base para generar excepciones y cambie el código a:
Luego, una o ambas subclases anulan
canMakeASandwich()
, y solo una implementa cada una demakeASandwich()
ycontactAliens()
.Use interfaces, no subclases concretas, para detectar qué capacidades tiene un tipo. Deje la clase base sola y cambie el código a:
o posiblemente (y siéntase libre de ignorar esta opción si no se adapta a su estilo, o para cualquier estilo Java que considere razonable):
Personalmente, generalmente no me gusta el último estilo de capturar una excepción medio esperada, debido al riesgo de detectar inapropiadamente una
ClassCastException
salida degetRandomIngredients
omakeASandwich
, pero YMMV.fuente
<
es evitar la capturaArrayIndexOutOfBoundsException
, y algunas personas deciden hacer eso también. Sin tener en cuenta el gusto ;-) Básicamente mi "problema" aquí es que trabajo en Python, donde el estilo preferido generalmente es que atrapar cualquier excepción es preferible a probar por adelantado si ocurrirá la excepción, no importa cuán simple sea la prueba avanzada. . Tal vez no valga la pena mencionar la posibilidad en Java.Aquí tenemos un caso interesante de una clase base que se reduce a sus propias clases derivadas. Sabemos muy bien que esto normalmente es malo, pero si queremos decir que encontramos una buena razón, veamos y veamos cuáles pueden ser las limitaciones:
Si 4 entonces tenemos: 5. La política de la clase derivada siempre está bajo el mismo control político que la política de la clase base.
2 y 5 implican directamente que podemos enumerar todas las clases derivadas, lo que significa que no se supone que haya ninguna clase derivada externa.
Pero aquí está la cosa. Si son todos suyos, puede reemplazar el if con una abstracción que es una llamada a un método virtual (incluso si es una tontería) y deshacerse del if y self-cast. Por lo tanto, no lo hagas. Un mejor diseño está disponible.
fuente
Coloque un método abstracto en la clase base que haga una cosa en Sub1 y la otra en Sub2, y llame a ese método abstracto en sus bucles mixtos.
Tenga en cuenta que esto puede requerir otros cambios dependiendo de cómo se definan getRandomIngredients y getFrequency. Pero, sinceramente, dentro de la clase que contacta a extraterrestres es probablemente un mejor lugar para definir getFrequency de todos modos.
Por cierto, sus métodos asSub1 y asSub2 son definitivamente una mala práctica. Si vas a hacer esto, hazlo lanzando. No hay nada que estos métodos te den que el casting no tenga.
fuente
Puede que haya empujado tanto
makeASandwich
ycontactAliens
en la clase base y luego ponerlas en práctica con implementaciones ficticias. Dado que los tipos / argumentos de retorno no pueden resolverse en una clase base común.Hay desventajas obvias para este tipo de cosas, como lo que eso significa para el contrato del método y lo que puede y no puede inferir sobre el contexto del resultado. No se puede pensar, ya que no pude hacer un sándwich, los ingredientes se agotaron en el intento, etc.
fuente
Lo que estás haciendo es perfectamente legítimo. No presten atención a los detractores que simplemente reiteran el dogma porque lo leen en algunos libros. Dogma no tiene lugar en la ingeniería.
He empleado el mismo mecanismo un par de veces, y puedo decir con confianza que el tiempo de ejecución de Java también podría haber hecho lo mismo en al menos un lugar en el que puedo pensar, mejorando así el rendimiento, la usabilidad y la legibilidad del código que lo usa
Tomemos, por ejemplo
java.lang.reflect.Member
, cuál es la base dejava.lang.reflect.Field
yjava.lang.reflect.Method
. (La jerarquía real es un poco más complicada que eso, pero eso es irrelevante). Los campos y los métodos son animales muy diferentes: uno tiene un valor que puede obtener o establecer, mientras que el otro no tiene tal cosa, pero puede invocarse con una serie de parámetros y puede devolver un valor. Por lo tanto, los campos y los métodos son miembros, pero las cosas que puede hacer con ellos son tan diferentes entre sí como hacer sándwiches frente a contactar con extraterrestres.Ahora, cuando escribimos código que usa la reflexión, a menudo tenemos un
Member
en nuestras manos, y sabemos que es un aMethod
o unField
(o, raramente, algo más) y, sin embargo, tenemos que hacer todo lo tediosoinstanceof
para descubrir con precisión qué es y luego tenemos que lanzarlo para obtener una referencia adecuada. (Y esto no solo es tedioso, sino que tampoco funciona muy bien). LaMethod
clase podría haber implementado muy fácilmente el patrón que está describiendo, facilitando así la vida de miles de programadores.Por supuesto, esta técnica solo es viable en jerarquías pequeñas y bien definidas de clases estrechamente acopladas de las que usted tiene (y siempre tendrá) control de nivel de origen: no desea hacer tal cosa si su jerarquía de clases es Es probable que sea extendido por personas que no están en libertad de refactorizar la clase base.
Así es como lo que he hecho difiere de lo que has hecho:
La clase base proporciona una implementación predeterminada para toda la
asDerivedClass()
familia de métodos, haciendo que cada uno de ellos regresenull
.Cada clase derivada solo anula uno de los
asDerivedClass()
métodos, regresando enthis
lugar denull
. No anula nada del resto, ni quiere saber nada sobre ellos. Por lo tanto, noIllegalStateException
se arrojan s.La clase base también proporciona
final
implementaciones para toda laisDerivedClass()
familia de métodos, codificados de la siguiente manera: Dereturn asDerivedClass() != null;
esta manera, se minimiza el número de métodos que deben ser anulados por las clases derivadas.No he estado usando
@Deprecated
este mecanismo porque no lo pensé. Ahora que me diste la idea, la pondré en práctica, ¡gracias!C # tiene un mecanismo relacionado incorporado mediante el uso de la
as
palabra clave. En C # puede decirDerivedClass derivedInstance = baseInstance as DerivedClass
y obtendrá una referencia aDerivedClass
si subaseInstance
pertenecía a esa clase onull
no. Esto (teóricamente) funciona mejor que lois
seguido por la conversión (is
es la palabra clave de C # mejor nombrada parainstanceof
), pero el mecanismo personalizado para el que hemos estado trabajando a mano funciona aún mejor: el par deinstanceof
operaciones de lanzamiento y lanzamiento de Java también ya que elas
operador de C # no funciona tan rápido como la llamada de método virtual único de nuestro enfoque personalizado.Por la presente, propongo que esta técnica debe declararse como un patrón y que se debe encontrar un buen nombre para ello.
Gee, gracias por los votos negativos!
Un resumen de la controversia, para salvarlo de la molestia de leer los comentarios:
La objeción de la gente parece ser que el diseño original era incorrecto, lo que significa que nunca debería haber clases muy diferentes que se derivan de una clase base común, o que incluso si lo hace, el código que utiliza dicha jerarquía nunca debería estar en la posición de tener una referencia base y la necesidad de descubrir la clase derivada. Por lo tanto, dicen, el mecanismo de autoelaboración propuesto por esta pregunta y por mi respuesta, que mejora el uso del diseño original, nunca debería haber sido necesario en primer lugar. (Realmente no dicen nada sobre el mecanismo de auto-fundición en sí, solo se quejan de la naturaleza de los diseños a los que se debe aplicar el mecanismo).
Sin embargo, en el ejemplo anterior que han ya demostrado que los creadores de la ejecución de Java, en efecto, elegir el diseño precisamente tal para el
java.lang.reflect.Member
,Field
,Method
jerarquía, y en los comentarios a continuación que también muestran que los creadores de la C # tiempo de ejecución llegado independientemente a una equivalente diseño para elSystem.Reflection.MemberInfo
,FieldInfo
,MethodInfo
jerarquía. Por lo tanto, estos son dos escenarios diferentes del mundo real que se encuentran justo debajo de la nariz de todos y que tienen soluciones demostrablemente viables utilizando precisamente tales diseños.A eso se reducen todos los siguientes comentarios. El mecanismo de auto-fundición apenas se menciona.
fuente
java.lang.reflection.Member
jerarquía. Los creadores del tiempo de ejecución de Java se metieron en este tipo de situación, ¿no supongo que dirían que se diseñaron en una caja, o los culpan por no seguir algún tipo de mejores prácticas? Entonces, lo que quiero decir aquí es que una vez que tienes este tipo de situación, este mecanismo es una buena manera de mejorar las cosas.Member
interfaz, pero solo sirve para verificar los calificadores de acceso. Por lo general, obtienes una lista de métodos o propiedades y la usas directamente (sin mezclarlos, por lo que noList<Member>
aparece), por lo que no necesitas lanzar. Tenga en cuenta queClass
no proporciona ungetMembers()
método. Por supuesto, un programador despistado podría crear elList<Member>
, pero eso sería hacer el menor sentido de que es unaList<Object>
y la adición de algunosInteger
oString
de ella.null
lugar de una excepción no lo hace mucho mejor. En igualdad de condiciones, el visitante está más seguro mientras hace el mismo trabajo.