Asumiendo un lenguaje con algún tipo de seguridad inherente (por ejemplo, no JavaScript):
Dado un método que acepta a SuperType
, sabemos que en la mayoría de los casos en los que podríamos sentir la tentación de realizar pruebas de tipo para elegir una acción:
public void DoSomethingTo(SuperType o) {
if (o isa SubTypeA) {
o.doSomethingA()
} else {
o.doSomethingB();
}
}
Por lo general, deberíamos, si no siempre, crear un método único y reemplazable en el SuperType
y hacer esto:
public void DoSomethingTo(SuperType o) {
o.doSomething();
}
... en el que cada subtipo tiene su propia doSomething()
implementación. El resto de nuestra aplicación puede ignorar adecuadamente si alguno de ellos SuperType
es realmente un SubTypeA
o un SubTypeB
.
Maravilloso.
Pero, todavía tenemos is a
operaciones similares en la mayoría, si no en todos, los lenguajes de tipo seguro. Y eso sugiere una posible necesidad de pruebas de tipo explícitas.
Por lo tanto, en qué situaciones, si las hay, deben nosotros o tienen que realizar pruebas de tipo explícito?
Perdona mi distracción o falta de creatividad. Sé que lo he hecho antes; pero, honestamente, hace tanto tiempo que no recuerdo si lo que hice fue bueno. Y en la memoria reciente, no creo que haya encontrado la necesidad de probar tipos fuera de mi JavaScript de vaquero.
fuente
Respuestas:
"Nunca" es la respuesta canónica a "¿cuándo está bien la prueba de tipo?" No hay forma de probar o refutar esto; Es parte de un sistema de creencias sobre lo que hace que el "buen diseño" o el "buen diseño orientado a objetos". También es hokum.
Sin duda, si tiene un conjunto integrado de clases y también más de una o dos funciones que necesitan ese tipo de prueba de tipo directo, probablemente lo esté HACIENDO INCORRECTAMENTE. Lo que realmente necesita es un método que se implemente de manera diferente
SuperType
y sus subtipos. Esto es parte integrante de la programación orientada a objetos, y existen toda la razón por las clases y la herencia.En este caso, la prueba de tipo explícitamente es incorrecta no porque la prueba de tipo sea intrínsecamente incorrecta, sino porque el lenguaje ya tiene una forma limpia, extensible e idiomática de lograr la discriminación de tipo, y usted no la usó. En cambio, volviste a un idioma primitivo, frágil, no extensible.
Solución: usa el idioma. Como sugirió, agregue un método a cada una de las clases, luego deje que la herencia estándar y los algoritmos de selección de métodos determinen qué caso se aplica. O si no puede cambiar los tipos base, subclase y agregue su método allí.
Esto en cuanto a la sabiduría convencional, y algunas respuestas. Algunos casos donde las pruebas de tipo explícito tienen sentido:
Es único. Si tuviera que hacer mucha discriminación de tipos, podría extender los tipos o la subclase. Pero tu no. Solo tiene uno o dos lugares donde necesita pruebas explícitas, por lo que no vale la pena regresar y trabajar a través de la jerarquía de clases para agregar las funciones como métodos. O no vale la pena el esfuerzo práctico para agregar el tipo de generalidad, pruebas, revisiones de diseño, documentación u otros atributos de las clases base para un uso tan simple y limitado. En ese caso, agregar una función que haga pruebas directas es racional.
No puedes ajustar las clases. Piensa en subclasificar, pero no puede. Muchas clases en Java, por ejemplo, están designadas
final
. Intenta incluirpublic class ExtendedSubTypeA extends SubTypeA {...}
ay el compilador le dice, en términos claros, que lo que está haciendo no es posible. Lo sentimos, gracia y sofisticación del modelo orientado a objetos. ¡Alguien decidió que no puedes extender sus tipos! Desafortunadamente, muchas de las bibliotecas estándar sonfinal
, y hacer clasesfinal
es una guía de diseño común. Una función de ejecución final es lo que queda disponible para usted.Por cierto, esto no se limita a los idiomas escritos estáticamente. El lenguaje dinámico Python tiene una serie de clases base que, bajo las cubiertas implementadas en C, no pueden modificarse realmente. Al igual que Java, eso incluye la mayoría de los tipos estándar.
Tu código es externo. Está desarrollando con clases y objetos que provienen de una variedad de servidores de bases de datos, motores de middleware y otras bases de código que no puede controlar o ajustar. Su código es solo un consumidor humilde de objetos generados en otros lugares. Incluso si pudiera subclasificar
SuperType
, no podrá obtener esas bibliotecas de las que depende para generar objetos en sus subclases. Te entregarán instancias de los tipos que conocen, no tus variantes. Este no es siempre el caso ... a veces están diseñados para la flexibilidad y crean instancias dinámicas de clases de las que les da de comer. O proporcionan un mecanismo para registrar las subclases que desea que construyan sus fábricas. Los analizadores XML parecen particularmente buenos para proporcionar dichos puntos de entrada; ver por ejemplo o lxml en Python . Pero la mayoría de las bases de código no proporcionan tales extensiones. Te devolverán las clases con las que fueron construidas y sobre las que sabes. Por lo general, no tendrá sentido representar sus resultados en sus resultados personalizados solo para que pueda usar un selector de tipo puramente orientado a objetos. Si va a hacer discriminación de tipo, tendrá que hacerlo de manera relativamente cruda. Su código de prueba de tipo parece bastante apropiado.Genéricos de la persona pobre / envío múltiple. Desea aceptar una variedad de tipos diferentes para su código, y siente que tener una variedad de métodos muy específicos de tipo no es elegante.
public void add(Object x)
Parece lógico, pero no una gran variedad deaddByte
,addShort
,addInt
,addLong
,addFloat
,addDouble
,addBoolean
,addChar
, yaddString
variantes (por nombrar algunos). Tener funciones o métodos que toman un súper tipo alto y luego determinan qué hacer tipo por tipo: no le van a ganar el Premio de Pureza en el Simposio de diseño anual de Booch-Liskov, sino que abandonan el La denominación húngara le dará una API más simple. En cierto sentido, tuis-a
ois-instance-of
las pruebas simulan el envío genérico o múltiple en un contexto de lenguaje que no lo admite de forma nativa.La compatibilidad con el lenguaje incorporado tanto para los genéricos como para la escritura de patos reduce la necesidad de la verificación de tipos al hacer que sea más probable "hacer algo elegante y apropiado". La selección de distribución / interfaz múltiple vista en lenguajes como Julia y Go reemplaza de manera similar las pruebas de tipo directo con mecanismos incorporados para la selección basada en tipo de "qué hacer". Pero no todos los idiomas son compatibles con estos. Java, por ejemplo, generalmente es de un solo envío, y sus expresiones idiomáticas no son muy fáciles de escribir.
Pero incluso con todas estas características de discriminación de tipo (herencia, genéricos, tipeo de pato y despacho múltiple), a veces es conveniente tener una rutina única y consolidada que hace que esté haciendo algo en función del tipo de objeto claro e inmediato En la metaprogramación, lo he encontrado esencialmente inevitable. Si recurrir a las consultas de tipo directo constituye "pragmatismo en acción" o "codificación sucia" dependerá de su filosofía de diseño y creencias.
fuente
(1.0).Equals(1.0f)
arrojando verdadero [el argumento promueve adouble
], pero(1.0f).Equals(1.0)
arrojando falso [el argumento promueve aobject
]; en Java,Math.round(123456789*1.0)
rinde 123456789, peroMath.round(123456789*1.0)
rinde 123456792 [el argumento promueve afloat
más que adouble
].Math.round
me parecen idénticas. ¿Cual es la diferencia?Math.round(123456789)
[indicativo de lo que podría suceder si alguien reescribeMath.round(thing.getPosition() * COUNTS_PER_MIL)
para devolver un valor de posición sin escala, sin darse cuenta de quegetPosition
devuelve unint
olong
.]La situación principal que siempre he necesitado fue al comparar dos objetos, como en un
equals(other)
método, que pueden requerir diferentes algoritmos dependiendo del tipo exacto deother
. Incluso entonces, es bastante raro.La otra situación que he tenido, una vez más, muy raramente, es después de la deserialización o el análisis, donde a veces lo necesitas para lanzar de forma segura a un tipo más específico.
Además, a veces solo necesitas un truco para evitar el código de terceros que no controlas. Es una de esas cosas que realmente no quieres usar de forma regular, pero te alegra que esté allí cuando realmente la necesites.
fuente
BaseClass base = deserialize(input)
, porque aún no conoces el tipo, entonces lo hacesif (base instanceof Derived) derived = (Derived)base
para almacenarlo como su tipo derivado exacto.El caso estándar (pero con suerte raro) se ve así: si en la siguiente situación
las funciones
DoSomethingA
oDoSomethingB
no pueden implementarse fácilmente como funciones miembro del árbol de herencia deSuperType
/SubTypeA
/SubTypeB
. Por ejemplo, siDoSomethingXXX
esa biblioteca significaría introducir una dependencia prohibida.Tenga en cuenta que a menudo hay situaciones en las que puede eludir este problema (por ejemplo, creando una envoltura o un adaptador para
SubTypeA
ySubTypeB
, o tratando de volver a implementarloDoSomething
completamente en términos de operaciones básicas deSuperType
), pero a veces estas soluciones no valen la pena ni la molestia. cosas más complicadas y menos extensibles que hacer la prueba de tipo explícito.Un ejemplo de mi trabajo de ayer: tuve una situación en la que iba a paralelizar el procesamiento de una lista de objetos (de tipo
SuperType
, con exactamente dos subtipos diferentes, donde es extremadamente improbable que haya más). La versión sin paralelo contenía dos bucles: un bucle para objetos del subtipo A, que llamaDoSomethingA
, y un segundo bucle para objetos del subtipo B, que llamaDoSomethingB
.Los métodos "DoSomethingA" y "DoSomethingB" son cálculos que requieren mucho tiempo, utilizando información de contexto que no está disponible en el alcance de los subtipos A y B. (por lo que no tiene sentido implementarlos como funciones miembro de los subtipos). Desde el punto de vista del nuevo "ciclo paralelo", facilita mucho las cosas al tratarlos de manera uniforme, por lo que implementé una función similar a la
DoSomethingTo
anterior. Sin embargo, al observar las implementaciones de "DoSomethingA" y "DoSomethingB", muestra que funcionan de manera muy diferente internamente. Por lo tanto, tratar de implementar un "DoSomething" genérico extendiéndoloSuperType
con muchos métodos abstractos realmente no funcionaría, o significaría diseñar demasiado las cosas por completo.fuente
SuperType
y es subclases?NSJSONSerialization
en Obj-C), pero no desea simplemente confiar en que la respuesta contiene el tipo que espera, por lo que antes de usarlo, verifíquelo (por ejemploif ([theResponse isKindOfClass:[NSArray class]])...
) .Como lo llama el tío Bob:
En uno de sus episodios de Clean Coder, dio un ejemplo de una llamada de función que se utiliza para devolver
Employee
s.Manager
es un sub-tipo deEmployee
. Supongamos que tenemos un servicio de aplicación que acepta unaManager
identificación y lo convoca a la oficina :) La funcióngetEmployeeById()
devuelve un supertipoEmployee
, pero quiero verificar si se devuelve un administrador en este caso de uso.Por ejemplo:
Aquí estoy verificando si el empleado devuelto por consulta es en realidad un gerente (es decir, espero que sea un gerente y, de lo contrario, falle rápidamente).
No es el mejor ejemplo, pero es tío Bob después de todo.
Actualizar
Actualicé el ejemplo tanto como puedo recordar de memoria.
fuente
Manager
la implementación desummon()
simplemente no arroja la excepción en este ejemplo?CEO
puede convocarManager
s.Employee
, solo debería importarle que obtenga algo que se comporte como unEmployee
. Si diferentes subclasesEmployee
tienen diferentes permisos, responsabilidades, etc., ¿qué hace que la prueba de tipo sea una mejor opción que un sistema de permisos real?Nunca.
is
cláusula, o (en algunos idiomas, o dependiendo del escenario) porque no puede extender el tipo sin modificar las partes internas de la función que realiza lais
verificación.is
cheques son una fuerte señal de que está violando el Principio de sustitución de Liskov . Cualquier cosa que funcioneSuperType
debe ser completamente ignorante de qué subtipos puede haber.Dicho todo esto, los
is
cheques pueden ser menos malos que otras alternativas. Poner toda la funcionalidad común en una clase base es difícil y a menudo conduce a problemas peores. Usar una sola clase que tenga una marca o enumeración para saber qué "tipo" es la instancia ... es peor que horrible, ya que ahora está extendiendo la elusión del sistema de tipos a todos los consumidores.En resumen, siempre debe considerar los controles de tipo como un fuerte olor a código. Pero como con todas las pautas, habrá ocasiones en las que se verá obligado a elegir entre qué violación de las pautas es la menos ofensiva.
fuente
instanceof
filtra detalles de implementación y rompe la abstracción.IEnumerable<T>
no promete que exista un "último" elemento. Si su método necesita dicho elemento, debería requerir un tipo que garantice la existencia de uno. Y los subtipos de ese tipo pueden proporcionar implementaciones eficientes del método "Último".Si tiene una base de código grande (más de 100K líneas de código) y está cerca del envío o está trabajando en una sucursal que luego tendrá que fusionarse, y por lo tanto, hay un gran costo / riesgo de cambiar mucho frente.
Luego, a veces tiene la opción de un gran refractor del sistema, o alguna "prueba de tipo" localizada simple. Esto crea una deuda técnica que debe pagarse lo antes posible, pero a menudo no lo es.
(Es imposible encontrar un ejemplo, ya que cualquier código que sea lo suficientemente pequeño como para ser utilizado como ejemplo, también es lo suficientemente pequeño como para que el mejor diseño sea claramente visible).
O, en otras palabras, cuando el objetivo es que le paguen su salario en lugar de obtener “votos positivos” por la limpieza de su diseño.
El otro caso común es el código de la interfaz de usuario, cuando, por ejemplo, muestra una interfaz de usuario diferente para algunos tipos de empleados, pero claramente no desea que los conceptos de la interfaz de usuario escapen a todas sus clases de "dominio".
Puede usar "pruebas de tipo" para decidir qué versión de la interfaz de usuario mostrar, o tener alguna tabla de búsqueda elegante que convierta de "clases de dominio" a "clases de interfaz de usuario". La tabla de búsqueda es solo una forma de ocultar la "prueba de tipo" en un solo lugar.
(El código de actualización de la base de datos puede tener los mismos problemas que el código de la interfaz de usuario, sin embargo, tiende a tener solo un conjunto de código de actualización de la base de datos, pero puede tener muchas pantallas diferentes que deben adaptarse al tipo de objeto que se muestra).
fuente
La implementación de LINQ utiliza muchas comprobaciones de tipos para posibles optimizaciones de rendimiento, y luego un respaldo para IEnumerable.
El ejemplo más obvio es probablemente el método ElementAt (pequeño extracto de la fuente .NET 4.5):
Pero hay muchos lugares en la clase Enumerable donde se usa un patrón similar.
Entonces, quizás optimizar el rendimiento para un subtipo de uso común es un uso válido. No estoy seguro de cómo podría haberse diseñado mejor.
fuente
IEnumerable<T>
incluido muchos métodos como esosList<T>
, junto con unaFeatures
propiedad que indica qué métodos pueden funcionar bien, lentamente o no funcionar, así como varias suposiciones que un consumidor puede hacer de manera segura sobre la colección (por ejemplo, ¿se garantiza que su tamaño y / o sus contenidos existentes nunca cambiarán [un tipo podría admitirAdd
mientras se garantiza que los contenidos existentes serían inmutables]).Hay un ejemplo que aparece a menudo en los desarrollos de juegos, específicamente en la detección de colisiones, que es difícil de tratar sin el uso de alguna forma de prueba de tipo.
Suponga que todos los objetos del juego derivan de una clase base común
GameObject
. Cada objeto tiene un cuerpo de forma colisión severaCollisionShape
que puede proporcionar una interfaz común (por decir posición de interrogación, orientación, etc) pero las formas de colisión reales todos serán subclases concretas, tales comoSphere
,Box
,ConvexHull
, etc. almacenar información específica a ese tipo de objeto geométrico (ver aquí para un ejemplo real)Ahora, para probar una colisión, necesito escribir una función para cada par de tipos de formas de colisión:
que contienen las matemáticas específicas necesarias para realizar una intersección de esos dos tipos geométricos.
En cada 'tic' de mi ciclo de juego, necesito verificar pares de objetos en busca de colisiones. Pero solo tengo acceso a
GameObject
sy sus correspondientesCollisionShape
s. Claramente, necesito saber tipos concretos para saber a qué función de detección de colisión llamar. Ni siquiera el envío doble (que lógicamente no es diferente de verificar el tipo de todos modos) puede ayudar aquí *.En la práctica, en esta situación, los motores de física que he visto (Bullet y Havok) se basan en pruebas de tipo de una forma u otra.
No digo que esta sea necesariamente una buena solución, es solo que puede ser la mejor de una pequeña cantidad de posibles soluciones a este problema.
* Técnicamente es posible usar el envío doble de una manera horrenda y complicada que requeriría N (N + 1) / 2 combinaciones (donde N es el número de tipos de formas que tiene) y solo ofuscaría lo que realmente está haciendo. está descubriendo simultáneamente los tipos de las dos formas, así que no considero que sea una solución realista.
fuente
A veces no desea agregar un método común a todas las clases porque realmente no es su responsabilidad realizar esa tarea específica.
Por ejemplo, desea dibujar algunas entidades pero no desea agregarles el código de dibujo directamente (lo cual tiene sentido). En idiomas que no admiten envíos múltiples, puede terminar con el siguiente código:
Esto se vuelve problemático cuando este código aparece en varios lugares y necesita modificarlo en todas partes al agregar un nuevo tipo de entidad. Si ese es el caso, entonces se puede evitar utilizando el patrón Visitante, pero a veces es mejor mantener las cosas simples y no diseñarlas en exceso. Esas son las situaciones en que las pruebas de tipo están bien.
fuente
El único momento que uso es en combinación con la reflexión. Pero incluso entonces, la verificación dinámica es mayoritaria, no codificada en una clase específica (o solo codificada en clases especiales como
String
oList
).Por verificación dinámica quiero decir:
y no codificado
fuente
Las pruebas de tipo y la conversión de tipos son dos conceptos muy relacionados. Tan estrechamente relacionado que me siento confiado al decir que nunca debe hacer una prueba de tipo a menos que su intención sea escribir el objeto en función del resultado.
Cuando piensas en el diseño ideal orientado a objetos, las pruebas de tipo (y la conversión) nunca deberían ocurrir. Pero es de esperar que ya haya descubierto que la programación orientada a objetos no es ideal. A veces, especialmente con el código de nivel inferior, el código no puede mantenerse fiel al ideal. Este es el caso de ArrayLists en Java; Como no saben en tiempo de ejecución qué clase se está almacenando en la matriz, crean
Object[]
matrices y las convierten estáticamente en el tipo correcto.Se ha señalado que una necesidad común de pruebas de tipo (y conversión de tipos) proviene del
Equals
método, que en la mayoría de los idiomas se supone que es claroObject
. La implementación debe tener algunas comprobaciones detalladas para hacer si los dos objetos son del mismo tipo, lo que requiere poder probar de qué tipo son.La prueba de tipo también aparece frecuentemente en la reflexión. A menudo tendrá métodos que devuelven
Object[]
u otra matriz genérica, y desea extraer todos losFoo
objetos por cualquier motivo. Este es un uso perfectamente legítimo de pruebas de tipo y casting.En general, las pruebas de tipo son malas cuando acoplan innecesariamente su código con la forma en que se escribió una implementación específica. Esto puede llevar fácilmente a la necesidad de una prueba específica para cada tipo o combinación de tipos, como si desea encontrar la intersección de líneas, rectángulos y círculos, y la función de intersección tiene un algoritmo diferente para cada combinación. Su objetivo es poner todos los detalles específicos de un tipo de objeto en el mismo lugar que ese objeto, porque eso facilitará el mantenimiento y la extensión de su código.
fuente
ArrayLists
no conoce la clase que se almacena en tiempo de ejecución porque Java no tenía genéricos y, cuando finalmente se introdujeron, Oracle optó por la compatibilidad con versiones anteriores con código sin genéricos.equals
tiene el mismo problema, y de todos modos es una decisión de diseño cuestionable; las comparaciones de igualdad no tienen sentido para cada tipo.String x = (String) myListOfStrings.get(0)
Object
hasta que se accedió de todos modos; los genéricos en Java solo proporcionan conversión implícita asegurada por las reglas del compilador.Es aceptable en un caso en el que tiene que tomar una decisión que involucra dos tipos y esta decisión se encapsula en un objeto fuera de esa jerarquía de tipos. Por ejemplo, supongamos que está programando qué objeto se procesa a continuación en una lista de objetos en espera de procesamiento:
Ahora digamos que nuestra lógica de negocios es literalmente "todos los automóviles tienen prioridad sobre los barcos y camiones". Agregar una
Priority
propiedad a la clase no le permite expresar esta lógica comercial de manera limpia porque terminará con esto:El problema es que ahora, para comprender el orden de prioridad, debe observar todas las subclases o, en otras palabras, ha agregado el acoplamiento a las subclases.
Por supuesto, debe convertir las prioridades en constantes y colocarlas en una clase por sí mismas, lo que ayuda a mantener la programación de la lógica empresarial:
Sin embargo, en realidad, el algoritmo de programación es algo que podría cambiar en el futuro y que eventualmente podría depender de algo más que el tipo. Por ejemplo, podría decir que "los camiones de más de 5000 kg tienen prioridad especial sobre todos los demás vehículos". Es por eso que el algoritmo de programación pertenece a su propia clase, y es una buena idea inspeccionar el tipo para determinar cuál debe ir primero:
Esta es la forma más sencilla de implementar la lógica empresarial y sigue siendo la más flexible para futuros cambios.
fuente
null
, aString
o aString[]
. Si el 99% de los objetos necesitará exactamente una cadena, encapsular cada cadena en una construcción separadaString[]
puede agregar una sobrecarga de almacenamiento considerable. Manejar el caso de una sola cadena usando una referencia directa a aString
requerirá más código, pero ahorrará almacenamiento y puede acelerar las cosas.La prueba de tipo es una herramienta, úsala sabiamente y puede ser un poderoso aliado. Úselo mal y su código comenzará a oler.
En nuestro software recibimos mensajes a través de la red en respuesta a solicitudes. Todos los mensajes deserializados comparten una clase base común
Message
.Las clases en sí eran muy simples, solo la carga útil como propiedades y rutinas de C # escritas para ordenarlas y desarmarlas (de hecho, generé la mayoría de las clases usando plantillas t4 a partir de la descripción XML del formato del mensaje)
El código sería algo como:
Por supuesto, uno podría argumentar que la arquitectura del mensaje podría estar mejor diseñada, pero fue diseñada hace mucho tiempo y no para C #, por lo que es lo que es. Aquí las pruebas de tipo nos resolvieron un problema real de una manera no muy lamentable.
Vale la pena señalar que C # 7.0 está obteniendo coincidencia de patrones (que en muchos aspectos es la prueba de tipo con esteroides) no puede ser del todo malo ...
fuente
Tome un analizador genérico JSON. El resultado de un análisis exitoso es una matriz, un diccionario, una cadena, un número, un valor booleano o un valor nulo. Puede ser cualquiera de esos. Y los elementos de una matriz o los valores en un diccionario pueden ser de nuevo cualquiera de esos tipos. Dado que los datos se proporcionan desde fuera de su programa, debe aceptar cualquier resultado (es decir, debe aceptarlo sin fallar; puede rechazar un resultado que no sea el esperado).
fuente