“Prefiere la composición sobre la herencia”: ¿es la única razón para defenderse contra los cambios de firma?


13

Esta página aboga por la composición sobre la herencia con el siguiente argumento (reformulado en mis palabras):

Un cambio en la firma de un método de la superclase (que no se ha anulado en la subclase) provoca cambios adicionales en muchos lugares cuando usamos Herencia. Sin embargo, cuando usamos Composición, el cambio adicional requerido es solo en un solo lugar: la subclase.

¿Es esa realmente la única razón para favorecer la composición sobre la herencia? Porque si ese es el caso, este problema se puede aliviar fácilmente aplicando un estilo de codificación que aboga por anular todos los métodos de la superclase, incluso si la subclase no cambia la implementación (es decir, colocando anulaciones ficticias en la subclase). ¿Me estoy perdiendo de algo?



1
ver también: Discuta esto $ {blog}
mosquito

2
La cita no dice que es "la única" razón.
Tulains Córdova

3
La composición es casi siempre mejor, porque la herencia generalmente agrega complejidad, acoplamiento, lectura de código y reduce la flexibilidad. Pero hay excepciones notables, a veces una clase base o dos no duelen. Por eso es ' preferir ' en lugar de ' siempre '.
Mark Rogers

Respuestas:


27

Me gustan las analogías, así que aquí hay una: ¿Alguna vez has visto uno de esos televisores que tenían un reproductor de VCR incorporado? ¿Qué tal el que tiene una videograbadora y un reproductor de DVD? O uno que tenga un Blu-ray, DVD y VCR. Ah, y ahora todo el mundo está transmitiendo, por lo que necesitamos diseñar un nuevo televisor ...

Lo más probable es que si posee un televisor, no tenga uno como el anterior. Probablemente la mayoría de ellos nunca han existido. Lo más probable es que tenga un monitor o conjunto que tenga numerosas entradas para que no necesite un nuevo conjunto cada vez que haya un nuevo tipo de dispositivo de entrada.

La herencia es como el televisor con la videograbadora incorporada. Si tiene 3 tipos diferentes de comportamientos que desea usar juntos y cada uno tiene 2 opciones de implementación, necesita 8 clases diferentes para representarlas. A medida que agrega más opciones, los números explotan. Si usa composición en su lugar, evita ese problema combinatorio y el diseño tenderá a ser más extensible.


66
Había muchos televisores con reproductores de VCR y DVD integrados a fines de los 90 y principios de los 2000.
JAB

@JAB y muchos (¿la mayoría?) Televisores vendidos hoy son compatibles con la transmisión, para el caso.
Casey

44
@JAB Y ninguno de ellos puede vender su reproductor de DVD para ayudar a comprar un reproductor de Bluray
Alexander - Restablecer a Monica

1
@JAB Correcto. La declaración "¿Alguna vez has visto uno de esos televisores que tenían un reproductor de VCR incorporado?" implica que existen.
JimmyJames

44
@Casey Porque obviamente, nunca habrá otra tecnología que sustituya a esta, ¿verdad? Apuesto a que cualquiera de esos televisores también admite entradas de dispositivos externos en caso de que alguien quiera usar otra cosa sin comprar un televisor nuevo. O, más bien, pocos compradores educados están dispuestos a comprar un televisor que carece de tales entradas. Mi premisa no es que estos enfoques no existan y tampoco afirmaría que la herencia no existe. El punto es que tales enfoques son inherentemente menos flexibles que la composición.
JimmyJames

4

No, no es. En mi experiencia, la composición sobre la herencia es el resultado de pensar en su software como un grupo de objetos con comportamientos que se comunican entre sí / objetos que se prestan servicios entre sí en lugar de clases y datos .

De esta manera, tiene algunos objetos que proporcionan servicios. Los usa como bloques de construcción (realmente como Lego) para habilitar otro comportamiento (por ejemplo, un UserRegistrationService utiliza los servicios proporcionados por un EmailerService y un UserRepository ).

Al construir sistemas más grandes con este enfoque, naturalmente utilicé la herencia en muy pocos casos. Al final del día, la herencia es una herramienta muy poderosa que debe usarse con precaución y solo cuando sea apropiado.


Veo la mentalidad de Java aquí. OOP trata sobre los datos que se empaquetan junto con las funciones que actúan sobre ellos; lo que está describiendo se denomina mejor "módulos". Tienes razón en que evitar la herencia es una buena idea cuando estás reutilizando objetos como módulos, pero me parece inquietante que parezca que estás recomendando eso como la forma preferida de usar objetos.
Brilliand

1
@Brilliand Si solo supiera cuánto código java está escrito en el estilo que describe y cuánto horror es esto ... Smalltalk introdujo el término OOP y fue diseñado alrededor de objetos que reciben mensajes. : "La informática debe verse como una capacidad intrínseca de los objetos que se pueden invocar de manera uniforme mediante el envío de mensajes". No se mencionan datos y funciones que operan en él. Lo siento, pero en mi opinión estás muy equivocado. Si se tratara solo de datos y funciones, felizmente podría continuar escribiendo estructuras.
marstato

@Tsar desafortunadamente, aunque Java admite métodos estáticos, la cultura Java no lo hace
k_g

@Brilliand ¿A quién le importa de qué trata "POO"? Lo que importa es cómo puede usarlo y los resultados de usarlo de esa manera.
user253751

1
Después de la discusión con @Tsar: las clases de Java no tienen que ser instanciadas, y de hecho, las clases como UserRegistrationService y EmailService generalmente no deberían ser instanciadas: las clases sin datos asociados funcionan mejor como clases estáticas (y como tales, son irrelevantes para el original pregunta). Eliminaré algunos de mis comentarios anteriores, pero mi crítica original de la respuesta de marsato se mantiene.
Brilliand

4

"Preferir composición sobre herencia" es solo una buena heurística

Debe considerar el contexto, ya que esta no es una regla universal. No lo tome en el sentido de que nunca use la herencia cuando pueda hacer composición. Si ese fuera el caso, lo solucionaría prohibiendo la herencia.

Espero aclarar este punto a lo largo de esta publicación.

No intentaré defender los méritos de la composición por sí mismo. Lo que considero fuera de tema. En cambio, hablaré sobre algunas situaciones en las que el desarrollador puede considerar la herencia que sería mejor usando la composición. En ese sentido, la herencia tiene sus propios méritos, que también considero fuera de tema.


Ejemplo de vehículo

Estoy escribiendo sobre desarrolladores que intentan hacer las cosas tontamente, con fines narrativos.

Vayamos para una variante de los ejemplos clásicos que algunos cursos de programación orientada a objetos utilizan ... Tenemos una Vehicleclase, entonces derivamos Car, Airplane, Balloony Ship.

Nota : Si necesita fundamentar este ejemplo, imagine que estos son tipos de objetos en un videojuego.

Entonces Cary Airplanepuede tener algún código común, porque ambos pueden rodar sobre ruedas en tierra. Los desarrolladores pueden considerar crear una clase intermedia en la cadena de herencia para eso. Sin embargo, en realidad también hay un código compartido entre Airplaney Balloon. Podrían considerar crear otra clase intermedia en la cadena de herencia para eso.

Por lo tanto, el desarrollador estaría buscando herencia múltiple. En el punto donde los desarrolladores buscan herencia múltiple, el diseño ya salió mal.

Es mejor modelar este comportamiento como interfaces y composición, para que podamos reutilizarlo sin tener que encontrar una herencia de clases múltiples. Si los desarrolladores, por ejemplo, crean la FlyingVehiculeclase. Dirían que Airplanees una FlyingVehicule(herencia de clase), pero podríamos decir que Airplanetiene un Flyingcomponente (composición) y Airplanees una IFlyingVehicule(herencia de interfaz).

Usando interfaces, si es necesario, podemos tener herencia múltiple (de interfaces). Además, no se está acoplando a una implementación particular. Aumento de la reutilización y la capacidad de prueba de su código.

Recuerde que la herencia es una herramienta para el polimorfismo. Además, el polimorfismo es una herramienta para la reutilización. Si puede aumentar la reutilización de su código utilizando la composición, hágalo. Si no está seguro de lo que sea o no, la composición proporciona una mejor reutilización, "Preferir composición sobre herencia" es una buena heurística.

Todo eso sin mencionarlo Amphibious.

De hecho, es posible que no necesitemos cosas que despegan. Stephen Hurn tiene un ejemplo más elocuente en sus artículos "Favorecer la composición sobre la herencia", parte 1 y parte 2 .


Sustituibilidad y encapsulación

¿Debe Aheredar o componer B?

Si Auna especialización de Beso debe cumplir el principio de sustitución de Liskov , la herencia es viable, incluso deseable. Si hay situaciones en las Aque no hay una sustitución válida, Bentonces no deberíamos usar la herencia.

Podríamos estar interesados ​​en la composición como una forma de programación defensiva, para defender la clase derivada . En particular, una vez que comience a usarlo Bpara otros fines diferentes, puede haber presión para cambiarlo o extenderlo para que sea más adecuado para esos fines. Si existe el riesgo de Bexponer métodos que podrían dar como resultado un estado no válido A, deberíamos usar composición en lugar de herencia. Incluso si somos el autor de ambos By A, es una cosa menos de qué preocuparse, por lo tanto, la composición facilita la reutilización de B.

Incluso podemos argumentar que si hay características Bque Ano son necesarias (y no sabemos si esas características podrían dar lugar a un estado no válido A, ya sea en la implementación actual o en el futuro), es una buena idea usar composición en lugar de herencia.

La composición también tiene las ventajas de permitir implementaciones de conmutación y facilitar la burla.

Nota : hay situaciones en las que queremos usar la composición a pesar de que la sustitución sea válida. Archivamos esa sustituibilidad mediante el uso de interfaces o clases abstractas (cuál usar cuando es otro tema) y luego usamos la composición con inyección de dependencia de la implementación real.

Finalmente, por supuesto, existe el argumento de que deberíamos usar la composición para defender la clase padre porque la herencia rompe la encapsulación de la clase padre:

La herencia expone una subclase a los detalles de la implementación de su padre, a menudo se dice que 'la herencia rompe la encapsulación'

- Patrones de diseño: elementos de software orientado a objetos reutilizables, pandilla de cuatro

Bueno, esa es una clase para padres mal diseñada. Por eso deberías:

Diseñe para la herencia, o prohíbala.

- Efectivo Java, Josh Bloch


Problema de YoYo

Otro caso donde la composición ayuda es el problema de YoYo . Esta es una cita de Wikipedia:

En el desarrollo de software, el problema del yoyo es un antipatrón que ocurre cuando un programador tiene que leer y comprender un programa cuyo gráfico de herencia es tan largo y complicado que el programador tiene que seguir cambiando entre muchas definiciones de clase diferentes para seguir El flujo de control del programa.

Puede resolver, por ejemplo: su clase Cno heredará de la clase B. En cambio, su clase Ctendrá un miembro de tipo A, que puede o no ser (o tener) un objeto de tipo B. De esta manera, no estará programando contra el detalle de implementación de B, sino contra el contrato que ofrece la interfaz (de) A.


Ejemplos de contadores

Muchos marcos favorecen la herencia sobre la composición (que es lo contrario de lo que hemos estado discutiendo). El desarrollador puede hacer esto porque ponen mucho trabajo en su clase base de que tenerlo implementado con composición hubiera aumentado el tamaño del código del cliente. A veces esto se debe a limitaciones del idioma.

Por ejemplo, un marco PHP ORM puede crear una clase base que use métodos mágicos para permitir escribir código como si el objeto tuviera propiedades reales. En cambio, el código manejado por los métodos mágicos irá a la base de datos, consultará el campo solicitado en particular (quizás lo almacene en caché para futuras solicitudes) y lo devolverá. Hacerlo con composición requeriría que el cliente cree propiedades para cada campo o escriba alguna versión del código de métodos mágicos.

Anexo : Hay algunas otras formas en que uno podría permitir extender los objetos ORM. Por lo tanto, no creo que la herencia sea necesaria en este caso. Es más barato.

Para otro ejemplo, un motor de videojuego puede crear una clase base que usa código nativo dependiendo de la plataforma de destino para hacer renderizado 3D y manejo de eventos. Este código es complejo y específico de la plataforma. Sería costoso y propenso a errores para el usuario desarrollador del motor lidiar con este código, de hecho, eso es parte de la razón del uso del motor.

Además, sin la parte de renderizado 3D, así es como funcionan muchos marcos de widgets. Esto lo libera de la preocupación de manejar los mensajes del sistema operativo ... de hecho, en muchos idiomas no puede escribir dicho código sin alguna forma de oferta nativa. Además, si lo hiciera, reduciría su portabilidad. En cambio, con herencia, siempre que el desarrollador no rompa la compatibilidad (demasiado); podrá transferir fácilmente su código a cualquier plataforma nueva que admitan en el futuro.

Además, tenga en cuenta que muchas veces solo queremos anular algunos métodos y dejar todo lo demás con las implementaciones predeterminadas. Si hubiéramos estado usando la composición, tendríamos que crear todos esos métodos, aunque solo fuera para delegar al objeto envuelto.

Según este argumento, hay un punto en el que la composición puede ser peor para la mantenibilidad que la herencia (cuando la clase base es demasiado compleja). Sin embargo, recuerde que la capacidad de mantenimiento de la herencia puede ser peor que la de la composición (cuando el árbol de herencia es demasiado complejo), que es a lo que me refiero en el problema del yoyo.

En los ejemplos presentados, los desarrolladores rara vez tienen la intención de reutilizar el código generado por herencia en otros proyectos. Eso mitiga la reutilización disminuida del uso de la herencia en lugar de la composición. Además, al usar la herencia, los desarrolladores del marco pueden proporcionar mucho código fácil de usar y fácil de descubrir.


Pensamientos finales

Como puede ver, la composición tiene alguna ventaja sobre la herencia en algunas situaciones, no siempre. Es importante tener en cuenta el contexto y los diferentes factores involucrados (como la reutilización, la mantenibilidad, la capacidad de prueba, etc.) para tomar la decisión. Volver al primer punto: "Prefiero la composición sobre la herencia" es un solo buena heurística.

También puede tener avisos de que muchas de las situaciones que describo pueden resolverse con Rasgos o Mixins hasta cierto punto. Lamentablemente, estas no son características comunes en la gran lista de idiomas, y generalmente tienen un costo de rendimiento. Afortunadamente, sus métodos de extensión primos populares y las implementaciones predeterminadas mitigan algunas situaciones.

Tengo una publicación reciente donde hablo sobre algunas de las ventajas de las interfaces en por qué requerimos interfaces entre UI, Business y acceso a datos en C # . Ayuda a desacoplar y facilita la reutilización y la capacidad de prueba, puede interesarle.


Uh ... .Net Framework utiliza varios tipos de secuencias heredadas para hacer su trabajo relacionado con la secuencia. Funciona increíblemente bien y es súper fácil de usar y extender. Su ejemplo no es muy bueno ...
T. Sar

1
@TSar "es súper fácil de usar y extender" ¿Alguna vez ha tratado de pasar el flujo de salida estándar a Debug? Esa ha sido mi única experiencia tratando de extender sus transmisiones. No fue fácil. Fue una pesadilla que terminó en anular todos los métodos de la clase explícitamente. Extremadamente repetitivo. Y si nos fijamos en el código fuente .NET, anular todo explícitamente es más o menos cómo lo hacen. Así que no estoy convencido de que sea tan fácil de extender.
jpmc26

La lectura y escritura de una estructura de una secuencia se realiza mejor con funciones libres o métodos múltiples.
user253751

@ jpmc26 Lamento que tu experiencia no haya sido muy buena. Sin embargo, no tuve problemas para usar o implementar cosas relacionadas con la transmisión para diferentes cosas.
T. Sar

3

Normalmente, la idea principal de la Composición sobre herencia es proporcionar una mayor flexibilidad de diseño y no reducir la cantidad de código que necesita cambiar para propagar un cambio (que me parece cuestionable).

El ejemplo en wikipedia da un mejor ejemplo del poder de su enfoque:

Al definir una interfaz base para cada "pieza de composición", podría crear fácilmente varios "objetos compuestos" con una variedad que podría ser difícil de alcanzar solo con la herencia.


Entonces esta sección da un ejemplo de composición, ¿verdad? Estoy preguntando porque no es obvio en el artículo.
Utku

1
Sí, "Composición sobre herencia" no significa que no tienes interfaces, si miras el objeto Player puedes ver que encaja perfectamente en una jerarquía, pero el código (perdona la brutalidad) no proviene de la herencia, pero de la composición con alguna clase que hace el trabajo
lbenini

2

La línea: "Prefiere la composición sobre la herencia" no es más que un empujón en la dirección correcta. No te salvará de tu propia estupidez y, de hecho, te dará la oportunidad de empeorar las cosas. Eso es porque hay mucho poder aquí.

La razón principal por la que esto es necesario decir es porque los programadores son flojos. Tan perezosos tomarán cualquier solución que signifique menos tipeo del teclado. Ese es el principal punto de venta de la herencia. Escribo menos hoy y alguien más se jode mañana. Y qué, quiero irme a casa.

Al día siguiente, el jefe insiste en que use composición. No explica por qué, pero insiste en ello. Así que tomo una instancia de lo que estaba heredando, expongo una interfaz idéntica a la que estaba heredando e implemento esa interfaz delegando todo el trabajo a esa cosa de la que heredé. Todo se trata de escribir con muerte cerebral que podría haberse hecho con una herramienta de refactorización y me parece inútil, pero el jefe lo quería, así que lo hago.

Al día siguiente le pregunto si esto es lo que quería. ¿Qué crees que dice el jefe?

A menos que hubiera alguna necesidad de poder cambiar dinámicamente (en tiempo de ejecución) lo que se heredaba (ver patrón de estado), esto era una gran pérdida de tiempo. ¿Cómo puede el jefe decir eso y aún estar a favor de la composición sobre la herencia?

Además de esto, no hacer nada para evitar la rotura debido a cambios en la firma del método, esto pierde por completo la mayor ventaja de la composición. A diferencia de la herencia, eres libre de hacer una interfaz diferente . Uno más apropiado para cómo se usará en este nivel. Ya sabes, abstracción!

Hay algunas ventajas menores para reemplazar automáticamente la herencia con composición y delegación (y algunas desventajas), pero mantener el cerebro apagado durante este proceso pierde una gran oportunidad.

La indirecta es muy poderosa. Úsalo con sabiduría.


0

Aquí hay otra razón: en muchos lenguajes OO (estoy pensando en otros como C ++, C # o Java aquí), no hay forma de cambiar la clase de un objeto una vez que lo ha creado.

Supongamos que estamos creando una base de datos de empleados. Tenemos una clase abstracta de Empleado base, y comenzamos a derivar nuevas clases para roles específicos: ingeniero, guardia de seguridad, conductor de camión, etc. Cada clase tiene un comportamiento especializado para ese rol. Todo es bueno.

Entonces, un día un ingeniero es ascendido a gerente de ingeniería. No puede volver a clasificar a un ingeniero existente. En su lugar, debe escribir una función que cree un Gerente de ingeniería, que copie todos los datos del Ingeniero, elimine al Ingeniero de la base de datos y luego agregue el nuevo Gerente de ingeniería. Y tiene que escribir dicha función para cada posible cambio de rol.

Peor aún, supongamos que un conductor de camión se va de licencia por enfermedad a largo plazo, y un guardia de seguridad le ofrece hacer una conducción de camión a tiempo parcial para completar. Ahora tiene que inventar una nueva clase de guardia de seguridad y conductor de camión solo para ellos.

Hace la vida mucho más fácil crear una clase de empleado concreta y tratarla como un contenedor al que puede agregar propiedades, como el rol de trabajo.


O puede crear una clase de empleado con un puntero a una lista de roles, y cada trabajo real extiende el rol. Su ejemplo podría hacerse para usar la herencia de una manera muy buena y sensata sin demasiado trabajo.
T. Sar

0

La herencia proporciona dos cosas:

1) Reutilización de código: se puede lograr fácilmente a través de la composición. Y, de hecho, se logra mejor a través de la composición, ya que conserva mejor la encapsulación.

2) Polimorfismo: se puede lograr a través de "interfaces" (clases donde todos los métodos son puramente virtuales).

Un argumento sólido para usar la herencia sin interfaz es cuando la cantidad de métodos requeridos es grande y su subclase solo quiere alterar un pequeño subconjunto de ellos. Sin embargo, en general, su interfaz debe tener huellas muy pequeñas si se suscribe al principio de "responsabilidad única" (debe hacerlo), por lo que estos casos deberían ser raros.

Tener el estilo de codificación que requiere anular todos los métodos de clase principal no evita escribir una gran cantidad de métodos de transferencia de todos modos, entonces, ¿por qué no reutilizar el código a través de la composición y obtener el beneficio adicional de la encapsulación? Además, si la superclase se extendiera agregando otro método, el estilo de codificación requeriría agregar ese método a todas las subclases (y hay una buena posibilidad de que estemos violando la "segregación de interfaz" ). Este problema crece rápidamente cuando comienza a tener múltiples niveles de herencia.

Finalmente, en muchos idiomas, la prueba unitaria de objetos basados ​​en "composición" es mucho más fácil que la prueba unitaria de objetos basados ​​en "herencia" al sustituir el objeto miembro con un objeto simulado / prueba / ficticio.


0

¿Es esa realmente la única razón para favorecer la composición sobre la herencia?

No

Hay muchas razones para favorecer la composición sobre la herencia. No hay una sola respuesta correcta. Pero pondré otra razón para favorecer la composición sobre la herencia en la mezcla:

Favorecer la composición sobre la herencia mejora la legibilidad de su código , lo cual es muy importante cuando se trabaja en un proyecto grande con varias personas.

Así es como lo pienso: cuando leo el código de otra persona, si veo que han extendido una clase, voy a preguntar qué comportamiento en la superclase están cambiando.

Y luego revisaré su código, buscando una función de reemplazo. Si no veo ninguno, entonces lo clasifico como un mal uso de la herencia. ¡Deberían haber usado composición en su lugar, aunque solo fuera para salvarme (y a todas las demás personas del equipo) de 5 minutos de leer su código!

Aquí hay un ejemplo concreto: en Java, la JFrameclase representa una ventana. Para crear una ventana, crea una instancia de JFramey luego llama a funciones en esa instancia para agregarle cosas y mostrarla.

Muchos tutoriales (incluso los oficiales) recomiendan que extiendas JFramepara que puedas tratar las instancias de tu clase como instancias de JFrame. Pero en mi humilde opinión (y la opinión de muchos otros) ¡es un mal hábito! En su lugar, solo debe crear una instancia JFramey llamar a funciones en esa instancia. No hay absolutamente ninguna necesidad de extender JFrameen esta instancia, ya que no está cambiando ninguno de los comportamientos predeterminados de la clase principal.

Por otro lado, una forma de realizar una pintura personalizada es extender la JPanelclase y anular la paintComponent()función. Este es un buen uso de la herencia. Si estoy revisando su código y veo que ha extendido JPanely anulado la paintComponent()función, sabré exactamente qué comportamiento está cambiando.

Si solo está escribiendo código usted mismo, entonces podría ser tan fácil usar la herencia como usar la composición. Pero imagina que estás en un entorno grupal y que otras personas leerán tu código. Favorecer la composición sobre la herencia hace que su código sea más fácil de leer.


Agregar una clase de fábrica completa con un solo método que devuelve una instancia de un objeto para que pueda usar la composición en lugar de la herencia realmente no hace que su código sea más legible ... (Sí, necesita esto si necesita una nueva instancia cada vez se llama un método particular.) Eso no significa que la herencia sea buena, pero la composición no siempre mejora la legibilidad.
jpmc26

1
@ jpmc26 ... ¿por qué necesitarías una clase de fábrica solo para crear una nueva instancia de una clase? ¿Y cómo mejora la herencia la legibilidad de lo que estás describiendo?
Kevin Workman

¿No te di explícitamente una razón para tal fábrica en el comentario anterior? Es más legible porque da como resultado menos código para leer . Puede lograr el mismo comportamiento con un único método abstracto en la clase base y algunas subclases. (De todos modos, iba a tener una implementación diferente de su clase compuesta para cada una). Si los detalles de la construcción del objeto están fuertemente vinculados a la clase base, no tendrá mucho uso en ningún otro lugar de su código, reduciendo aún más Los beneficios de hacer una clase separada. Luego, mantiene partes de código estrechamente relacionadas entre sí.
jpmc26

Como dije, esto no necesariamente significa que la herencia es buena, pero ejemplifica cómo la composición no mejora la legibilidad en algunas situaciones.
jpmc26

1
Sin ver un ejemplo específico, es difícil comentar realmente. Pero lo que has descrito me parece un caso de ingeniería excesiva. ¿Necesitas una instancia de una clase? La newpalabra clave te da uno. De todos modos, gracias por la discusión.
Kevin Workman
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.