¿Dividir una aplicación potencialmente monolítica en varias más pequeñas ayuda a prevenir errores? [cerrado]


48

Otra forma de preguntar esto es; ¿Por qué los programas tienden a ser monolíticos?

Estoy pensando en algo como un paquete de animación como Maya, que la gente usa para varios flujos de trabajo diferentes.

Si las capacidades de animación y modelado se dividieran en su propia aplicación separada y se desarrollaran por separado, con archivos que se pasan entre ellos, ¿no serían más fáciles de mantener?


99
If the animation and modelling capabilities were split into their own separate application and developed separately, with files being passed between them, would they not be easier to maintain?No mezcle más fácil de extender con un módulo más fácil de mantener , por sí mismo, no está libre de complicaciones o diseños dudosos. Maya puede ser el infierno en la tierra para mantener, mientras que sus complementos no lo son. O viceversa.
Laiv

37
Agregaré que un solo programa monolítico tiende a ser más fácil de vender y más fácil de usar para la mayoría de las personas .
DarthFennec

2
@DarthFennec Las mejores aplicaciones se ven como una aplicación para el usuario pero utilizan lo que sea necesario bajo el capó. ¿Cuántos microservicios alimentan los diversos sitios web que visita? ¡Casi ninguno de ellos ya son monolitos!
corsiKa

23
@corsiKa Por lo general, no hay nada que ganar escribiendo una aplicación de escritorio como múltiples programas que se comunican bajo el capó, eso no se obtiene simplemente escribiendo múltiples módulos / bibliotecas y vinculándolos en un binario monolítico. Los microservicios tienen un propósito completamente diferente, ya que permiten que una sola aplicación se ejecute en varios servidores físicos, lo que permite que el rendimiento se amplíe con la carga.
DarthFennec

55
@corsiKa: supongo que una cantidad abrumadora de sitios web que uso siguen siendo monolitos. La mayor parte de Internet, después de todo, se ejecuta en Wordpress.
Davor Ždralo

Respuestas:


94

Si. En general, dos aplicaciones más pequeñas y menos complejas son mucho más fáciles de mantener que una sola grande.

Sin embargo, obtienes un nuevo tipo de error cuando todas las aplicaciones trabajan juntas para lograr un objetivo. Para que trabajen juntos, tienen que intercambiar mensajes y esta orquestación puede salir mal de varias maneras, aunque cada aplicación pueda funcionar perfectamente. Tener un millón de pequeñas aplicaciones tiene sus propios problemas especiales.

Una aplicación monolítica es realmente la opción predeterminada con la que termina cuando agrega más y más funciones a una sola aplicación. Es el enfoque más fácil cuando considera cada característica por sí sola. Solo una vez que ha crecido puede ver el conjunto y decir "sabes qué, esto funcionaría mejor si separáramos X e Y".


66
Sí, y también hay consideraciones de rendimiento, por ejemplo, el costo de pasar un puntero frente a la serialización de datos.
JimmyJames

65
"En general, 2 aplicaciones más pequeñas y menos complejas son mucho más fáciles de mantener que una sola grande". - Eso es cierto, excepto cuando no lo es. Depende en gran medida de dónde y cómo esas dos aplicaciones tienen que interactuar entre sí.
Doc Brown

10
"En general, 2 aplicaciones más pequeñas y menos complejas son mucho más fáciles de mantener que una sola grande". Creo que querré más explicaciones para eso. ¿Por qué exactamente el proceso de generar dos ejecutables en lugar de uno a partir de una base de código mágicamente facilitaría el código? Lo que decide qué tan fácil es razonar el código, es qué tan estrechamente acoplado está y cosas similares. Pero esa es una separación lógica y no tiene nada que ver con la física .
Voo

11
@Ew La separación física no fuerza una separación lógica, ese es el problema. Puedo diseñar fácilmente un sistema en el que dos aplicaciones separadas estén estrechamente unidas. Claro que hay alguna correlación involucrada aquí, ya que las personas que pasan el tiempo para separar una aplicación son lo suficientemente competentes como para considerar estas cosas, pero hay pocas razones para asumir cualquier causa . Por la misma lógica, puedo afirmar que usar la última versión de C # hace que el código sea mucho más fácil de mantener, ya que el tipo de equipo que se mantiene actualizado con sus herramientas probablemente también se preocupe por el mantenimiento del código.
Voo

99
Creo que la discusión aquí se puede resumir con 2 declaraciones: 1) Dividir una aplicación en sí misma no hace que una aplicación sea más fácil de mantener; por el contrario, proporciona otro posible punto de falla 2) Dividir una aplicación te obliga a pensar dónde dividir eso, que proporciona una ventaja en comparación con un monolito donde eso nunca se ha hecho.
R. Schmitz

51

¿Dividir una aplicación potencialmente monolítica en varias más pequeñas ayuda a prevenir errores?

Las cosas rara vez son tan simples en la realidad.

Separarse definitivamente no ayuda a prevenir esos errores en primer lugar. A veces puede ayudar encontrar errores más rápido. Una aplicación que consta de componentes pequeños y aislados puede permitir pruebas más individuales (tipo de "unidad") para esos componentes, lo que puede hacer que a veces sea más fácil detectar la causa raíz de ciertos errores, y así permitir que los repare más rápido.

Sin embargo,

  • Incluso una aplicación que parece ser monolítica desde el exterior puede consistir en un montón de componentes comprobables por unidad en el interior, por lo que las pruebas unitarias no son necesariamente más difíciles para una aplicación monolítica.

  • Como ya mencionó Ewan, la interacción de varios componentes introduce riesgos y errores adicionales. Y depurar un sistema de aplicación con comunicación compleja entre procesos puede ser significativamente más difícil que depurar una aplicación de proceso único

Esto también depende en gran medida de qué tan bien se puede dividir una aplicación más grande en componentes, y qué tan amplias son las interfaces entre los componentes y cómo se usan esas interfaces.

En resumen, esto es a menudo una compensación, y nada donde una respuesta de "sí" o "no" es correcta en general.

¿Por qué los programas tienden a ser monolíticos?

¿Ellos? Mire a su alrededor, hay miles de millones de aplicaciones web en el mundo que no me parecen muy monolíticas, todo lo contrario. También hay muchos programas disponibles que proporcionan un modelo de complemento (AFAIK, incluso el software Maya que mencionó lo hace).

no serían más fáciles de mantener

El "mantenimiento más fácil" aquí a menudo proviene del hecho de que diferentes partes de una aplicación pueden ser desarrolladas más fácilmente por diferentes equipos, por lo que una carga de trabajo mejor distribuida, equipos especializados con un enfoque más claro y más.


44
En su última oración, la ley de Conway dice que la estructura del sistema tiende a imitar org. estructura: los desarrolladores / equipos están más familiarizados con algunas partes que con otras, por lo que si bien las correcciones / mejoras deberían ocurrir en la parte más relevante, puede ser más fácil para un desarrollador piratearlas en "sus" partes en lugar de (a) aprender cómo la otra parte funciona o (b) trabaja con alguien más familiarizado con esa parte. Esto está relacionado con las "costuras" que menciona @TKK, y lo difícil que puede ser encontrar y hacer cumplir las "correctas" / simples.
Warbo

38

Tendré que estar en desacuerdo con la mayoría en este caso. Dividir una aplicación en dos por separado no hace que el código sea más fácil de mantener o razonar.

Separar el código en dos ejecutables solo cambia la estructura física del código, pero eso no es lo importante. Lo que decide qué tan compleja es una aplicación es cuán estrechamente acopladas están las diferentes partes que la componen. Esta no es una propiedad física, sino lógica .

Puede tener una aplicación monolítica que tenga una separación clara de diferentes preocupaciones e interfaces simples. Puede tener una arquitectura de microservicio que se base en los detalles de implementación de otros microservicios y esté estrechamente relacionada con todos los demás.

Lo cierto es que el proceso de cómo dividir una aplicación grande en otras más pequeñas es muy útil cuando se intenta establecer interfaces y requisitos claros para cada parte. En DDD hablar, eso vendría con sus contextos limitados. Pero si crea muchas aplicaciones pequeñas o una grande que tenga la misma estructura lógica es más una decisión técnica.


Pero, ¿qué pasa si uno toma una aplicación de escritorio con múltiples modos de edición y en su lugar solo crea una aplicación de escritorio para cada modo que un usuario abriría individualmente en lugar de tener una interfaz? ¿No eliminaría eso una cantidad no trivial de código dedicado a producir la "característica" de "el usuario puede cambiar entre modos de edición"?
El gran pato

3
@TheGreatDuck Eso parece que también eliminaría una cantidad no trivial de usuarios a los que no les gusta tener que cambiar entre diferentes aplicaciones. ;) Pero sí, la eliminación de características generalmente conducirá a un código más simple. Elimine el corrector ortográfico y eliminará la posibilidad de tener errores de corrección ortográfica. Raramente se hace porque la función se agregó porque alguien la quería.
Odalrick

1
@TheGreatDuck Seguramente el diseño de UX debe venir antes de cualquier decisión arquitectónica. No tiene sentido tener la arquitectura mejor diseñada si nadie usa su programa. Primero decida lo que desea construir y en función de los detalles técnicos. Si se prefieren dos aplicaciones separadas, hágalo. Sin embargo, aún puede compartir mucho código a través de bibliotecas compartidas.
Voo

¿Es realmente cierto decir que la complejidad del sistema se debe al acoplamiento estrecho de las partes? Quisiera decir que la complejidad total aumenta si particiona su sistema a medida que introduce la indirección y la comunicación, aunque la complejidad de los componentes individuales específicos está aislada en un estado acotado de complejidad más limitada.
Alex

1
@TheGreatDuck La suposición subyacente aquí era que los sistemas tienen algo en común y en realidad tienen que comunicarse de una forma u otra entre sí. No creo que el OP preguntara si dos aplicaciones completamente diferentes que se agrupan por alguna extraña razón serían más fáciles de mantener si se separan. Parece un caso marginal extraño que no aparece a menudo en la práctica (aunque estoy seguro de que alguien en algún lugar lo ha hecho).
Voo

15

Más fácil de mantener una vez que haya terminado de dividirlos, sí. Pero dividirlos no siempre es fácil. Intentar dividir una parte de un programa en una biblioteca reutilizable revela dónde los desarrolladores originales no pudieron pensar dónde deberían estar las costuras . Si una parte de la aplicación está llegando a otra parte de la aplicación, puede ser difícil de solucionar. Rasgar las costuras te obliga a definir las API internas más claramente, y esto es lo que finalmente hace que la base del código sea más fácil de mantener. La reutilización y el mantenimiento son productos de costuras bien definidas.


buena publicación. Creo que un ejemplo clásico / canónico de lo que hablas es una aplicación GUI. muchas veces una aplicación GUI es un programa y el backend / frontend están estrechamente acoplados. A medida que pasa el tiempo, surgen problemas ... como si alguien más necesita usar el backend pero no puede porque está vinculado a la interfaz. o el procesamiento del backend lleva demasiado tiempo y bloquea el frontend. a menudo, la gran aplicación de la GUI se divide en dos programas: uno es el GUI de la interfaz y otro es un back-end.
Trevor Boyd Smith

13

Es importante recordar que la correlación no es causalidad.

Construir un monolito grande y luego dividirlo en varias partes pequeñas puede o no conducir a un buen diseño. ( Puede mejorar el diseño, pero no está garantizado).

Pero un buen diseño a menudo conduce a la construcción de un sistema como varias partes pequeñas en lugar de un gran monolito. (Un monolito puede ser el mejor diseño, es mucho menos probable que lo sea).

¿Por qué son mejores las piezas pequeñas? Porque son más fáciles de razonar. Y si es fácil razonar sobre la corrección, es más probable que obtenga un resultado correcto.

Para citar CAR Hoare:

Hay dos formas de construir un diseño de software: una es hacerla tan simple que obviamente no haya deficiencias, y la otra es hacerla tan complicada que no haya deficiencias obvias .

Si ese es el caso, ¿por qué alguien construiría una solución innecesariamente complicada o monolítica? Hoare proporciona la respuesta en la siguiente oración:

El primer método es mucho más difícil.

Y más tarde en la misma fuente (la Conferencia del Premio Turing de 1980):

El precio de la fiabilidad es la búsqueda de la máxima simplicidad. Es un precio que los muy ricos encuentran más difícil de pagar.


6

Esta no es una pregunta con un sí o un no. La pregunta no es solo la facilidad de mantenimiento, sino también el uso eficiente de las habilidades.

En general, una aplicación monolítica bien escrita es eficiente. La comunicación entre procesos y entre dispositivos no es barata. Romper un solo proceso disminuye la eficiencia. Sin embargo, ejecutar todo en un solo procesador puede sobrecargar el procesador y disminuir el rendimiento. Este es el problema básico de escalabilidad. Cuando la red entra en escena, el problema se vuelve más complicado.

Una aplicación monolítica bien escrita que puede operar eficientemente como un solo proceso en un solo servidor puede ser fácil de mantener y estar libre de defectos, pero aún así no puede ser un uso eficiente de las habilidades de codificación y arquitectura. El primer paso es dividir el proceso en bibliotecas que todavía se ejecutan como el mismo proceso, pero que están codificadas de forma independiente, siguiendo disciplinas de cohesión y acoplamiento flexible. Un buen trabajo en este nivel mejora la capacidad de mantenimiento y rara vez afecta el rendimiento.

La siguiente etapa es dividir el monolito en procesos separados. Esto es más difícil porque entras en territorio complicado. Es fácil introducir errores de condición de carrera. La sobrecarga de la comunicación aumenta y debe tener cuidado con las "interfaces comunicativas". Las recompensas son excelentes porque rompe una barrera de escalabilidad, pero también aumenta el potencial de defectos. Las aplicaciones multiproceso son más fáciles de mantener a nivel de módulo, pero el sistema general es más complicado y más difícil de solucionar. Las soluciones pueden ser endiabladamente complicadas.

Cuando los procesos se distribuyen a servidores separados oa una implementación de estilo de nube, los problemas se vuelven más difíciles y las recompensas son mayores. La escalabilidad se dispara. (Si está considerando una implementación en la nube que no ofrece escalabilidad, piense bien). Pero los problemas que entran en esta etapa pueden ser increíblemente difíciles de identificar y pensar.


4

No se . No hace que sea más fácil de mantener. Si algo bienvenido a más problemas.

¿Por qué?

  • Los programas no son ortogonales, necesitan preservar el trabajo de los demás en la medida en que sea razonable, lo que implica un entendimiento común.
  • Gran parte del código de ambos programas es idéntico. ¿Mantiene una biblioteca compartida común o mantiene dos copias separadas?
  • Ahora tiene dos equipos de desarrollo. ¿Cómo se están comunicando?
  • Ahora tiene dos productos que necesitan:

    • un estilo de interfaz de usuario común, mecanismos de interacción, etc. Así que ahora tiene problemas de diseño. (¿Cómo se están comunicando nuevamente los equipos de desarrollo?)
    • compatibilidad con versiones anteriores (¿se puede importar modeller v1 en animator v3?)
    • La integración de la nube / red (si es una característica) ahora debe actualizarse en el doble de productos.
  • Ahora tiene tres mercados de consumo: modeladores, animadores y animadores de modelistas.

    • Tendrán prioridades en conflicto
    • Tendrán necesidades de apoyo conflictivas
    • Tendrán estilos de uso conflictivos
  • ¿Los animadores Modeller tienen que abrir dos aplicaciones separadas para trabajar en el mismo archivo? ¿Hay una tercera aplicación con ambas funciones, una aplicación carga las funciones de la otra?
  • etc ...

Dicho esto, las bases de código más pequeñas son más fáciles de mantener a nivel de aplicación, simplemente no obtendrá un almuerzo gratis. Este es el mismo problema en el corazón de Micro-Service / Any-Modular-Architecture. No es una panacea, la dificultad de mantenimiento a nivel de aplicación se cambia por dificultades de mantenimiento a nivel de orquestación. Esos problemas siguen siendo problemas, simplemente ya no están en la base del código, deberán evitarse o resolverse.

Si resolver el problema a nivel de orquestación es más sencillo que resolverlo en cada nivel de aplicación, entonces tiene sentido dividirlo en dos bases de código y tratar los problemas de orquestación.

De lo contrario, no, simplemente no lo hagas, sería mejor para ti mejorar la modularidad interna de la aplicación en sí. Inserte secciones de código en bibliotecas coherentes y más fáciles de mantener para las que la aplicación actúa como complemento. Después de todo, un monolito es solo la capa de orquestación de un paisaje de biblioteca.


3

Hubo muchas buenas respuestas, pero como hay una división casi mortal, también arrojaré mi sombrero al ring.

En mi experiencia como ingeniero de software, he encontrado que esto no es un problema simple. Realmente depende del tamaño , la escala y el propósito de la aplicación. Las aplicaciones más antiguas en virtud de la inercia requerida para cambiarlas, generalmente son monolíticas, ya que fue una práctica común durante mucho tiempo (Maya calificaría en esta categoría). Supongo que estás hablando de nuevas aplicaciones en general.

En aplicaciones lo suficientemente pequeñas que son más o menos simples, la sobrecarga requerida para mantener muchas partes separadas generalmente excede la utilidad de tener la separación. Si puede ser mantenido por una persona, probablemente se puede hacer monolítico sin causar demasiados problemas. La excepción a esta regla es cuando tiene muchas partes diferentes (un frontend, backend, quizás algunas capas de datos intermedias) que están convenientemente separadas (lógicamente).

En aplicaciones muy grandes, incluso individuales, dividirlo tiene sentido en mi experiencia. Tiene la ventaja de reducir un subconjunto de la clase de errores posibles a cambio de otros errores (a veces más fáciles de resolver). En general, también puede tener equipos de personas trabajando de forma aislada, lo que mejora la productividad. Sin embargo, muchas aplicaciones en estos días se dividen con bastante precisión, a veces en detrimento propio. También he estado en equipos donde la aplicación se dividió en tantos microservicios innecesariamente que introdujo una sobrecarga cuando las cosas dejan de hablar entre sí. Además, tener que tener todo el conocimiento de cómo cada parte habla con las otras partes se vuelve mucho más difícil con cada división sucesiva. Hay un equilibrio, y como puede ver por las respuestas aquí, la forma de hacerlo no está muy clara,


2
Mi primer trabajo como programador fue como programador de errores de milenio. El software en el que estaba trabajando se dividió en cientos de pequeños programas que hicieron una pequeña parte, unidos con archivos por lotes y usando archivos para comunicar el estado. Fue un gran desastre, inventado en una época en que las computadoras eran lentas, tenían poca memoria y el almacenamiento era costoso. Cuando trabajé con él, el código ya tenía entre 10 y 15 años. Una vez que terminamos, me pidieron mi consejo y mi consejo fue convertir todo a una nueva aplicación monolítica. Lo hicieron y un año después recibí un gran agradecimiento.
Pieter B

@PieterB He tenido una experiencia similar. Desafortunadamente, la tecnología "de vanguardia" es un culto de carga muy grande en muchos sentidos. En lugar de elegir el mejor método para el trabajo, muchas compañías simplemente seguirán cualquier cosa que haga un FAANG en ese momento sin ninguna pregunta.
CL40

y también: lo que puede aparecer como una aplicación monolítica una vez compilada, puede ser una aplicación muy modular, en cuanto al código.
Pieter B

1

Para las aplicaciones de IU es poco probable que disminuya la cantidad general de errores, pero cambiará el equilibrio de la mezcla de errores hacia los problemas causados ​​por la comunicación.

Hablando de aplicaciones / sitios de IU que enfrentan los usuarios, los usuarios no son pacientes y exigen un tiempo de respuesta bajo. Esto hace que cualquier retraso en la comunicación se convierta en errores. Como resultado, se cambiará la posible disminución de errores debido a la menor complejidad de un solo componente con errores muy difíciles y el requisito de tiempo de comunicación entre procesos y máquinas.

Si las unidades de datos con las que se ocupa el programa son grandes (es decir, imágenes), cualquier retraso entre procesos sería más largo y más difícil de eliminar; algo como "aplicar transformación a imagen de 10 mb" ganará instantáneamente + 20 mb de disco / red IO además a 2 conversión de formato en memoria a formato serializado y viceversa. Realmente no hay mucho que puedas hacer para ocultarle al usuario el tiempo necesario para hacerlo.

Además, cualquier comunicación y especialmente la E / S del disco está sujeta a las comprobaciones de antivirus / cortafuegos; esto inevitablemente agrega otra capa de errores difíciles de reproducir e incluso más demoras.

La división del "programa" monolítico brilla cuando los retrasos en la comunicación no son críticos o ya son inevitables

  • procesamiento masivo de información en paralelo donde puede intercambiar pequeños retrasos adicionales para una mejora significativa de los pasos individuales (a veces eliminando la necesidad de componentes personalizados mediante el uso inmediato de la plataforma). La pequeña huella individual de pasos puede permitirle usar varias máquinas más baratas en lugar de una sola cara, por ejemplo.
  • dividir los servicios monolíticos en microservicios menos acoplados: llamar a varios servicios en paralelo en lugar de uno probablemente no agregará demoras adicionales (incluso puede disminuir el tiempo general si cada uno es más rápido y no hay dependencias)
  • mudando operaciones que los usuarios esperan tomar mucho tiempo: renderizando escenas / películas en 3D complicadas, calculando métricas complejas sobre datos, ...
  • todo tipo de "autocompletar", "corrección ortográfica" y otras ayudas opcionales pueden y a menudo se hacen externas: el ejemplo más obvio son las sugerencias automáticas de URL del navegador donde su entrada se envía al servicio externo (motor de búsqueda) todo el tiempo .

Tenga en cuenta que esto se aplica tanto a las aplicaciones de escritorio como a los sitios web (la parte del programa orientada al usuario tiende a ser "monolítica"); todo el código de interacción del usuario vinculado a una sola pieza de datos generalmente se ejecuta en un solo proceso (no es inusual dividirlo) procesa por pieza de datos, como una página HTML o una imagen, pero es ortogonal a esta pregunta). Incluso para el sitio más básico con entrada del usuario, verá que la lógica de validación se ejecuta en el lado del cliente, incluso si hacerla en el lado del servidor sería más modular y reduciría la complejidad / duplicación de código.


0

¿Ayuda [a] a prevenir errores?

¿Evitar? Bueno, no, en realidad no.

  • Ayuda a detectar errores .
    Es decir, todos los errores que ni siquiera sabía que tenía, que solo descubrió cuando intentó dividir todo ese desastre en partes más pequeñas. Entonces, en cierto modo, evitó que esos errores aparecieran en producción, pero los errores ya estaban allí.
  • Ayuda a reducir el impacto de los errores .
    Los errores en las aplicaciones monolíticas tienen el potencial de derribar todo el sistema y evitar que el usuario interactúe con su aplicación. Si divide esa aplicación en componentes, la mayoría de los errores, por diseño, solo afectarán a uno de los componentes.
  • Crea un escenario para nuevos errores .
    Si desea mantener la experiencia del usuario igual, deberá incluir una nueva lógica para que todos esos componentes se comuniquen (a través de los servicios REST, a través de las llamadas del sistema operativo, lo que tenga) para que puedan interactuar sin problemas desde el punto de vista del usuario.
    Como un simple ejemplo: su aplicación monolítica permite a los usuarios crear un modelo y animarlo sin salir de la aplicación. Divide la aplicación en dos componentes: modelado y animación. Ahora sus usuarios tienen que exportar el modelo de la aplicación de modelado a un archivo, luego encontrar el archivo y luego abrirlo con la aplicación de animación ... Seamos sinceros, a algunos usuarios no les va a gustar eso, por lo que debe incluir una nueva lógica para el archivo aplicación de modelado para exportar el archivo yInicie automáticamente la aplicación de animación y haga que abra el archivo. Y esta nueva lógica, tan simple como puede ser, puede tener una serie de errores relacionados con la serialización de datos, el acceso a los archivos y los permisos, los usuarios que cambian la ruta de instalación de las aplicaciones, etc.
  • Es la excusa perfecta para aplicar una refactorización muy necesaria .
    Cuando decide dividir una aplicación monolítica en componentes más pequeños, (con suerte) lo hace con mucho más conocimiento y experiencia sobre el sistema que cuando se diseñó por primera vez, y gracias a eso puede aplicar una serie de refactores para hacer el código más limpio, más simple, más eficiente, más resistente, más seguro. Y esta refactorización puede, de alguna manera, ayudar a prevenir errores. Por supuesto, también puede aplicar la misma refactorización a la aplicación monolítica para evitar los mismos errores, pero no lo hace porque es tan monolítico que tiene miedo de tocar algo en la interfaz de usuario y romper la lógica empresarial ¯ \ _ (ツ) _ / ¯

Por lo tanto, no diría que está evitando errores simplemente dividiendo una aplicación monolítica en componentes más pequeños, pero de hecho está haciendo que sea más fácil llegar a un punto en el que los errores se pueden prevenir más fácilmente.

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.