¿Se consideran excepciones como flujo de control un antipatrón serio? Si es así, ¿por qué?


129

A fines de los 90, trabajé bastante con una base de código que usaba excepciones como control de flujo. Implementó una máquina de estados finitos para manejar aplicaciones de telefonía. Últimamente me acuerdo de esos días porque he estado haciendo aplicaciones web MVC.

Ambos tienen Controllers que deciden a dónde ir después y suministran los datos a la lógica de destino. Las acciones de los usuarios del dominio de un teléfono de la vieja escuela, como los tonos DTMF, se convirtieron en parámetros para los métodos de acción, pero en lugar de devolver algo como a ViewResult, arrojaron un StateTransitionException.

Creo que la principal diferencia era que los métodos de acción eran voidfunciones. No recuerdo todas las cosas que hice con este hecho, pero he dudado incluso de recordar mucho porque desde ese trabajo, como hace 15 años, nunca vi esto en el código de producción en ningún otro trabajo . Asumí que esto era una señal de que se trataba de un llamado antipatrón.

¿Es este el caso, y si es así, por qué?

Actualización: cuando hice la pregunta, ya tenía en mente la respuesta de @ MasonWheeler, así que elegí la respuesta que más me pareció. Creo que la suya también es una buena respuesta.



25
No. En Python, el uso de excepciones como flujo de control se considera "pitónico".
user16764

44
Si tuviera que hacer algo así en Java, ciertamente no lanzaría una excepción por ello. Derivaría de alguna jerarquía que no sea de excepción, que no sea de error y que pueda lanzarse.
Thomas Eding

2
Para agregar a las respuestas existentes, aquí hay una breve guía que me ha servido bien: - Nunca use excepciones para "el camino feliz". La ruta feliz puede ser tanto (para la web) la solicitud completa, o simplemente un objeto / método. Todas las otras reglas sensatas todavía se aplican, por supuesto :)
Houen

8
¿Las excepciones no siempre controlan el flujo de una aplicación?

Respuestas:


109

Hay una discusión detallada de esto en la Wiki de Ward . En general, el uso de excepciones para flujo de control es un anti-patrón, con muchos notable situación - y el lenguaje específico (véase, por ejemplo Python ) tos excepciones tos .

Como resumen rápido de por qué, en general, es un antipatrón:

  • Las excepciones son, en esencia, declaraciones GOTO sofisticadas
  • La programación con excepciones, por lo tanto, conduce a un código más difícil de leer y comprender
  • La mayoría de los idiomas tienen estructuras de control existentes diseñadas para resolver sus problemas sin el uso de excepciones.
  • Los argumentos para la eficiencia tienden a ser discutibles para los compiladores modernos, que tienden a optimizarse con el supuesto de que no se usan excepciones para el flujo de control.

Lea la discusión en el wiki de Ward para obtener información mucho más detallada.


Vea también un duplicado de esta pregunta, aquí


18
Su respuesta hace que parezca que las excepciones son malas para todas las circunstancias, mientras que la búsqueda se centra en las excepciones como control de flujo.
cuál es el

14
@MasonWheeler La diferencia es que los bucles for / while contienen sus cambios de control de flujo claramente y hacen que el código sea fácil de leer. Si ve una instrucción for en su código, no tiene que intentar averiguar qué archivo contiene el final del ciclo. Los Goto no son malos porque algún dios dijo que lo eran, son malos simplemente porque son más difíciles de seguir que las construcciones en bucle. Las excepciones son similares, no imposibles pero lo suficientemente difíciles como para confundir las cosas.
Bill K

24
@BillK, luego argumenta eso y no hagas declaraciones simplistas sobre cómo se hacen las excepciones.
Winston Ewert

66
Bien, pero en serio, ¿qué pasa con los desarrolladores del lado del servidor y de la aplicación que entierran los errores con declaraciones de captura vacías en JavaScript? Es un fenómeno desagradable que me ha costado mucho tiempo y no sé cómo preguntar sin despotricar. Los errores son tu amigo.
Erik Reppen

12
@mattnz: ify foreachtambién son sofisticados GOTOs. Francamente, creo que la comparación con goto no es útil; Es casi como un término despectivo. El uso de GOTO no es intrínsecamente malo: tiene problemas reales, y las excepciones pueden compartirlos, pero pueden no serlo. Sería más útil escuchar esos problemas.
Eamon Nerbonne

110

El caso de uso para el que se diseñaron las excepciones es "Acabo de encontrar una situación que no puedo manejar adecuadamente en este momento, porque no tengo suficiente contexto para manejarlo, pero la rutina que me llamó (o algo más adelante) pila) debería saber cómo manejarlo ".

El caso de uso secundario es "Acabo de encontrar un error grave, y en este momento salir de este flujo de control para evitar la corrupción de datos u otros daños es más importante que tratar de continuar".

Si no está utilizando excepciones por alguna de estas dos razones, probablemente haya una mejor manera de hacerlo.


18
Sin embargo, esto no responde la pregunta. Para lo que fueron diseñados es irrelevante; Lo único que es relevante es por qué usarlos para controlar el flujo es malo, que es un tema que no tocó. Como ejemplo, las plantillas de C ++ se diseñaron para una cosa, pero están perfectamente bien para su uso en la metaprogramación, un uso que los diseñadores nunca anticiparon.
Thomas Bonini

66
@Krelp: ¡Los diseñadores nunca anticiparon muchas cosas, como terminar por accidente con un sistema de plantillas completo de Turing! Las plantillas de C ++ no son un buen ejemplo para usar aquí.
Mason Wheeler

15
@Krelp: las plantillas C ++ NO son 'perfectamente adecuadas' para la metaprogramación Son una pesadilla hasta que lo hagas bien, y luego tienden a código de solo escritura si no eres un genio de la plantilla. Es posible que desee elegir un mejor ejemplo.
Michael Kohne

Las excepciones perjudican la composición de la función en particular y la brevedad del código en general. Por ejemplo, cuando no tenía experiencia, utilicé una excepción en un proyecto, y causó problemas durante mucho tiempo, porque 1) las personas deben recordar atraparlo, 2) no se puede escribir const myvar = theFunctionporque myvar debe crearse a partir de try-catch, así no es más constante. Eso no significa que no lo use en C #, ya que es una corriente principal allí por cualquier razón, pero estoy tratando de reducirlos de todos modos.
Hi-Angel

2
@AndreasBonini Lo que están diseñados / destinados para asuntos porque a menudo resulta en decisiones de implementación del compilador / marco / tiempo de ejecución alineadas con el diseño. Por ejemplo, lanzar una excepción puede ser mucho más "costoso" que un simple retorno porque recopila información destinada a ayudar a alguien a depurar el código, como los seguimientos de pila, etc.
binki

29

Las excepciones son tan poderosas como las Continuaciones y GOTO. Son una construcción de flujo de control universal.

En algunos idiomas, son la única construcción de flujo de control universal. JavaScript, por ejemplo, no tiene ni Continuaciones ni GOTOsiquiera tiene Llamadas de cola adecuadas. Por lo tanto, si se desea implementar el flujo de control sofisticado en JavaScript, que tiene de utilizar excepciones.

El proyecto Microsoft Volta fue un proyecto de investigación (ahora descontinuado) para compilar código .NET arbitrario a JavaScript. .NET tiene excepciones cuya semántica no se asigna exactamente a JavaScript, pero lo más importante es que tiene subprocesos , y debe asignarlos de alguna manera a JavaScript. Volta hizo esto implementando Continuaciones de Volta usando Excepciones de JavaScript y luego implementó todas las construcciones de flujo de control .NET en términos de Continuaciones de Volta. Ellos tuvieron que utilizar excepciones como el flujo de control, porque no hay otro flujo de control construir lo suficientemente potente.

Usted mencionó las máquinas estatales. Las SM son triviales de implementar con llamadas de cola adecuadas: cada estado es una subrutina, cada transición de estado es una llamada de subrutina. Las SM también se pueden implementar fácilmente con GOTOCoroutines o Continuations. Sin embargo, Java no tiene ninguno de esos cuatro, pero tiene Excepciones. Por lo tanto, es perfectamente aceptable usarlos como flujo de control. (Bueno, en realidad, la opción correcta probablemente sería usar un lenguaje con la construcción de flujo de control adecuada, pero a veces puede estar atascado con Java).


77
Si tan solo hubiéramos tenido una adecuada recidiva de llamadas de cola, tal vez podríamos haber dominado el desarrollo web del lado del cliente con JavaScript y luego haber seguido extendiéndonos al lado del servidor y al móvil como la última solución de plataforma panorámica como un incendio forestal. Por desgracia, pero no fue así. Malditos sean nuestras insuficientes funciones de primera clase, cierres y paradigma basado en eventos Lo que realmente necesitábamos era lo real.
Erik Reppen

20
@ ErikReppen: Me doy cuenta de que solo estás siendo sarcástico, pero. . . Yo realmente no creo que el hecho de que "hemos dominado el desarrollo web del lado del cliente con JavaScript" tiene algo que ver con las características de la lengua. Tiene el monopolio en ese mercado, por lo que ha podido salirse con la suya con muchos problemas que no se pueden descartar con sarcasmo.
ruakh

1
La recursión de la cola habría sido una buena ventaja (están despreciando las funciones para tenerla en el futuro) pero sí, diría que ganó contra VB, Flash, Applets, etc. por razones relacionadas con las funciones. Si no redujera la complejidad y se normalizara tan fácilmente como lo hubiera hecho, habría tenido una competencia real en algún momento. Actualmente estoy ejecutando Node.js para manejar la reescritura de 21 archivos de configuración para una horrible pila C # + de Java y sé que no soy el único que está haciendo eso. Es muy bueno en lo que es bueno.
Erik Reppen

1
Muchos idiomas no tienen gotos (¡Goto considerado dañino!), Corutinas o continuaciones e implementan un control de flujo perfectamente válido simplemente usando ifs, whiles y llamadas a funciones (¡o gosubs!). De hecho, la mayoría de estos pueden usarse para emularse entre sí, así como una continuación puede usarse para representar todo lo anterior. (Por ejemplo, puede usar un tiempo para realizar un if, incluso si es una forma maloliente de codificar). Por lo tanto, NO son necesarias excepciones para realizar un control de flujo avanzado.
Shayne

2
Bueno, sí, podrías estar en lo cierto si. "For" en su forma de estilo C, sin embargo, se puede usar como un tiempo incómodo, y luego se puede usar junto con un if para implementar una máquina de estados finitos que luego puede emular todas las otras formas de control de flujo. De nuevo, apestosa forma de codificar, pero sí. Y de nuevo, goto considerado dañino. (Y argumentaría, también con el uso de excepciones en circunstancias no excepcionales). Tenga en cuenta que existen lenguajes perfectamente válidos y muy potentes que no proporcionan ni goto ni excepciones y funcionan bien.
Shayne

19

Como otros han mencionado en numerosas ocasiones ( por ejemplo, en esta pregunta de desbordamiento de pila ), el principio de menor asombro prohibirá que use excepciones en exceso para fines de control de flujo solamente. Por otro lado, ninguna regla es 100% correcta, y siempre hay casos en los que una excepción es "la herramienta correcta", muy parecida a gotosí misma, por cierto, que se presenta en forma breaky continueen lenguajes como Java, que a menudo son la forma perfecta de saltar de bucles muy anidados, que no siempre son evitables.

La siguiente publicación de blog explica un caso de uso bastante complejo pero también bastante interesante para un no local ControlFlowException :

Explica cómo dentro de jOOQ (una biblioteca de abstracción SQL para Java) (descargo de responsabilidad: trabajo para el proveedor), tales excepciones se usan ocasionalmente para abortar el proceso de representación SQL temprano cuando se cumple alguna condición "rara".

Ejemplos de tales condiciones son:

  • Se encuentran demasiados valores de enlace. Algunas bases de datos no admiten números arbitrarios de valores de enlace en sus sentencias SQL (SQLite: 999, Ingres 10.1.0: 1024, Sybase ASE 15.5: 2000, SQL Server 2008: 2100). En esos casos, jOOQ aborta la fase de representación de SQL y vuelve a representar la instrucción SQL con valores de enlace en línea. Ejemplo:

    // Pseudo-code attaching a "handler" that will
    // abort query rendering once the maximum number
    // of bind values was exceeded:
    context.attachBindValueCounter();
    String sql;
    try {
    
      // In most cases, this will succeed:
      sql = query.render();
    }
    catch (ReRenderWithInlinedVariables e) {
      sql = query.renderWithInlinedBindValues();
    }

    Si extrajéramos explícitamente los valores de enlace de la consulta AST para contarlos cada vez, desperdiciaríamos valiosos ciclos de CPU para el 99.9% de las consultas que no sufren este problema.

  • Parte de la lógica está disponible solo indirectamente a través de una API que queremos ejecutar solo "parcialmente". El UpdatableRecord.store()método genera una declaración INSERTo UPDATE, dependiendo de los Recordindicadores internos de. Desde el "exterior", no sabemos qué tipo de lógica está contenida store()(por ejemplo, bloqueo optimista, manejo de escucha de eventos, etc.), por lo que no queremos repetir esa lógica cuando almacenamos varios registros en una declaración por lotes, donde nos gustaría tener store()solo generar la instrucción SQL, no ejecutarla realmente. Ejemplo:

    // Pseudo-code attaching a "handler" that will
    // prevent query execution and throw exceptions
    // instead:
    context.attachQueryCollector();
    
    // Collect the SQL for every store operation
    for (int i = 0; i < records.length; i++) {
      try {
        records[i].store();
      }
    
      // The attached handler will result in this
      // exception being thrown rather than actually
      // storing records to the database
      catch (QueryCollectorException e) {
    
        // The exception is thrown after the rendered
        // SQL statement is available
        queries.add(e.query());                
      }
    }

    Si hubiéramos externalizado la store()lógica en una API "reutilizable" que se puede personalizar para que opcionalmente no ejecute el SQL, estaríamos buscando crear una API bastante difícil de mantener y difícilmente reutilizable.

Conclusión

En esencia, nuestro uso de estos mensajes no locales gotoes similar a lo que Mason Wheeler dijo en su respuesta:

"Acabo de encontrar una situación que no puedo manejar adecuadamente en este momento, porque no tengo suficiente contexto para manejarlo, pero la rutina que me llamó (o algo más arriba en la pila de llamadas) debería saber cómo manejarlo ".

Ambos usos de ControlFlowExceptionsfueron bastante fáciles de implementar en comparación con sus alternativas, lo que nos permitió reutilizar una amplia gama de lógica sin refactorizarla de las partes internas relevantes.

Pero la sensación de que esto sea una sorpresa para los futuros mantenedores permanece. El código se siente bastante delicado y, si bien fue la elección correcta en este caso, siempre preferimos no usar excepciones para el flujo de control local , donde es fácil evitar el uso de ramificaciones ordinarias if - else.


11

El uso de excepciones para el flujo de control generalmente se considera un antipatrón, pero hay excepciones (sin juego de palabras).

Se ha dicho mil veces que las excepciones son para condiciones excepcionales. Una conexión de base de datos rota es una condición excepcional. Un usuario que ingresa letras en un campo de entrada que solo debe permitir números no lo es .

Un error en su software que hace que se llame a una función con argumentos ilegales, por ejemplo, nulldonde no se permite, es una condición excepcional.

Al utilizar excepciones para algo que no es excepcional, está utilizando abstracciones inapropiadas para el problema que está tratando de resolver.

Pero también puede haber una penalización de rendimiento. Algunos idiomas tienen una implementación de manejo de excepciones más o menos eficiente, por lo que si su idioma de elección no tiene un manejo eficiente de excepciones, puede ser muy costoso, en términos de rendimiento *.

Pero otros lenguajes, por ejemplo Ruby, tienen una sintaxis similar a una excepción para el flujo de control. Las situaciones excepcionales son manejadas por los operadores raise/ rescue. Pero puede usar throw/ catchpara construcciones de flujo de control similares a excepciones **.

Entonces, aunque las excepciones generalmente no se usan para el flujo de control, su idioma de elección puede tener otros modismos.

* Por ejemplo, el uso costoso de las excepciones de rendimiento: una vez fui configurado para optimizar una aplicación ASP.NET Web Form de bajo rendimiento. Resultó que el renderizado de una mesa grande requería int.Parse()aprox. mil cadenas vacías en una página promedio, lo que resulta en aprox. mil excepciones siendo manejadas. ¡Al reemplazar el código con int.TryParse()me afeité un segundo! ¡Por cada solicitud de página!

** Esto puede ser muy confuso para un programador que viene a Ruby desde otros lenguajes, ya que ambos throwy catchson las palabras clave asociadas con excepciones en muchos otros idiomas.


1
+1 para "Al utilizar excepciones para algo que no es excepcional, está utilizando abstracciones inapropiadas para el problema que está tratando de resolver".
Giorgio

8

Es completamente posible manejar condiciones de error sin el uso de excepciones. Algunos lenguajes, especialmente C, ni siquiera tienen excepciones, y las personas aún logran crear aplicaciones bastante complejas con él. La razón por la que las excepciones son útiles es que le permiten especificar sucintamente dos flujos de control esencialmente independientes en el mismo código: uno si se produce un error y otro si no es así. Sin ellos, terminas con un código en todo el lugar que se ve así:

status = getValue(&inout);
if (status < 0)
{
    logError("message");
    return status;
}

doSomething(*inout);

O equivalente en su idioma, como devolver una tupla con un valor como estado de error, etc. A menudo las personas que señalan cuán "costoso" es el manejo de excepciones, descuidan todas las ifdeclaraciones adicionales como las anteriores que deben agregar si no ' No use excepciones.

Si bien este patrón ocurre con mayor frecuencia cuando se manejan errores u otras "condiciones excepcionales", en mi opinión, si comienza a ver código repetitivo como este en otras circunstancias, tiene un argumento bastante bueno para usar excepciones. Dependiendo de la situación y la implementación, puedo ver que las excepciones se usan de manera válida en una máquina de estados, porque tiene dos flujos de control ortogonales: uno que cambia el estado y otro para los eventos que ocurren dentro de los estados.

Sin embargo, esas situaciones son raras, y si va a hacer una excepción (juego de palabras) a la regla, es mejor que esté preparado para mostrar su superioridad a otras soluciones. Una desviación sin tal justificación se llama correctamente un antipatrón.


2
Siempre y cuando las declaraciones solo fallen en circunstancias 'excepcionales', la lógica de predicción de ramificación de las CPU modernas hace que su costo sea insignificante. Este es un lugar donde las macros realmente pueden ayudar, siempre que tenga cuidado y no intente hacer demasiado en ellas.
James

5

En Python, las excepciones se utilizan para la terminación del generador y la iteración. Python tiene bloques try / except muy eficientes, pero en realidad generar una excepción tiene algo de sobrecarga.

Debido a la falta de saltos de varios niveles o una declaración goto en Python, a veces he usado excepciones:

class GOTO(Exception):
  pass

try:
  # Do lots of stuff
  # in here with multiple exit points
  # each exit point does a "raise GOTO()"
except GOTO:
  pass
except Exception as e:
  #display error

66
Si tiene que terminar el cálculo en algún lugar de una declaración profundamente anidada e ir a algún código de continuación común, es muy probable que esta ruta de ejecución en particular se tenga en cuenta como una función, returnen lugar de goto.
9000

3
@ 9000 Estaba a punto de comentar exactamente lo mismo ... Mantenga los try:bloques en 1 o 2 líneas, por favor, nunca try: # Do lots of stuff.
wim

1
@ 9000, en algunos casos, claro. Pero luego pierdes el acceso a las variables locales y mueves tu código a otro lugar, cuando el proceso es coherente y lineal.
gahooa

44
@gahooa: Solía ​​pensar como tú. Era una señal de la pobre estructura de mi código. Cuando pensé más en ello, noté que los contextos locales pueden desenredarse y que todo el desorden se convierte en funciones cortas con pocos parámetros, pocas líneas de código y un significado muy preciso. Nunca miré hacia atrás.
9000

4

Dibujemos un uso de excepción de este tipo:

El algoritmo busca de forma recursiva hasta encontrar algo. Entonces, volviendo de la recursión, uno tiene que verificar el resultado para ser encontrado, y luego regresar, de lo contrario, continuar. Y eso regresa repetidamente desde cierta profundidad de recursión.

Además de necesitar un booleano adicional found(para empacar en una clase, donde de lo contrario tal vez solo se hubiera devuelto un int), y para la profundidad de recursión ocurre el mismo postludio.

Tal desenrollamiento de una pila de llamadas es para lo que es una excepción. Entonces, me parece un medio de codificación no similar a Goto, más inmediato y apropiado. No es necesario, uso raro, quizás mal estilo, pero va al grano. Comparable con la operación de corte Prolog.


Hmm, ¿es el voto negativo para la posición realmente contraria, o para una argumentación insuficiente o corta? Tengo mucha curiosidad
Joop Eggen

Estoy enfrentando exactamente esta situación, ¿esperaba que pudieras ver lo que piensas de mi siguiente pregunta? Uso
recurrente de

@Jodes Acabo de leer tu interesante técnica, pero estoy bastante ocupado por ahora. Espero que alguien más arroje su luz sobre el tema.
Joop Eggen

4

La programación es sobre el trabajo.

Creo que la forma más fácil de responder a esto es entender el progreso que OOP ha logrado a lo largo de los años. Todo lo que se hace en OOP (y la mayoría de los paradigmas de programación, para el caso) se basa en la necesidad de trabajar .

Cada vez que se llama a un método, la persona que llama dice "No sé cómo hacer este trabajo, pero tú sabes cómo hacerlo, así que lo haces por mí".

Esto presentaba una dificultad: ¿qué sucede cuando el método llamado generalmente sabe cómo hacer el trabajo, pero no siempre? Necesitábamos una forma de comunicarnos "Quería ayudarte, realmente lo hice, pero no puedo hacer eso".

Una metodología temprana para comunicar esto fue simplemente devolver un valor "basura". Tal vez espere un número entero positivo, por lo que el método llamado devuelve un número negativo. Otra forma de lograr esto era establecer un valor de error en alguna parte. Desafortunadamente, ambas formas resultaron en un código repetitivo de " déjenme revisar-aquí-para-asegurar-todo-kosher ". A medida que las cosas se vuelven más complicadas, este sistema se desmorona.

Una analogía excepcional

Digamos que tienes un carpintero, un plomero y un electricista. Desea que el plomero arregle su fregadero, así que él lo mira. No es muy útil si solo te dice: "Lo siento, no puedo arreglarlo. Está roto". Demonios, es aún peor si él echara un vistazo, se fuera y le enviara una carta diciéndole que no podía arreglarlo. Ahora tiene que revisar su correo antes de saber que él no hizo lo que usted quería.

Lo que preferiría es que le dijera: "Mire, no pude arreglarlo porque parece que su bomba no funciona".

Con esta información, puede concluir que desea que el electricista analice el problema. Quizás el electricista encuentre algo relacionado con el carpintero, y necesitará que el carpintero lo arregle.

Demonios, es posible que ni siquiera sepas que necesitas un electricista, es posible que no sepas a quién necesitas. Usted es solo una gerencia intermedia en un negocio de reparación de viviendas, y su enfoque es la plomería. Entonces le dices a tu jefe sobre el problema, y ​​luego le dice al electricista que lo arregle.

Esto es lo que las excepciones son el modelado: modos de falla complejos de manera desacoplada. El plomero no necesita saber acerca de electricista, ni siquiera necesita saber que alguien en la cadena puede solucionar el problema. Simplemente informa sobre el problema que encontró.

Entonces ... ¿un antipatrón?

Ok, entonces entender el punto de excepciones es el primer paso. Lo siguiente es entender qué es un antipatrón.

Para calificar como un antipatrón, necesita

  • resolver el problema
  • tener consecuencias definitivamente negativas

El primer punto se cumple fácilmente: el sistema funcionó, ¿verdad?

El segundo punto es más pegajoso. La razón principal para usar excepciones ya que el flujo de control normal es malo es porque ese no es su propósito. Cualquier pieza de funcionalidad dada en un programa debe tener un propósito relativamente claro, y cooptar ese propósito conduce a una confusión innecesaria.

Pero eso no es un daño definitivo . Es una forma pobre de hacer las cosas, y raro, pero ¿un antipatrón? No. Solo ... extraño.


Hay una mala consecuencia, al menos en los idiomas que proporcionan un seguimiento completo de la pila. Como el código está muy optimizado con muchas líneas, el seguimiento real de la pila y el que un desarrollador quiere ver difieren mucho y, por lo tanto, la generación del seguimiento de la pila es costosa. El uso excesivo de excepciones es muy malo para el rendimiento en dichos lenguajes (Java, C #).
maaartinus

"Es una forma pobre de hacer las cosas". ¿No debería ser suficiente para clasificarlo como un antipatrón?
Maybe_Factor

@Maybe_Factor Según la definición de un patrón de hormigas, no.
MirroredFate
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.