Formas de eliminar el cambio en el código [cerrado]


175

¿Cuáles son las formas de eliminar el uso del interruptor en el código?


18
¿Por qué querrías eliminar los interruptores, cuando se usan adecuadamente? Por favor, ¿puede explicar su pregunta?
RB.

Yo voto que esta pregunta sea editable por la comunidad porque todos dicen lo mismo y podría ser bueno consolidar todas las opiniones;)
Josh

2
El interruptor no es tan estándar como otras instrucciones. Por ejemplo, en C ++, es probable que olvide el 'descanso' y luego obtendrá resultados inesperados. Además, este 'descanso' es demasiado similar a un GOTO. Nunca he tratado de eliminar interruptores, pero seguro que no son mis instrucciones favoritas. ;)

2
Creo que se refiere al concepto de cambio y no a la declaración de cambio de C ++ o Java. Un interruptor también puede ser una cadena de bloques 'if-else if'.
Outlaw Programmer

2
Puede divertirse: hay una gran cantidad de programadores notables de la comunidad ágil que se unen a la campaña anti-IF: antiifcampaign.com/supporters.html
Mike A

Respuestas:


267

Las declaraciones de cambio no son un antipatrón per se, pero si está codificando orientado a objetos, debe considerar si el uso de un cambio se resuelve mejor con polimorfismo en lugar de usar una declaración de cambio.

Con polimorfismo, esto:

foreach (var animal in zoo) {
    switch (typeof(animal)) {
        case "dog":
            echo animal.bark();
            break;

        case "cat":
            echo animal.meow();
            break;
    }
}

se convierte en esto:

foreach (var animal in zoo) {
    echo animal.speak();
}

2
Me criticaron por una sugerencia similar en stackoverflow.com/questions/374239/… Muchas personas no creen en el polimorfismo :) Muy buen ejemplo.
Nazgob

30
-1: Nunca he usado una declaración de cambio typeof, y esta respuesta no sugiere formas o razones para evitar las declaraciones de cambio en otras situaciones.
Kevin

3
Estoy de acuerdo con @Kevin: el ejemplo dado no muestra realmente cómo eliminar el cambio por polimorfismo. Los ejemplos simples: obtenga el nombre de enum cambiando sus valores, o realice algún código con los valores apropiados en algún tipo de algoritmo.
HotJard

Me preguntaba cómo eliminarlo antes de que pueda confiar en implementaciones específicas, es decir, en una fábrica. Y encontré un gran ejemplo aquí stackoverflow.com/a/3434505/711855
juanmf

1
Un ejemplo muy trivial.
GuardianX

239

Ver el olor de las declaraciones del interruptor :

Por lo general, las declaraciones de cambio similares se encuentran dispersas en un programa. Si agrega o elimina una cláusula en un interruptor, a menudo también tiene que encontrar y reparar los demás.

Tanto Refactoring como Refactoring to Patterns tienen enfoques para resolver esto.

Si su (pseudo) código se ve así:

class RequestHandler {

    public void handleRequest(int action) {
        switch(action) {
            case LOGIN:
                doLogin();
                break;
            case LOGOUT:
                doLogout();
                break;
            case QUERY:
               doQuery();
               break;
        }
    }
}

Este código viola el Principio de Abierto Cerrado y es frágil para cada nuevo tipo de código de acción que se presente. Para remediar esto, podría introducir un objeto 'Comando':

interface Command {
    public void execute();
}

class LoginCommand implements Command {
    public void execute() {
        // do what doLogin() used to do
    }
}

class RequestHandler {
    private Map<Integer, Command> commandMap; // injected in, or obtained from a factory
    public void handleRequest(int action) {
        Command command = commandMap.get(action);
        command.execute();
    }
}

Si su (pseudo) código se ve así:

class House {
    private int state;

    public void enter() {
        switch (state) {
            case INSIDE:
                throw new Exception("Cannot enter. Already inside");
            case OUTSIDE:
                 state = INSIDE;
                 ...
                 break;
         }
    }
    public void exit() {
        switch (state) {
            case INSIDE:
                state = OUTSIDE;
                ...
                break;
            case OUTSIDE:
                throw new Exception("Cannot leave. Already outside");
        }
    }

Entonces podría introducir un objeto 'Estado'.

// Throw exceptions unless the behavior is overriden by subclasses
abstract class HouseState {
    public HouseState enter() {
        throw new Exception("Cannot enter");
    }
    public HouseState leave() {
        throw new Exception("Cannot leave");
    }
}

class Inside extends HouseState {
    public HouseState leave() {
        return new Outside();
    }
}

class Outside extends HouseState {
    public HouseState enter() {
        return new Inside();
    }
}

class House {
    private HouseState state;
    public void enter() {
        this.state = this.state.enter();
    }
    public void leave() {
        this.state = this.state.leave();
    }
}

Espero que esto ayude.


77
Gracias por el gran ejemplo de cómo refactorizar el código. Aunque podría decir que al principio es un poco difícil de leer (porque uno tiene que cambiar entre varios archivos para comprenderlo por completo)
rshimoda

8
Los argumentos en contra del cambio son válidos siempre que se dé cuenta de que la solución polimórfica sacrifica la simplicidad del código. Además, si siempre almacena sus casos de conmutador en enumeraciones, algunos compiladores le advertirán que faltan estados en el conmutador.
Harvey

1
Este es un gran ejemplo sobre operaciones completas / incompletas y, por supuesto, reestructurar el código en OOP. Gracias una tonelada. Creo que será útil si los proponentes de OOP / Patrones de diseño sugieren tratar los conceptos de OOP como operadores en lugar de conceptos . Me refiero a que "extensiones", "fábrica", "implementos", etc. se usan con tanta frecuencia en archivos, clases, ramas. Deben ser simples como operadores como "+", "-", "+ =", "?:", "==", "->" etc. Cuando un programador los usa en su mente simplemente como operadores, solo entonces puede pensar en toda la biblioteca de clase sobre el estado del programa y (in) operaciones completas.
namespaceform

13
Estoy empezando a pensar que SWITCH es mucho más comprensible y lógico, en comparación con esto. Y generalmente me gusta mucho la OOP, pero esta resolución me parece demasiado abstracta
JK,

1
Con el objeto Command, ¿el código para generar Map<Integer, Command>no necesitaría un interruptor?
ataulm

41

Un switch es un patrón, ya sea implementado con una declaración de switch, si es otra cadena, tabla de búsqueda, polimorfismo opulento, coincidencia de patrones u otra cosa.

¿Desea eliminar el uso de la " declaración de cambio " o el " patrón de cambio "? El primero se puede eliminar, el segundo, solo si se puede usar otro patrón / algoritmo, y la mayoría de las veces eso no es posible o no es un mejor enfoque para hacerlo.

Si desea eliminar la declaración de cambio del código, la primera pregunta que debe hacerse es dónde tiene sentido eliminar la declaración de cambio y utilizar alguna otra técnica. Lamentablemente, la respuesta a esta pregunta es específica del dominio.

Y recuerde que los compiladores pueden hacer varias optimizaciones para cambiar las declaraciones. Entonces, por ejemplo, si desea procesar el mensaje de manera eficiente, una declaración de cambio es el camino a seguir. Pero, por otro lado, ejecutar reglas de negocios basadas en una declaración de cambio probablemente no sea la mejor manera de hacerlo y la aplicación debe ser reorganizada.

Aquí hay algunas alternativas para cambiar la declaración:


1
¿Podría alguien dar una comparación del procesamiento de mensajes usando el interruptor versus otras alternativas?
Mike A

37

El cambio en sí mismo no es tan malo, pero si tiene muchos "cambios" o "si / más" en los objetos en sus métodos, puede ser una señal de que su diseño es un poco "procesal" y que sus objetos son solo un valor cubos Mueva la lógica a sus objetos, invoque un método en sus objetos y deje que ellos decidan cómo responder.


Suponiendo, por supuesto, que no está escribiendo en C. :)
Bernard

1
en C, puede (ab?) usar punteros de función y estructuras para construir algo parecido a un objeto;)
Tetha

Puede escribir FORT ^ H ^ H ^ H ^ H Java en cualquier idioma. ; p
Bernard

Totalmente de acuerdo: el cambio es una excelente manera de reducir las líneas de código, pero no juegues con él demasiado mach.
HotJard

21

Creo que la mejor manera es usar un buen mapa. Usando un diccionario puede asignar casi cualquier entrada a algún otro valor / objeto / función.

su código se vería algo (psuedo) como este:

void InitMap(){
    Map[key1] = Object/Action;
    Map[key2] = Object/Action;
}

Object/Action DoStuff(Object key){
    return Map[key];
}

44
Depende del idioma. Puede ser mucho menos legible que un interruptor
Vinko Vrsalovic

Esta es una buena solución elegent en la situación correcta. Hice este código clave de mapeo recientemente, y parecía leer bien para ese propósito.
Bernard

Esto es cierto, probablemente no usaría esto para nada simple, pero proporciona cierta flexibilidad en términos de configuración sobre una declaración de interruptor. Se puede preparar un diccionario sobre la marcha, mientras que un interruptor siempre estará codificado.
Josh

Puede estar más limpio en algunas circunstancias. También más lento, ya que requiere llamadas a funciones.
Nick Johnson

1
Eso depende de tus llaves. Un compilador puede compilar una declaración de cambio en una búsqueda simple o en una búsqueda binaria estática extremadamente rápida, en cualquier caso sin requerir ninguna llamada a la función.
Nick Johnson

12

Todos aman los ENORMES if elsebloques. ¡Tan fácil de leer! Sin embargo, tengo curiosidad por saber por qué querría eliminar las declaraciones de cambio. Si necesita una declaración de cambio, probablemente necesite una declaración de cambio. En serio, diría que depende de lo que esté haciendo el código. Si todo lo que está haciendo el conmutador es llamar a funciones (por ejemplo), podría pasar punteros de función. Si es una mejor solución es discutible.

El lenguaje es un factor importante aquí también, creo.


13
Supongo que eso fue sarcasmo :)
Craig Day

6

Creo que lo que estás buscando es el Patrón de estrategia.

Esto se puede implementar de varias maneras, que se han mencionado en otras respuestas a esta pregunta, como:

  • Un mapa de valores -> funciones
  • Polimorfismo. (el subtipo de un objeto decidirá cómo maneja un proceso específico).
  • Funciones de primera clase.

5

switch sería bueno reemplazar las declaraciones si se encuentra agregando nuevos estados o un nuevo comportamiento a las declaraciones:

int estado;

String getString () {
   interruptor (estado) {
     caso 0: // comportamiento para el estado 0
           devuelve "cero";
     caso 1: // comportamiento para el estado 1
           devuelve "uno";
   }
   lanzar nueva IllegalStateException ();
}

double getDouble () {

   switch (this.state) {
     caso 0: // comportamiento para el estado 0
           devuelve 0d;
     caso 1: // comportamiento para el estado 1
           volver 1d;
   }
   lanzar nueva IllegalStateException ();
}

Agregar un nuevo comportamiento requiere copiar el switch, y agregar nuevos estados significa agregar otro casea cada switch declaración.

En Java, solo puede cambiar un número muy limitado de tipos primitivos cuyos valores conoce en tiempo de ejecución. Esto presenta un problema en sí mismo: los estados se representan como números o caracteres mágicos.

La coincidencia de patrones y múltiples if - elsebloques se pueden usar, aunque realmente tienen los mismos problemas al agregar nuevos comportamientos y nuevos estados.

La solución que otros han sugerido como "polimorfismo" es una instancia del patrón de Estado :

Reemplace cada uno de los estados con su propia clase. Cada comportamiento tiene su propio método en la clase:

Estado del estado;

String getString () {
   return state.getString ();
}

double getDouble () {
   return state.getDouble ();
}

Cada vez que agrega un nuevo estado, debe agregar una nueva implementación de la IStateinterfaz. En un switchmundo, estarías agregando un casea cada uno switch.

Cada vez que agrega un nuevo comportamiento, debe agregar un nuevo método a la IStateinterfaz y a cada una de las implementaciones. Esta es la misma carga que antes, aunque ahora el compilador verificará que tenga implementaciones del nuevo comportamiento en cada estado preexistente.

Otros ya han dicho que esto puede ser demasiado pesado, por lo que, por supuesto, hay un punto al que se llega donde se mueve de uno a otro. Personalmente, la segunda vez que escribo un interruptor es el punto en el que refactorizo.


4

si más

Sin embargo, rechazo la premisa de que el cambio es inherentemente malo.


3

Bueno, por un lado, no sabía que usar el interruptor era un anti patrón.

En segundo lugar, el interruptor siempre se puede reemplazar con declaraciones if / else if.


exactamente: el interruptor es solo metadona sintáctica para un montón de if / elsifs.
Mike A

3

¿Por qué quieres? En manos de un buen compilador, una declaración de cambio puede ser mucho más eficiente que los bloques if / else (además de ser más fácil de leer), y es probable que solo los cambios más grandes se aceleren si se reemplazan por algún tipo de la estructura de datos de búsqueda indirecta.


2
En ese momento, está adivinando el compilador y realizando cambios en su diseño en función de los componentes internos del compilador. El diseño debe seguir la naturaleza del problema, no la naturaleza del compilador.
Mike A

3

'cambiar' es solo una construcción de lenguaje y todas las construcciones de lenguaje pueden considerarse como herramientas para hacer un trabajo. Al igual que con las herramientas reales, algunas herramientas se adaptan mejor a una tarea que a otra (no usaría un martillo para colocar un gancho de imagen). La parte importante es cómo se define 'hacer el trabajo'. ¿Necesita ser mantenible, necesita ser rápido, necesita escalar, necesita ser extensible y así sucesivamente?

En cada punto del proceso de programación, generalmente hay una gama de construcciones y patrones que se pueden usar: un interruptor, una secuencia if-else-if, funciones virtuales, tablas de salto, mapas con punteros de función, etc. Con experiencia, un programador sabrá instintivamente la herramienta adecuada para usar en una situación dada.

Se debe suponer que cualquier persona que mantenga o revise el código es al menos tan hábil como el autor original para que cualquier construcción pueda usarse de manera segura.


Bien, pero ¿por qué necesitamos 5 formas diferentes y redundantes de hacer lo mismo: ejecución condicional?
Mike A

@ mike.amy: Porque cada método tiene diferentes beneficios y costos y se trata de obtener el mayor beneficio con el menor costo.
Skizz el

1

Si el interruptor está ahí para distinguir entre varios tipos de objetos, probablemente te faltan algunas clases para describir con precisión esos objetos, o algunos métodos virtuales ...


1

Para C ++

Si se refiere, es decir, a un AbstractFactory, creo que un método registerCreatorFunc (..) generalmente es mejor que requerir agregar un caso para cada una de las declaraciones "nuevas" que se necesitan. Luego, dejar que todas las clases creen y registren una función creator (..) que se puede implementar fácilmente con una macro (si me atrevo a mencionar). Creo que este es un enfoque común que hacen muchos marcos. Lo vi por primera vez en ET ++ y creo que muchos marcos que requieren una macro DECL e IMPL lo usan.


1

En un lenguaje de procedimiento, como C, el cambio será mejor que cualquiera de las alternativas.

En un lenguaje orientado a objetos, casi siempre hay otras alternativas disponibles que utilizan mejor la estructura del objeto, particularmente el polimorfismo.

El problema con las declaraciones de cambio surge cuando se producen varios bloques de cambio muy similares en varios lugares de la aplicación, y se necesita agregar soporte para un nuevo valor. Es bastante común que un desarrollador olvide agregar soporte para el nuevo valor a uno de los bloques de interruptores dispersos por la aplicación.

Con el polimorfismo, una nueva clase reemplaza el nuevo valor y el nuevo comportamiento se agrega como parte de la adición de la nueva clase. El comportamiento en estos puntos de cambio se hereda de la superclase, se anula para proporcionar un nuevo comportamiento o se implementa para evitar un error del compilador cuando el supermétodo es abstracto.

Donde no esté ocurriendo un polimorfismo obvio, puede valer la pena implementar el patrón de Estrategia .

Pero si tu alternativa es un gran bloque SI ... ENTONCES ... OTRO, entonces olvídalo.


1

Use un lenguaje que no venga con una declaración de cambio incorporada. Perl 5 viene a la mente.

En serio, ¿por qué querrías evitarlo? Y si tiene buenas razones para evitarlo, ¿por qué no simplemente evitarlo entonces?


1

Los punteros de función son una forma de reemplazar una declaración de interruptor enorme y gruesa, son especialmente buenos en lenguajes donde puede capturar funciones por sus nombres y hacer cosas con ellas.

Por supuesto, no debe forzar las declaraciones de cambio fuera de su código, y siempre existe la posibilidad de que lo esté haciendo todo mal, lo que resulta en estúpidos trozos de código redundantes. (Esto es inevitable a veces, pero un buen lenguaje debería permitirle eliminar la redundancia mientras se mantiene limpio).

Este es un gran ejemplo de divide y vencerás:

Digamos que tiene un intérprete de algún tipo.

switch(*IP) {
    case OPCODE_ADD:
        ...
        break;
    case OPCODE_NOT_ZERO:
        ...
        break;
    case OPCODE_JUMP:
        ...
        break;
    default:
        fixme(*IP);
}

En cambio, puedes usar esto:

opcode_table[*IP](*IP, vm);

... // in somewhere else:
void opcode_add(byte_opcode op, Vm* vm) { ... };
void opcode_not_zero(byte_opcode op, Vm* vm) { ... };
void opcode_jump(byte_opcode op, Vm* vm) { ... };
void opcode_default(byte_opcode op, Vm* vm) { /* fixme */ };

OpcodeFuncPtr opcode_table[256] = {
    ...
    opcode_add,
    opcode_not_zero,
    opcode_jump,
    opcode_default,
    opcode_default,
    ... // etc.
};

Tenga en cuenta que no sé cómo eliminar la redundancia de opcode_table en C. Quizás debería hacer una pregunta al respecto. :)


0

La respuesta más obvia, independiente del lenguaje, es usar una serie de 'si'.

Si el lenguaje que está utilizando tiene punteros de función (C) o tiene funciones que son valores de primera clase (Lua), puede obtener resultados similares a un "interruptor" usando una matriz (o una lista) de funciones (punteros a).

Debería ser más específico en el idioma si desea mejores respuestas.


0

Las declaraciones de cambio a menudo se pueden reemplazar por un buen diseño OO.

Por ejemplo, tiene una clase de cuenta y está utilizando una declaración de cambio para realizar un cálculo diferente según el tipo de cuenta.

Sugeriría que esto se reemplace por una serie de clases de cuenta, que representan los diferentes tipos de cuenta, y que todas implementen una interfaz de Cuenta.

El cambio se vuelve innecesario, ya que puede tratar todos los tipos de cuentas de la misma manera y, gracias al polimorfismo, se ejecutará el cálculo apropiado para el tipo de cuenta.


0

¡Depende de por qué quieras reemplazarlo!

Muchos intérpretes usan 'gotos computados' en lugar de declaraciones de cambio para la ejecución del código de operación.

Lo que extraño sobre el interruptor C / C ++ es el Pascal 'in' y los rangos. También desearía poder encender las cuerdas. Pero estos, aunque triviales para un compilador para comer, son un trabajo duro cuando se hace usando estructuras e iteradores y cosas. Entonces, por el contrario, hay muchas cosas que desearía poder reemplazar con un interruptor, ¡si solo el interruptor de C () fuera más flexible!


0

El cambio no es una buena manera de hacerlo, ya que rompe el Open Close Principal. Así es como lo hago.

public class Animal
{
       public abstract void Speak();
}


public class Dog : Animal
{
   public virtual void Speak()
   {
       Console.WriteLine("Hao Hao");
   }
}

public class Cat : Animal
{
   public virtual void Speak()
   {
       Console.WriteLine("Meauuuu");
   }
}

Y aquí está cómo usarlo (tomando su código):

foreach (var animal in zoo) 
{
    echo animal.speak();
}

Básicamente, lo que estamos haciendo es delegar la responsabilidad a la clase infantil en lugar de que los padres decidan qué hacer con los niños.

Es posible que también desee leer sobre el "Principio de sustitución de Liskov".


0

En JavaScript usando la matriz asociativa:
esto:

function getItemPricing(customer, item) {
    switch (customer.type) {
        // VIPs are awesome. Give them 50% off.
        case 'VIP':
            return item.price * item.quantity * 0.50;

            // Preferred customers are no VIPs, but they still get 25% off.
        case 'Preferred':
            return item.price * item.quantity * 0.75;

            // No discount for other customers.
        case 'Regular':
        case
        default:
            return item.price * item.quantity;
    }
}

se convierte en esto:

function getItemPricing(customer, item) {
var pricing = {
    'VIP': function(item) {
        return item.price * item.quantity * 0.50;
    },
    'Preferred': function(item) {
        if (item.price <= 100.0)
            return item.price * item.quantity * 0.75;

        // Else
        return item.price * item.quantity;
    },
    'Regular': function(item) {
        return item.price * item.quantity;
    }
};

    if (pricing[customer.type])
        return pricing[customer.type](item);
    else
        return pricing.Regular(item);
}

Cortesía


-12

Otro voto para si / si no. No soy un gran admirador de las declaraciones de casos o cambios porque hay algunas personas que no las usan. El código es menos legible si usa mayúsculas o minúsculas. Quizás no sea menos legible para usted, sino para aquellos que nunca han necesitado usar el comando.

Lo mismo ocurre con las fábricas de objetos.

Los bloques If / else son una construcción simple que todos obtienen. Hay algunas cosas que puede hacer para asegurarse de no causar problemas.

En primer lugar, no intente sangrar las declaraciones más de un par de veces. Si te encuentras sangrando, lo estás haciendo mal.

 if a = 1 then 
     do something else 
     if a = 2 then 
         do something else
     else 
         if a = 3 then 
             do the last thing
         endif
     endif 
  endif

Es realmente malo, haz esto en su lugar.

if a = 1 then 
   do something
endif 
if a = 2 then 
   do something else
endif 
if a = 3 then 
   do something more
endif 

La optimización sea condenada. No hace mucha diferencia en la velocidad de su código.

En segundo lugar, no soy reacio a salir de un Bloque If siempre que haya suficientes declaraciones de interrupción dispersas a través del bloque de código particular para que sea obvio

procedure processA(a:int)
    if a = 1 then 
       do something
       procedure_return
    endif 
    if a = 2 then 
       do something else
       procedure_return
    endif 
    if a = 3 then 
       do something more
       procedure_return
    endif 
end_procedure

EDITAR : en Switch y por qué creo que es difícil asimilar:

Aquí hay un ejemplo de una declaración de cambio ...

private void doLog(LogLevel logLevel, String msg) {
   String prefix;
   switch (logLevel) {
     case INFO:
       prefix = "INFO";
       break;
     case WARN:
       prefix = "WARN";
       break;
     case ERROR:
       prefix = "ERROR";
       break;
     default:
       throw new RuntimeException("Oops, forgot to add stuff on new enum constant");
   }
   System.out.println(String.format("%s: %s", prefix, msg));
 }

Para mí, el problema aquí es que las estructuras de control normales que se aplican en lenguajes similares a C se han roto por completo. Existe una regla general de que si desea colocar más de una línea de código dentro de una estructura de control, use llaves o una declaración de inicio / fin.

p.ej

for i from 1 to 1000 {statement1; statement2}
if something=false then {statement1; statement2}
while isOKtoLoop {statement1; statement2}

Para mí (y puede corregirme si me equivoco), la declaración de Caso arroja esta regla por la ventana. Un bloque de código ejecutado condicionalmente no se coloca dentro de una estructura de inicio / fin. Debido a esto, creo que Case es conceptualmente diferente para no ser utilizado.

Espero que eso conteste tus preguntas.


Wow, obviamente una respuesta contenciosa. Me interesaría saber en qué me equivoqué.
seanyboy

Er, ¿cambiar como demasiado complejo? No sé ... me parece que no quedarían muchas características de lenguaje que podrías usar. :) Además, en su ejemplo central, ¿no sería más inteligente hacerlo si (a == 1 || a == 2 || a == 3) hace algo?
Lars Westergren

Además, en su último ejemplo, "break" no hará nada en la mayoría de los idiomas, que se separa del bloque más cercano (generalmente un bucle), que en su caso ocurre de todos modos en la siguiente línea (endif). Si usa un idioma donde el descanso es "retorno", muchos retornos también están mal vistos (excepto por "declaraciones de guardia")
Lars Westergren

44
"La optimización está condenada. No hace mucha diferencia en la velocidad de su código". Mi código se ejecuta en una plataforma móvil. Las optimizaciones importan. Además, los interruptores pueden hacer que el código se vea MUY limpio (en comparación con if..elseif..elseif..elseif ...) si se usa correctamente. ¿Nunca los has visto? Aprendelos.
Swati

El cambio no es complicado en absoluto, pero siempre he tratado de minimizar la cantidad de construcciones utilizadas en el código para reducir la fricción en la comprensión.
seanyboy
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.