¿Cuándo está bien la prueba de tipo?

53

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 SuperTypey 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 SuperTypees realmente un SubTypeAo un SubTypeB.

Maravilloso.

Pero, todavía tenemos is aoperaciones 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.

svidgen
fuente
1
Lea sobre inferencia de tipos y sistemas de tipos
Basile Starynkevitch
44
Vale la pena señalar que ni Java ni C # tenían genéricos en su primera versión. Tenías que lanzar hacia y desde Objeto para usar contenedores.
Doval
66
"Verificación de tipo" casi siempre se refiere a verificar si el código observa las reglas de escritura estática del lenguaje. Lo que quiere decir generalmente se llama prueba de tipo .
3
Es por eso que me gusta escribir C ++ y dejar RTTI desactivado. Cuando uno literalmente no puede probar los tipos de objetos en tiempo de ejecución, obliga al desarrollador a adherirse a un buen diseño OO con respecto a la pregunta que se hace aquí.

Respuestas:

48

"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 SuperTypey 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:

  1. 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.

  2. No puedes ajustar las clases. Piensa en subclasificar, pero no puede. Muchas clases en Java, por ejemplo, están designadas final. Intenta incluir public 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 son final, y hacer clases finales 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.

  3. 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.

  4. 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 de addByte, addShort, addInt, addLong, addFloat, addDouble, addBoolean, addChar, y addStringvariantes (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, tu is-aois-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.

Jonathan Eunice
fuente
Si una operación no puede realizarse sobre un tipo particular, pero puede realizarse sobre dos tipos diferentes a los que sería implícitamente convertible, la sobrecarga solo es apropiada si ambas conversiones producirían resultados idénticos. De lo contrario, uno termina con comportamientos malos como en .NET: (1.0).Equals(1.0f)arrojando verdadero [el argumento promueve a double], pero (1.0f).Equals(1.0)arrojando falso [el argumento promueve a object]; en Java, Math.round(123456789*1.0)rinde 123456789, pero Math.round(123456789*1.0)rinde 123456792 [el argumento promueve a floatmás que a double].
supercat
Este es el argumento clásico contra la conversión / escalado automático de tipos. Resultados malos y paradójicos, al menos en casos extremos. Estoy de acuerdo, pero no estoy seguro de cómo piensa que se relacione con mi respuesta.
Jonathan Eunice
Estaba respondiendo a su punto # 4 que parecía estar abogando por la sobrecarga en lugar de utilizar métodos con nombres diferentes con diferentes tipos.
supercat
2
@supercat Culpa a mi pobre vista, pero las dos expresiones Math.roundme parecen idénticas. ¿Cual es la diferencia?
Lily Chung
2
@IstvanChung: Ooops ... esto último debería haber sido Math.round(123456789)[indicativo de lo que podría suceder si alguien reescribe Math.round(thing.getPosition() * COUNTS_PER_MIL)para devolver un valor de posición sin escala, sin darse cuenta de que getPositiondevuelve un into long.]
supercat
25

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 de other. 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.

Karl Bielefeldt
fuente
1
Estuve momentáneamente encantado con el caso de deserialización, recordando falsamente usarlo allí; ¡Pero ahora no puedo imaginar cómo lo habría hecho! Sé que he hecho algunas búsquedas de tipos raras para eso; pero no estoy seguro de si eso constituye una prueba de tipo . Tal vez la serialización es un paralelo más cercano: tener que interrogar objetos para su tipo concreto.
svidgen
1
Serializar es usualmente factible usando polimorfismo. Al deserializar, a menudo haces algo como BaseClass base = deserialize(input), porque aún no conoces el tipo, entonces lo haces if (base instanceof Derived) derived = (Derived)basepara almacenarlo como su tipo derivado exacto.
Karl Bielefeldt
1
La igualdad tiene sentido. En mi experiencia, tales métodos a menudo tienen una forma como “si estos dos objetos tienen el mismo tipo concreto, devuelva si todos sus campos son iguales; de lo contrario, devuelve falso (o no comparable) ".
Jon Purdy
2
En particular, el hecho de que te encuentres probando un tipo es un buen indicador de que las comparaciones de igualdad polimórfica son un nido de víboras.
Steve Jessop
12

El caso estándar (pero con suerte raro) se ve así: si en la siguiente situación

public void DoSomethingTo(SuperType o) {
  if (o isa SubTypeA) {
    DoSomethingA((SubTypeA) o )
  } else {
    DoSomethingB((SubTypeB) o );
  }
}

las funciones DoSomethingAo DoSomethingBno pueden implementarse fácilmente como funciones miembro del árbol de herencia de SuperType/ SubTypeA/ SubTypeB. Por ejemplo, si

  • los subtipos son parte de una biblioteca que no puede cambiar, o
  • si agregar el código para DoSomethingXXXesa 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 SubTypeAy SubTypeB, o tratando de volver a implementarlo DoSomethingcompletamente en términos de operaciones básicas de SuperType), 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 llama DoSomethingA, y un segundo bucle para objetos del subtipo B, que llama DoSomethingB.

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 DoSomethingToanterior. 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éndolo SuperTypecon muchos métodos abstractos realmente no funcionaría, o significaría diseñar demasiado las cosas por completo.

Doc Brown
fuente
2
¿Hay alguna posibilidad de que pueda agregar un pequeño ejemplo concreto para convencer a mi cerebro nebuloso de que este escenario no está ideado?
svidgen
Para ser claros, no digo que sea artificial. Sospecho que hoy estoy sumamente loco por la niebla.
svidgen
@svidgen: esto está lejos de ser artificial, en realidad me encontré con esta situación hoy (aunque no puedo publicar este ejemplo aquí porque contiene elementos internos de negocios). Y estoy de acuerdo con las otras respuestas de que usar el operador "es" debería ser una excepción y solo debe hacerse en casos excepcionales.
Doc Brown
Creo que su edición, que pasé por alto, es un buen ejemplo. ... ¿Hay casos en los que esto está bien, incluso cuando se hace el control SuperTypey es subclases?
svidgen
66
Aquí hay un ejemplo concreto: el contenedor de nivel superior en una respuesta JSON de un servicio web puede ser un diccionario o una matriz. Por lo general, tiene alguna herramienta que convierte el JSON en objetos reales (por ejemplo, NSJSONSerializationen 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 ejemplo if ([theResponse isKindOfClass:[NSArray class]])...) .
Caleb
5

Como lo llama el tío Bob:

When your compiler forgets about the type.

En uno de sus episodios de Clean Coder, dio un ejemplo de una llamada de función que se utiliza para devolver Employees. Manageres un sub-tipo de Employee. Supongamos que tenemos un servicio de aplicación que acepta una Manageridentificación y lo convoca a la oficina :) La función getEmployeeById()devuelve un supertipo Employee, pero quiero verificar si se devuelve un administrador en este caso de uso.

Por ejemplo:

var manager = employeeRepository.getEmployeeById(empId);
if (!(manager is Manager))
   throw new Exception("Invalid Id specified.");
manager.summon();

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.

Songo
fuente
1
¿Por qué Managerla implementación de summon()simplemente no arroja la excepción en este ejemplo?
svidgen
@svidgen tal vez el CEOpuede convocar Managers.
user253751
@svidgen, entonces no sería tan claro que employeeRepository.getEmployeeById (empId) se espera que regrese un gerente
Ian
@ Ian, no veo eso como un problema. Si el código de llamada solicita un Employee, solo debería importarle que obtenga algo que se comporte como un Employee. Si diferentes subclases Employeetienen diferentes permisos, responsabilidades, etc., ¿qué hace que la prueba de tipo sea una mejor opción que un sistema de permisos real?
svidgen
3

¿Cuándo está bien la verificación de tipos?

Nunca.

  1. Al tener el comportamiento asociado con ese tipo en alguna otra función, está violando el Principio Abierto Cerrado , ya que es libre de modificar el comportamiento existente del tipo cambiando la isclá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 la isverificación.
  2. Más importante aún, los ischeques son una fuerte señal de que está violando el Principio de sustitución de Liskov . Cualquier cosa que funcione SuperTypedebe ser completamente ignorante de qué subtipos puede haber.
  3. Estás asociando implícitamente algún comportamiento con el nombre del tipo. Esto hace que su código sea más difícil de mantener porque estos contratos implícitos se extienden por todo el código y no se garantiza su cumplimiento universal y consistente como lo hacen los miembros reales de la clase.

Dicho todo esto, los ischeques 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.

Telastyn
fuente
3
Hay una advertencia menor: implementar tipos de datos algebraicos en idiomas que no los admiten. Sin embargo, el uso de la herencia y la verificación de tipos es puramente un detalle de implementación hacky allí; La intención no es introducir subtipos, sino clasificar los valores. Lo menciono solo porque los ADT son útiles y nunca es un calificador muy fuerte, pero de lo contrario estoy totalmente de acuerdo; instanceoffiltra detalles de implementación y rompe la abstracción.
Doval
17
"Nunca" es una palabra que realmente no me gusta en ese contexto, especialmente cuando contradice lo que escribe a continuación.
Doc Brown
10
Si por "nunca" realmente quieres decir "a veces", entonces tienes razón.
Caleb
2
OK significa permisible, aceptable, etc., pero no necesariamente ideal u óptimo. Contraste con no está bien : si algo no está bien, entonces no debería hacerlo en absoluto. Como indica, la necesidad de verificar el tipo de algo puede ser una indicación de problemas más profundos en el código, pero hay momentos en que es la opción más conveniente y menos mala, y en tales situaciones, obviamente está bien usar las herramientas en su disposición. (Si fuera fácil evitarlos en todas las situaciones, probablemente no estarían allí en primer lugar). La pregunta se reduce a identificar esas situaciones, y nunca ayuda.
Caleb
2
@supercat: los métodos deben exigir el tipo menos específico posible que satisfaga sus requisitos . 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".
cHao
2

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).

Ian
fuente
El patrón de visitante suele ser una buena forma de resolver su caso de IU.
Ian Goldby
@IanGoldby, acordó en ocasiones que puede ser, sin embargo, todavía está haciendo "pruebas de tipo", solo oculto un poco.
Ian
¿Oculto en el mismo sentido que está oculto cuando llamas a un método virtual normal? ¿O quieres decir algo más? El patrón de visitante que he usado no tiene declaraciones condicionales que dependan del tipo. Todo se hace por el idioma.
Ian Goldby
@IanGoldby, quise decir oculto en el sentido de que puede hacer que el código WPF o WinForms sea más difícil de entender. Espero que para algunas IU de base web funcione muy bien.
Ian
2

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):

public static TSource ElementAt<TSource>(this IEnumerable<TSource> source, int index) { 
    IList<TSource> list = source as IList<TSource>;

    if (list != null) return list[index];
    // ... and then an enumerator is created and MoveNext is called index times

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.

Menno van den Heuvel
fuente
Podría haberse diseñado mejor al proporcionar un medio conveniente por el cual las interfaces pueden proporcionar implementaciones predeterminadas, y luego haber IEnumerable<T>incluido muchos métodos como esos List<T>, junto con una Featurespropiedad 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 admitir Addmientras se garantiza que los contenidos existentes serían inmutables]).
supercat
Excepto en escenarios en los que un tipo puede necesitar contener una de dos cosas completamente diferentes en diferentes momentos y los requisitos son claramente excluyentes entre sí (y, por lo tanto, el uso de campos separados sería redundante), generalmente considero que la necesidad de probar el casting es una señal que los miembros que deberían haber sido parte de una interfaz base, no lo eran. Eso no quiere decir que el código que usa una interfaz que debería haber incluido algunos miembros pero no no debería usar try-casting para evitar la omisión de la interfaz base, pero esas interfaces básicas de escritura deberían minimizar la necesidad de los clientes de probar.
supercat
1

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 severa CollisionShapeque 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 como Sphere, 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:

detectCollision(Sphere, Sphere)
detectCollision(Sphere, Box)
detectCollision(Sphere, ConvexHull)
detectCollision(Box, ConvexHull)
...

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 GameObjectsy sus correspondientes CollisionShapes. 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.

Martín
fuente
1

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:

void DrawEntity(Entity entity) {
    if (entity instanceof Circle) {
        DrawCircle((Circle) entity));
    else if (entity instanceof Rectangle) {
        DrawRectangle((Rectangle) entity));
    } ...
}

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.

Honza Brabec
fuente
0

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 Stringo List).

Por verificación dinámica quiero decir:

boolean checkType(Type type, Object object) {
    if (object.isOfType(type)) {

    }
}

y no codificado

boolean checkIsManaer(Object object) {
    if (object instanceof Manager) {

    }
}
m3th0dman
fuente
0

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 Equalsmétodo, que en la mayoría de los idiomas se supone que es claro Object. 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 los Fooobjetos 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.

meustrus
fuente
1
ArrayListsno 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. equalstiene 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.
Doval
1
Técnicamente, las colecciones Java no lanzan sus contenidos a nada. El compilador inyecta los String x = (String) myListOfStrings.get(0)
La última vez que miré la fuente de Java (que puede haber sido 1.6 o 1.5) hubo un reparto explícito en la fuente ArrayList. Genera una advertencia de compilador (suprimida) por una buena razón, pero de todos modos está permitido. Supongo que se podría decir que debido a cómo se implementan los genéricos, siempre fue así Objecthasta que se accedió de todos modos; los genéricos en Java solo proporcionan conversión implícita asegurada por las reglas del compilador.
meustrus
0

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:

abstract class Vehicle
{
    abstract void Process();
}

class Car : Vehicle { ... }
class Boat : Vehicle { ... }
class Truck : Vehicle { ... }

Ahora digamos que nuestra lógica de negocios es literalmente "todos los automóviles tienen prioridad sobre los barcos y camiones". Agregar una Prioritypropiedad a la clase no le permite expresar esta lógica comercial de manera limpia porque terminará con esto:

abstract class Vehicle
{
    abstract void Process();
    abstract int Priority { get }
}

class Car : Vehicle { public Priority { get { return 1; } } ... }
class Boat : Vehicle { public Priority { get { return 2; } } ... }
class Truck : Vehicle { public Priority { get { return 2; } } ... }

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:

static class Priorities
{
    public const int CAR_PRIORITY = 1;
    public const int BOAT_PRIORITY = 2;
    public const int TRUCK_PRIORITY = 2;
}

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:

class VehicleScheduler : IScheduleVehicles
{
    public Vehicle WhichVehicleGoesFirst(Vehicle vehicle1, Vehicle vehicle2)
    {
        if(vehicle1 is Car) return vehicle1;
        if(vehicle2 is Car) return vehicle2;
        return vehicle1;
    }
}

Esta es la forma más sencilla de implementar la lógica empresarial y sigue siendo la más flexible para futuros cambios.

Scott Whitlock
fuente
No estoy de acuerdo con su ejemplo particular, pero sí estaría de acuerdo con el principio de tener una referencia privada que pueda tener diferentes tipos con diferentes significados. Lo que sugeriría como un mejor ejemplo sería un campo que puede contener null, a Stringo a String[]. Si el 99% de los objetos necesitará exactamente una cadena, encapsular cada cadena en una construcción separada String[]puede agregar una sobrecarga de almacenamiento considerable. Manejar el caso de una sola cadena usando una referencia directa a a Stringrequerirá más código, pero ahorrará almacenamiento y puede acelerar las cosas.
supercat
0

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:

Message response = await PerformSomeRequest(requestParameter);

// Server (out of our control) would send one message as response, but 
// the actual message type is not known ahead of time (it depended on 
// the exact request and the state of the server etc.)
if (response is ErrorMessage)
{ 
    // Extract error message and pass along (for example using exceptions)
}
else if (response is StuffHappenedMessage)
{
    // Extract results
}
else if (response is AnotherThingHappenedMessage)
{
    // Extract another type of result
}
// Half a dozen other possible checks for messages

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 ...

Isak Savo
fuente
0

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).

gnasher729
fuente
Bueno, algunos deserializadores JSON intentarán crear una instancia de un árbol de objetos si su estructura tiene información de tipo para "campos de inferencia". Pero sí. Creo que esta es la dirección en la que se dirigió la respuesta de Karl B.
svidgen