SOLID PRINCIPLES Flashcards

1
Q

¿Que es STUPID?

A

STUPID es simplemente un acrónimo basado en seis code smells que describen cómo NO debe ser el software que
desarrollamos

  • Singleton: patrón singleton
  • Tight Coupling: alto acoplamiento
  • Untestability: código no testeable
  • Premature optimization: optimizaciones prematuras
  • Indescriptive Naming: nombres poco descriptivos
  • Duplication: duplicidad de código
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
2
Q

STUPID 1: Singleton como code smell

A

El patrón Singleton es un patrón de diseño que busca asegurar que una clase tenga una única instancia en toda la aplicación y proporciona un punto de acceso global a esa instancia.

¿Cómo funciona?

  1. La clase tiene una propiedad estática que almacena la instancia única.
  2. Al intentar crear una nueva instancia, el constructor verifica si ya existe una instancia en esa propiedad estática:
    • Si existe, devuelve esa instancia.
    • Si no existe, crea una nueva, la almacena en la propiedad estática y la devuelve.

```javascript
class Singleton {
constructor() {
if (Singleton.instance) {
return Singleton.instance; // Devuelve la instancia existente
}
this.title = “my singleton”; // Inicializa la nueva instancia
Singleton.instance = this; // Almacena la instancia única
}
}

let mySingleton = new Singleton();
let mySingleton2 = new Singleton();

console.log(“Singleton 1: “, mySingleton.title); // “my singleton”
mySingleton.title = “modified in instance 1”;
console.log(“Singleton 2: “, mySingleton2.title); // “modified in instance 1”
~~~

Problemas del Singleton

  1. Exposición global del estado
    • Como el Singleton es accesible globalmente, cualquier parte de la aplicación puede modificar su estado. Esto dificulta rastrear los cambios y genera dependencias implícitas entre módulos, complicando el mantenimiento.
  2. Dificultad para realizar pruebas unitarias
    • Las pruebas unitarias deben ser independientes, pero el Singleton mantiene su estado entre ejecuciones.
    • Si una prueba modifica el estado, otra prueba podría fallar inesperadamente.
  3. Acoplamiento fuerte
    • Los módulos que acceden directamente al Singleton introducen una dependencia implícita, dificultando cambios futuros, como reemplazarlo por otra implementación.
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
3
Q

Cohesión

A

Cohesión

La cohesión se refiere a qué tan relacionados están los elementos dentro de un módulo o clase. Un módulo tiene alta cohesión cuando todas las partes que lo componen están muy enfocadas en una única responsabilidad o propósito. Un código con alta cohesión es más fácil de entender, mantener y probar, ya que todas las piezas del código están muy conectadas entre sí para lograr un objetivo claro.

Ejemplo de alta cohesión:
Imagina una clase que solo se encarga de realizar cálculos:

```javascript
class Calculadora {
sumar(a, b) {
return a + b;
}

restar(a, b) {
    return a - b;
} } ~~~ - Aquí, **todos los métodos de la clase están relacionados con el cálculo**.   - Esto hace que la clase sea **fácil de entender** y **sólo se enfoque en una tarea** (realizar cálculos).   - Esta clase tiene **alta cohesión**.

Ejemplo de baja cohesión:
Si la misma clase comenzara a hacer más tareas no relacionadas, como enviar correos y manipular texto, perdería cohesión:

```javascript
class Utilidades {
enviarCorreo(correo) {
// lógica para enviar correo
}

convertirTextoAMayusculas(texto) {
    return texto.toUpperCase();
}

sumar(a, b) {
    return a + b;
} } ~~~ - Aquí, **los métodos están desconectados entre sí**: enviar correos, modificar texto y realizar cálculos.   - Esto crea una clase que es **difícil de entender y mantener** porque tiene responsabilidades **divididas**, lo que se traduce en **baja cohesión**.
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
4
Q

Acoplamiento

A

Acoplamiento

El acoplamiento mide qué tan dependientes están los módulos entre sí. Un sistema tiene alto acoplamiento cuando los módulos están estrechamente interconectados y dependen unos de otros. Por el contrario, un sistema con bajo acoplamiento tiene módulos más independientes, lo que facilita el mantenimiento, las pruebas y la reutilización.

Ejemplo de alto acoplamiento:
Imagina una clase Pedido que depende directamente de la clase Notificador:

```javascript
class Pedido {
constructor() {
this.notificador = new Notificador(); // Dependencia directa
}

procesar() {
    this.notificador.enviarNotificacion("Pedido procesado");
} }

class Notificador {
enviarNotificacion(mensaje) {
console.log(“Notificación enviada:”, mensaje);
}
}
~~~
- En este caso, Pedido depende directamente de Notificador.
- Si alguna vez cambias el funcionamiento de Notificador, tendrás que modificar también la clase Pedido.
- Esto es alto acoplamiento porque los módulos no son independientes.

Ejemplo de bajo acoplamiento:
En este caso, si pasamos el Notificador como parámetro al constructor de Pedido, reducimos la dependencia directa.
Esto se llama Inyección de Dependencias:

```javascript
class Pedido {
constructor(notificador) {
this.notificador = notificador; // Inyección de dependencia
}

procesar() {
    this.notificador.enviarNotificacion("Pedido procesado");
} }

class Notificador {
enviarNotificacion(mensaje) {
console.log(“Notificación enviada:”, mensaje);
}
}

// Uso:
const notificador = new Notificador();
const pedido = new Pedido(notificador);
pedido.procesar();
~~~
- Aquí, Pedido no depende directamente de la clase Notificador, sino que la recibe desde fuera.
- Esto es bajo acoplamiento, porque puedes cambiar la implementación de Notificador sin necesidad de modificar la clase Pedido.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
5
Q

El equilibrio entre cohesión y acoplamiento

A

El equilibrio entre cohesión y acoplamiento

  • Alta cohesión significa que los módulos o clases deben enfocarse en una sola responsabilidad y tener todo lo necesario para cumplir con esa responsabilidad. Esto mejora la comprensión y el mantenimiento del código.
  • Bajo acoplamiento significa que los módulos deben depender lo menos posible unos de otros. Esto facilita la reutilización, el cambio y las pruebas, ya que cada módulo puede ser modificado de manera independiente.

El verdadero desafío en el diseño de sistemas es encontrar el equilibrio entre cohesión y acoplamiento. Si maximizas la cohesión, podrías crear módulos muy específicos pero con muchas dependencias entre ellos, lo que aumenta el acoplamiento. Por el contrario, si minimizas el acoplamiento, puedes terminar con módulos más dispersos y difíciles de gestionar. Por eso, la clave es favorecer un bajo acoplamiento sin sacrificar la cohesión.

Esto permitirá crear sistemas modulares, fáciles de mantener, probar y extender.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
6
Q

Código no testeable

A

El código no testeable suele ser el resultado de un alto acoplamiento y la FALTA de inyección de dependencias. Este último aspecto se aborda con el principio SOLID de inversión de dependencias. Aunque existen técnicas específicas para solucionar estos problemas, la clave es diseñar el sistema teniendo en cuenta la testabilidad desde el inicio. Esto nos permite detectar problemas como el alto acoplamiento y las dependencias de estado global de manera temprana. Estos temas se explorarán más a fondo en las secciones de Unit Testing y TDD.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
7
Q

Optimización Prematura

A

Evitar la Complicación Innecesaria

“Cuando lleguemos a ese río, cruzaremos ese puente”. Retrasar decisiones permite enfocarse en lo más importante: las reglas de negocio, donde está el valor real. Al aplazar estas decisiones, se obtiene más información sobre los requisitos del proyecto, lo que facilita tomar mejores decisiones.

Donald Knuth (el de The Art of Computer Programming) afirmaba que la optimización prematura es la raíz de todos los males. No se trata de escribir código no optimizado, sino de evitar desarrollar abstracciones innecesarias que compliquen el software sin necesidad.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
8
Q

Complejidad Esencial vs. Complejidad Accidental

A

La complejidad accidental ocurre cuando se introduce una solución innecesariamente compleja en un proyecto software. La complejidad esencial es la inherente al problema, la cual debe ser la única presente. Sin embargo, a menudo agregamos complejidad accidental debido al desconocimiento o falta de planificación, lo que hace el proyecto difícil de mantener y poco adaptable al cambio.

Fred Brooks, en su artículo No Silver Bullet, distinguió entre estas dos complejidades, basándose en la descomposición aristotélica del conocimiento.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
9
Q

STUPID 2: Indescriptive Naming

A

nombres poco
descriptivos. Básicamente viene a decirnos que los nombres de variables, métodos
y clases deben seleccionarse con cuidado para que den expresividad y significado a
nuestro código

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
10
Q

Principio DRY (Don’t Repeat Yourself)

A

El último principio del acrónimo STUPID hace referencia al principio DRY, que nos aconseja evitar la duplicación de código. La idea central de DRY es que cada pieza de conocimiento o lógica debe tener una única representación dentro del sistema. Esto mejora la mantenibilidad, reduce errores y facilita las modificaciones en el futuro.

Aunque este principio es valioso, es importante recordar que no es absoluto. Existen excepciones donde la duplicación puede ser necesaria, por ejemplo, en casos de optimización o cuando la claridad del código se ve beneficiada.

En resumen, el principio DRY es fundamental para escribir código más limpio y fácil de mantener, pero siempre se debe considerar el contexto y las necesidades del proyecto.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
11
Q

Principios STUPID: Orden y Significado

A
  1. S - Singleton: Evitar la creación de instancias innecesarias de una clase.
  2. T - Tight coupling: Evitar el alto acoplamiento entre componentes.
  3. U - Untestability: Evitar código difícil de testear.
  4. P - Premature optimization: Evitar la optimización prematura que añade complejidad innecesaria.
  5. I - Inconsistent interfaces: Evitar interfaces inconsistentes que complican la interacción entre componentes.
  6. D - Duplication: Evitar la duplicación de código (principio DRY).
  7. S - Speculative generality: Evitar abstracciones innecesarias o generalizaciones especulativas que añaden complejidad.
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
12
Q

Duplicidad Real: Qué Es y Cómo Evitarla

A

La duplicidad real ocurre cuando el código es idéntico y cumple la misma función en múltiples lugares del proyecto. Esto implica que, al hacer un cambio, debemos propagarlo manualmente en todos esos lugares, lo cual aumenta las probabilidades de error humano.

Ejemplo:

Imagina que tienes el siguiente código duplicado en dos funciones diferentes:

```javascript
function calcularDescuento1(precio) {
return precio * 0.9;
}

function calcularDescuento2(precio) {
return precio * 0.9;
}
~~~

Si necesitamos cambiar la lógica del descuento (por ejemplo, modificar el porcentaje), tendríamos que hacerlo en ambos lugares, lo que aumenta la posibilidad de cometer errores.

Solución:
Unifica la lógica del descuento en una sola función y reutilízala.

```javascript
function calcularDescuento(precio, porcentaje = 0.9) {
return precio * porcentaje;
}
~~~

Este formato ayuda a recordar lo que es la duplicidad real y cómo solucionarla con un ejemplo práctico.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
13
Q

Duplicidad Accidental: Qué Es y Cómo Evitarla

A

Duplicidad Accidental: Qué Es y Cómo Evitarla

La duplicidad accidental ocurre cuando el código parece el mismo pero cumple funciones diferentes. A diferencia de la duplicidad real, en este caso, no es necesario propagar el cambio a todos los lugares donde aparece el código, ya que el cambio solo afectará a uno de esos lugares. A pesar de esto, es un tipo de código que deberíamos evitar y buscar unificar.

Ejemplo:

Imagina que tienes dos funciones que calculan el impuesto en diferentes situaciones:

```javascript
function calcularImpuestoVenta(precio) {
return precio * 0.21; // Impuesto sobre ventas del 21%
}

function calcularImpuestoServicios(precio) {
return precio * 0.18; // Impuesto sobre servicios del 18%
}
~~~

Aunque el código se ve similar, cada función está haciendo un cálculo con un porcentaje diferente. Si necesitas cambiar el porcentaje, es probable que solo debas cambiar uno de los lugares, lo que introduce el riesgo de inconsistencias.

Solución:
Unifica la lógica de cálculo del impuesto y hazla flexible para manejar diferentes porcentajes:

```javascript
function calcularImpuesto(precio, porcentaje) {
return precio * porcentaje;
}
~~~

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
14
Q

Principios SOLID: Qué Son

A

Los principios SOLID nos guían sobre cómo organizar nuestras funciones, estructuras de datos y componentes de software. Aunque originalmente se aplican a clases en la orientación a objetos, estos principios también son útiles para agrupaciones de funciones y datos (por ejemplo, en una Closure). Son fundamentales para cualquier tipo de producto software, ya sea orientado a objetos o no.

El acrónimo SOLID fue creado por Michael Feathers y popularizado por Robert C. Martin en su libro Agile Software Development: Principles, Patterns, and Practices. Los cinco principios tienen como objetivo mejorar la mantenibilidad, simplificar los cambios y facilitar el testing del código.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
15
Q

Principios SOLID: Qué Son Ancronimo

A
  1. Single Responsibility (Responsabilidad Única)
    Cada clase debe tener una única responsabilidad para evitar cambios innecesarios.
    Ejemplo: Separar cálculo de salario y generación de reportes.
  2. Open/Closed (Abierto/Cerrado)
    Las clases deben estar abiertas para la extensión pero cerradas para la modificación.
    Ejemplo: Añadir nuevos descuentos sin modificar el código original.
  3. Liskov Substitution (Sustitución de Liskov)
    Las subclases deben ser reemplazables por sus clases base sin alterar el comportamiento.
    Ejemplo: Sustituir Perro por Animal sin problemas.
  4. Interface Segregation (Segregación de Interfaz)
    Las interfaces deben ser específicas y no forzar la implementación de métodos innecesarios.
    Ejemplo: Crear interfaces pequeñas, como Imprimible, sin agregar métodos irrelevantes.
  5. Dependency Inversion (Inversión de Dependencia)
    Las clases deben depender de abstracciones, no de clases concretas.
    Ejemplo: Usar una interfaz ProcesadorDePago en lugar de una clase concreta como Paypal.
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
16
Q

Responsabilidad Única (SRP): Resumen y Ejemplo

A

El principio de responsabilidad única no se refiere a tener clases con un solo método, sino a diseñar componentes que solo estén expuestos a una fuente de cambio. Cada módulo o clase debe ser responsable de un único aspecto de la funcionalidad del software.

Ejemplo:

```javascript
class UseCase {
doSomethingWithTaxes() {
console.log(“Do something related with taxes …”);
}

saveChangesInDatabase() {
console.log(“Saving in database …”);
}

sendEmail() {
console.log(“Sending email …”);
}
}

function start() {
const myUseCase = new UseCase();

myUseCase.doSomethingWithTaxes();
myUseCase.saveChangesInDatabase();
myUseCase.sendEmail();
}

start();
~~~

En este ejemplo, la clase UseCase mezcla tres capas de lógica: negocio, presentación y persistencia, lo cual viola el principio de responsabilidad única (SRP). Cada método podría ser responsable de distintos actores:

  • Contabilidad podría cambiar doSomethingWithTaxes.
  • Marketing podría modificar sendEmail.
  • Base de datos podría cambiar saveChangesInDatabase.

Aunque en proyectos pequeños no siempre se percibe la necesidad, diferenciar responsabilidades aumenta la flexibilidad y tolerancia al cambio en el software.

Este resumen incluye el código de ejemplo y muestra cómo la violación del principio SRP afecta la flexibilidad del software.

17
Q

Responsabilidad Única (SRP) aplicado a componentes de Vue

A

El principio de responsabilidad única nos dice que un componente debe encargarse de una sola responsabilidad. En Vue, esto significa que un componente debe tener un único propósito y no mezclar diferentes tipos de lógica (como la presentación, la lógica de negocio o la persistencia de datos) dentro del mismo componente.

Ejemplo de Violación del SRP en Vue

Supongamos que tienes un componente de Vue que maneja tanto la lógica de negocio (calcular impuestos), como el envío de emails y la persistencia de datos:

```vue

<template>
<div>
<button @click="handleClick">Ejecutar Acción</button>
</div>
</template>

<script>
export default {
  data() {
    return {
      user: { name: 'Juan', email: 'juan@example.com' },
    };
  },
  methods: {
    handleClick() {
      this.doSomethingWithTaxes();
      this.saveChangesInDatabase();
      this.sendEmail();
    },

    doSomethingWithTaxes() {
      console.log("Calculando impuestos...");
    },

    saveChangesInDatabase() {
      console.log("Guardando cambios en la base de datos...");
    },

    sendEmail() {
      console.log("Enviando email a: " + this.user.email);
    }
  }
}
</script>
En este ejemplo, el componente hace tres cosas diferentes:
1. **Cálculo de impuestos** (`doSomethingWithTaxes`)
2. **Persistencia de datos** (`saveChangesInDatabase`)
3. **Envío de emails** (`sendEmail`)

Problema con este Enfoque

Este componente **viola el principio de responsabilidad única** porque está manejando múltiples responsabilidades:
- El cálculo de impuestos puede cambiar debido a nuevas regulaciones fiscales (lo que involucra a un equipo de contabilidad).
- El proceso de enviar emails podría cambiar por necesidades de marketing.
- La lógica de persistencia podría cambiar si se cambian los requisitos de la base de datos.

Cómo Aplicar SRP en Vue

Para aplicar el **SRP**, deberías dividir este componente en **múltiples componentes** o servicios que manejen cada responsabilidad por separado.

Ejemplo de Cómo Corregirlo

```vue
<!-- TaxCalculator.vue -->
<template>
  <div>
    <p>Calculando impuestos...</p>
  </div>
</template>

<script>
export default {
  methods: {
    calculateTaxes() {
      console.log("Calculando impuestos...");
    }
  }
}
</script>

```vue
<!-- EmailSender.vue -->

<template>
<div>
<p>Enviando email...</p>
</div>
</template>

<script>
export default {
  methods: {
    sendEmail(user) {
      console.log("Enviando email a: " + user.email);
    }
  }
}
</script>
```vue
<!-- DatabaseSaver.vue -->
<template>
  <div>
    <p>Guardando cambios...</p>
  </div>
</template>

<script>
export default {
  methods: {
    saveChanges() {
      console.log("Guardando cambios en la base de datos...");
    }
  }
}
</script>

```vue
<!-- MainComponent.vue -->

<template>
<div>
<TaxCalculator></TaxCalculator>
<EmailSender :user="user" />
<DatabaseSaver></DatabaseSaver>
</div>
</template>

<script>
import TaxCalculator from './TaxCalculator.vue';
import EmailSender from './EmailSender.vue';
import DatabaseSaver from './DatabaseSaver.vue';

export default {
  components: {
    TaxCalculator,
    EmailSender,
    DatabaseSaver
  },
  data() {
    return {
      user: { name: 'Juan', email: 'juan@example.com' }
    };
  }
}
</script>

~~~

Beneficios

  1. Flexibilidad: Cada componente tiene una única responsabilidad y puede evolucionar sin afectar a los demás. Si el cálculo de impuestos cambia, solo debes actualizar TaxCalculator.vue.
  2. Mantenibilidad: Los cambios en una parte del sistema (por ejemplo, el envío de emails) no afectarán otras partes del sistema.
  3. Escalabilidad: A medida que el proyecto crezca, será más fácil agregar nuevas funcionalidades sin comprometer el código existente.

Este enfoque aplica el principio SRP de forma práctica a los componentes de Vue, asegurando que cada componente tenga un único propósito y facilitando el mantenimiento y evolución del código.

18
Q

Vuex como Singleton

A

Sí, Vuex puede considerarse un Singleton en el contexto de gestión de estado, ya que:

  1. Una única instancia: Vuex crea una única instancia global de la tienda (store) para toda la aplicación. Este es el principio básico del patrón Singleton, que asegura que solo haya una instancia de la tienda y que todos los componentes accedan a esa misma instancia.
  2. Acceso único a la tienda: Toda la aplicación de Vue interactúa con esa única tienda de Vuex. Los componentes no crean nuevas instancias de Vuex, sino que simplemente se conectan a la tienda global a través de this.$store.

Ejemplo de Vuex como Singleton:

store.js (instancia única de Vuex):
```js
import { createStore } from ‘vuex’;

const store = createStore({
state: {
counter: 0,
},
mutations: {
increment(state) {
state.counter++;
},
},
actions: {
increment({ commit }) {
commit(‘increment’);
},
},
getters: {
getCounter: (state) => state.counter,
},
});

export default store;
~~~

main.js (registrando Vuex en la aplicación):
```js
import { createApp } from ‘vue’;
import App from ‘./App.vue’;
import store from ‘./store’; // Única instancia de Vuex

createApp(App)
.use(store) // Usamos la tienda Vuex en toda la aplicación
.mount(‘#app’);
~~~

¿Cómo funciona Vuex como Singleton?
- Acceso global: Todos los componentes de la aplicación usan this.$store para acceder al estado global, a las mutaciones, acciones y getters definidos en la tienda. No se crean instancias adicionales, ya que this.$store se refiere siempre a la misma instancia.
- Centralización de la lógica de estado: La tienda Vuex centraliza todo el estado compartido y la lógica de modificación de este estado, asegurando que toda la aplicación esté sincronizada con esa única fuente de verdad.

Por lo tanto, Vuex actúa como un Singleton, ya que proporciona una instancia global de la tienda de estado que puede ser accedida y modificada por todos los componentes, asegurando que solo haya una instancia de esa tienda en toda la aplicación.

19
Q

Detectar violaciones del SRP

A

Detectar violaciones del SRP:

Saber si estamos respetando o no el principio de responsabilidad única puede ser ambiguo. A continuación, algunas señales que indican una posible violación del SRP:

  • Nombre demasiado genérico: Un nombre genérico puede derivar en un God Object, es decir, un objeto que realiza demasiadas tareas.
  • Cambios frecuentes en la clase: Si un elevado porcentaje de cambios afecta a la misma clase, puede ser señal de que esa clase tiene demasiadas responsabilidades.
  • La clase involucra múltiples capas de la arquitectura: Si una clase realiza tareas como acceder a la capa de persistencia o notificar al usuario, además de implementar la lógica de negocio, está violando el SRP.
  • Número alto de imports: Un alto número de imports podría ser un indicio de violación del SRP, aunque no es una regla definitiva.
  • Exceso de métodos públicos: Si una clase tiene una API con muchos métodos públicos, puede ser síntoma de que tiene demasiadas responsabilidades.
  • Excesivo número de líneas de código: Si una clase tiene demasiadas líneas de código, puede ser un indicio de que está asumiendo varias responsabilidades.
20
Q

OCP - Principio Open-Closed

A

Principio Open-Closed (Abierto/Cerrado)
*“Todas las entidades software deberían estar * ABIERTAS A EXTENSION Y CERRADAS A MODIFICACION” – Bertrand Meyer

El principio Open-Closed recomienda que, al introducir nuevos comportamientos en sistemas existentes, no se modifiquen los componentes antiguos, sino que se creen nuevos componentes. Esto se debe a que modificar clases o componentes que ya están siendo utilizados en otros lugares puede alterar su comportamiento y provocar efectos indeseados.

Ventajas:
- Mejora la estabilidad de la aplicación, evitando cambios frecuentes en las clases existentes.
- Reduce la fragilidad de las cadenas de dependencia, ya que hay menos partes móviles que podrían generar problemas.
- Facilita la extensión futura de nuevas clases sin tener que modificar el código existente.

Este principio ayuda a mantener un sistema más robusto y escalable a medida que evoluciona.

21
Q

Aplicando el OCP

A

Aplicando el OCP (Open-Closed Principle)

Aunque el principio Open-Closed puede parecer contradictorio, existen varias formas de aplicarlo, dependiendo del contexto. Algunas técnicas comunes incluyen:

  • Herencia y Composición: Se pueden usar mecanismos de extensión, como la herencia o la composición, para modificar el comportamiento de las clases sin alterar su código original. Sin embargo, se recomienda priorizar la composición sobre la herencia.

Ejemplo práctico: Desacoplar un elemento de infraestructura de la capa de dominio.

Imagina un sistema de gestión de tareas con una clase TodoService que realiza una petición HTTP a una API REST para obtener las tareas. El código podría verse así:

```javascript
const axios = require(‘axios’);

class TodoExternalService {
requestTodoItems(callback) {
const url = ‘https://jsonplaceholder.typicode.com/todos/’;
axios.get(url).then(callback);
}
}

new TodoExternalService().requestTodoItems(response => console.log(response.data));
~~~

Problemas en este ejemplo:
1. Acoplamiento: La clase TodoExternalService depende de una librería externa (axios).
2. Violación del OCP: Si quisiéramos reemplazar axios por fetch u otra librería, tendríamos que modificar el código existente.

Solución: Usar un patrón adaptador para desacoplar la librería y aplicar el principio Open-Closed, permitiendo cambiar la implementación sin modificar el código original.

Este enfoque ayuda a mantener el sistema flexible y fácil de extender sin modificar el comportamiento existente.

22
Q

Herencia

A

La herencia permite que una clase (subclase) herede las propiedades y métodos de otra clase (superclase).

  • Relación: “Es un”. Por ejemplo, un Perro es un Animal.
  • Ventajas:
    • Reutilización de código.
    • Extensibilidad (agregar o sobrescribir métodos en la subclase).
  • Desventajas:
    • Acoplamiento fuerte entre la subclase y la superclase.
    • Puede generar una jerarquía rígida que limita la flexibilidad.
    • Problemas con la herencia múltiple en lenguajes que la permiten.

Ejemplo:
```javascript
class Animal {
eat() {
console.log(“Eating…”);
}
}

class Dog extends Animal {
bark() {
console.log(“Barking…”);
}
}

const dog = new Dog();
dog.eat(); // Heredado de Animal
dog.bark(); // Definido en Dog
~~~

23
Q

Composición

A

La composición permite que una clase incluya instancias de otras clases como propiedades, delegando comportamientos a estos objetos.

  • Relación: “Tiene un”. Por ejemplo, un Coche tiene un Motor.
  • Ventajas:
    • Menor acoplamiento entre clases.
    • Mayor flexibilidad en el diseño del sistema.
    • Evita los problemas de la herencia múltiple.
  • Desventajas:
    • Requiere más trabajo para delegar comportamientos a objetos internos.
    • Puede ser más difícil de entender si no se usa correctamente.

Ejemplo:
```javascript
class Engine {
start() {
console.log(“Engine starting…”);
}
}

class Car {
constructor() {
this.engine = new Engine(); // Composición: Car tiene un Engine
}

drive() {
this.engine.start();
console.log(“Driving…”);
}
}

const myCar = new Car();
myCar.drive(); // Comportamiento delegando a Engine
~~~

24
Q

ISP - Principio de segregación de
la interfaz

A

“Los clientes no deberían estar obligados a depender de interfaces que no utilicen.”
– Robert C. Martin

El principio de segregación de la interfaz establece que una clase no debe depender de métodos o propiedades que no necesita. Las interfaces deben diseñarse pensando en las clases que las usarán (principio de clase cliente), no en implementaciones existentes.

En lenguajes como JavaScript, donde no hay interfaces, se confía en el buen uso del duck typing: la validez de un objeto depende de sus métodos y propiedades, no de su jerarquía de clases.

Con TypeScript, las interfaces son herramientas poderosas para definir contratos claros y aplicar este principio de manera efectiva.

25
Q

Principio de Inversión de Dependencias (DIP)

A

El Principio de Inversión de Dependencias, propuesto por Robert C. Martin en 1995, establece que:
1. Los módulos de alto nivel no deben depender de módulos de bajo nivel.
2. Ambos deben depender de abstracciones.
3. Las abstracciones no deben depender de concreciones; en cambio, los detalles deben depender de abstracciones.

Esto significa que las capas superiores de un sistema (módulos de alto nivel) no deberían estar acopladas directamente a las capas inferiores (módulos de bajo nivel). En su lugar, ambos deben conectarse mediante interfaces o abstracciones. De este modo, se reduce la rigidez y se aumenta la flexibilidad y capacidad de mantenimiento del sistema.

El DIP fomenta un diseño desacoplado, permitiendo que los cambios en los detalles no afecten la lógica principal del sistema, logrando así sistemas más robustos y escalables.

26
Q

Principio de Responsabilidad Única (SRP - Single Responsibility Principle) EN VUE

A

¿Qué significa SRP en Vue?
Cada componente, archivo o función debe encargarse de una única tarea específica, lo que lo hace más fácil de entender, mantener y probar. Si un componente o función realiza más de una tarea, es más probable que las modificaciones en una parte del código afecten negativamente a otras.

Cómo identificar violaciones al SRP
1. Componentes monolíticos:
- Un componente Vue que maneja demasiadas responsabilidades (como lógica de negocio, lógica de UI y llamadas a APIs en el mismo archivo).
- Ejemplo: Un componente UserProfile que muestra información del usuario, realiza validaciones de formularios y realiza actualizaciones a la base de datos.

  1. Funciones gigantescas dentro de los métodos:
    • Métodos con múltiples pasos no relacionados. Por ejemplo, un método submitForm que valida datos, procesa información y realiza la llamada a la API.
  2. Propiedades y estados no relacionados:
    • Componentes que tienen data o props innecesariamente variados y poco cohesionados.

Ventajas de respetar el SRP:
1. Mantenibilidad: Modificar un aspecto del sistema es más sencillo y menos propenso a errores.
2. Reutilización: Los componentes y funciones pueden ser usados en diferentes partes del proyecto.
3. Pruebas más fáciles: Con una responsabilidad clara, escribir pruebas unitarias es más simple.
4. Colaboración eficiente: Equipos pueden trabajar en diferentes responsabilidades sin conflictos.

26
Q

Principio de Abierto/Cerrado (OCP) en Vue - Concepto Básico de Slots

A

Un slot en Vue es una herramienta poderosa para crear componentes dinámicos y reutilizables. Los slots permiten insertar contenido personalizado dentro de un componente hijo desde el componente padre. Esto es especialmente útil para respetar principios como el de Abierto/Cerrado (OCP), ya que te permiten extender un componente sin modificar su código base.

Concepto Básico de Slots

Un slot es un espacio reservado en un componente hijo que puede ser llenado con contenido dinámico definido por el componente padre.

Ejemplo básico:

Componente hijo (Card.vue):
```vue

<template>
<div>
<slot></slot> <!-- Aquí va el contenido proporcionado por el padre -->
</div>
</template>

**Componente padre:**
```vue
<template>
  <Card>
    <h1>Contenido personalizado</h1>
    <p>Esto es dinámico y viene del componente padre.</p>
  </Card>
</template>

<script>
import Card from './Card.vue';

export default {
  components: { Card },
};
</script>

Resultado en la interfaz:
```html

<div>
<h1>Contenido personalizado</h1>
<p>Esto es dinámico y viene del componente padre.</p>
</div>

---

**Slots Nombrados**
A veces necesitas colocar contenido en lugares específicos del componente hijo (por ejemplo, encabezado, cuerpo o pie de página). Para esto, puedes usar **slots nombrados**.

**Ejemplo:**
**Componente hijo (`Card.vue`):**
```vue
<template>
  <div class="card">
    <div class="header">
      <slot name="header"></slot> <!-- Slot para el encabezado -->
    </div>
    <div class="body">
      <slot></slot> <!-- Slot para contenido por defecto -->
    </div>
    <div class="footer">
      <slot name="footer"></slot> <!-- Slot para el pie -->
    </div>
  </div>
</template>

Componente padre:
```vue

<template>
<Card>
<template #header>
<h1>Encabezado Personalizado</h1>
</template>
<p>Este es el contenido del cuerpo.</p>
<template #footer>
<button>Botón Personalizado</button>
</template>
</Card>
</template>

**Resultado en la interfaz:**
```html
<div class="card">
  <div class="header">
    <h1>Encabezado Personalizado</h1>
  </div>
  <div class="body">
    <p>Este es el contenido del cuerpo.</p>
  </div>
  <div class="footer">
    <button>Botón Personalizado</button>
  </div>
</div>

Slots con Contenido Predeterminado
A veces, puede ser útil definir contenido por defecto en un slot, para que si el componente padre no proporciona contenido personalizado, el componente hijo tenga algo que mostrar.

Ejemplo:
Componente hijo (Card.vue):
```vue

<template>
<div>
<slot>Encabezado por defecto</slot>
<slot>Contenido por defecto</slot>
<slot>Pie por defecto</slot>
</div>
</template>

**Componente padre sin personalización:**
```vue
<template>
  <Card />
</template>

Resultado en la interfaz:
```html

<div>
<div>Encabezado por defecto</div>
<div>Contenido por defecto</div>
<div>Pie por defecto</div>
</div>

**Componente padre con personalización:**
```vue
<template>
  <Card>
    <template #header>
      <h1>Encabezado Personalizado</h1>
    </template>
    <p>Contenido específico</p>
  </Card>
</template>

Resultado en la interfaz:
```html

<div>
<div>
<h1>Encabezado Personalizado</h1>
</div>
<div>Contenido específico</div>
<div>Pie por defecto</div>
</div>

~~~

Ventajas de usar slots:

  1. Flexibilidad:
    • Los slots permiten a los desarrolladores adaptar un componente a diferentes contextos sin modificar su código base.
  2. Reutilización:
    • Puedes usar un mismo componente en múltiples escenarios, cambiando solo el contenido dinámico.
  3. Cumplimiento del Principio Abierto/Cerrado:
    • Los slots hacen que los componentes estén cerrados para la modificación (el código base no cambia) pero abiertos para la extensión (contenido dinámico puede ser agregado desde el exterior).
  4. Mantenimiento:
    • Si el diseño o comportamiento de un slot cambia, solo afecta a los padres que lo usan, no al componente base.

Conclusión:
Un slot en Vue es una herramienta clave para crear componentes flexibles y extensibles. Usarlos correctamente asegura un diseño modular, facilita la personalización y mejora la mantenibilidad del código. ¿Te gustaría practicar con un caso concreto? 😊

27
Q

Principio de Abierto/Cerrado (OCP) en Vue

A

Principio de Abierto/Cerrado (OCP) en Vue

Definición:
El Principio de Abierto/Cerrado (OCP) establece que una entidad de software (como una clase, módulo o componente) debe estar abierta para la extensión pero cerrada para la modificación. Esto significa que debemos ser capaces de añadir nuevas funcionalidades sin modificar el código existente.

Contexto en Vue:
En aplicaciones Vue, el OCP se aplica al diseñar componentes o módulos de manera que puedan ser ampliados o personalizados sin necesidad de cambiar el código base. Esto se puede lograr mediante:
- Props para personalización.
- Slots para contenido dinámico.
- Mixins o composables para reutilización de lógica.
- Plugins o configuraciones externas para funcionalidades adicionales.

Ejemplo Real: Un sistema de tarjetas (Cards)

Caso inicial:
Tienes un componente Card que muestra contenido básico (título y descripción). Ahora, un cliente quiere añadir:
1. Un botón personalizado.
2. Un icono adicional al encabezado.

Modificar directamente el componente puede romper funcionalidades existentes o hacerlo más difícil de mantener.

Mal diseño (violación del OCP):
El componente Card está rígido porque todas las personalizaciones se manejan dentro del mismo código. Si hay más requisitos en el futuro, debes modificar el componente principal cada vez.

```vue

<template>
<div>
<h2>{{ title }}</h2>
<p>{{ description }}</p>
<button @click="onAction">Click Me</button>
</div>
</template>

<script>
export default {
  props: {
    title: String,
    description: String,
    onAction: Function,
  },
};
</script>
Este diseño no permite fácilmente personalizar el botón o añadir elementos nuevos al encabezado. Cada cambio futuro requiere modificar el componente base.

---

**Buen diseño (respetando el OCP):**
El componente `Card` es abierto para la extensión pero cerrado para la modificación gracias al uso de **slots**. Esto permite a los desarrolladores añadir funcionalidad personalizada sin tocar el código base del componente.

---

**Componente base `Card.vue`:**
```vue
<template>
  <div class="card">
    <div class="card-header">
      <slot name="header">
        <h2>{{ title }}</h2>
      </slot>
    </div>
    <div class="card-body">
      <p>{{ description }}</p>
      <slot name="body"></slot>
    </div>
    <div class="card-footer">
      <slot name="footer">
        <button @click="onAction">Default Action</button>
      </slot>
    </div>
  </div>
</template>

<script>
export default {
  props: {
    title: String,
    description: String,
    onAction: Function,
  },
};
</script>

Extensiones sin modificar el componente base:

  1. Añadir un icono al encabezado:
    Puedes proporcionar un slot personalizado para el encabezado.

```vue

<Card>
<template #header>
<h2><i></i> Welcome</h2>
</template>
</Card>

---

2. **Reemplazar el botón con uno más avanzado:**
   Usa el slot `footer` para introducir un botón personalizado.

```vue
<Card
  title="Actions"
  description="This card has a custom button."
  :onAction="() => alert('Default action')"
>
  <template #footer>
    <button class="custom-button" @click="customAction">Custom Button</button>
  </template>
</Card>

<script>
export default {
  methods: {
    customAction() {
      alert("Custom action executed!");
    },
  },
};
</script>
  1. Añadir contenido dinámico al cuerpo:
    Usa el slot body para contenido adicional.

```vue

<Card>
<template #body>
<ul>
<li>Extra item 1</li>
<li>Extra item 2</li>
</ul>
</template>
</Card>

~~~

Ventajas del diseño final:

  1. Extensible:
    • Los desarrolladores pueden añadir o cambiar funcionalidades usando slots y no tocando el componente base.
    • Nuevas personalizaciones (como añadir un ícono, cambiar el botón o incluir contenido dinámico) no requieren modificaciones directas al código existente.
  2. Reutilizable:
    • El componente Card puede ser usado en diferentes contextos con personalizaciones específicas para cada caso.
  3. Escalable:
    • Si necesitas añadir más slots o funcionalidades, puedes hacerlo sin afectar las implementaciones actuales.
  4. Cumple con OCP:
    • El componente está cerrado para modificación (no cambias el código base) y abierto para extensión (slots y props permiten ampliarlo).

¿Quieres que detalle cómo podrías probar este diseño o aplicarlo a otro ejemplo más complejo? 😊

28
Q

Principio de Sustitución de Liskov (LSP) en Vue

A

Definición:
El Principio de Sustitución de Liskov (LSP) establece que una subclase o implementación derivada debe poder ser sustituida por su clase base sin alterar el comportamiento correcto del programa.

En Vue, esto se traduce a diseñar componentes y funcionalidades de manera que las extensiones, variaciones o personalizaciones no rompan las expectativas del uso original.

Aplicación del LSP en Vue
1. Diseñar componentes reutilizables que mantengan una API coherente.
2. Asegurar que los componentes hijos o extendidos respeten el contrato (propiedades, eventos, slots) definido por el componente padre.
3. Evitar dependencias fuertes o modificaciones que cambien la lógica base.

Violación del LSP
Una violación ocurre si las subclases o variantes no cumplen con el contrato establecido por el componente base.

Ejemplo:
1. Cambias el evento @click en el botón secundario para emitir un evento diferente (@secondary-click).
2. El padre que espera el comportamiento estándar (@click) se rompe porque el evento ya no es consistente.

```vue

<template>
<BaseButton buttonClass="btn-secondary" @click="$emit('secondary-click')">
<slot>{{ label }}</slot>
</BaseButton>
</template>

~~~

En este caso, el botón secundario no puede ser sustituido por el botón base porque altera el comportamiento esperado. Esto rompe el Principio de Sustitución de Liskov.

Principios Clave para Cumplir el LSP:
1. Consistencia de API:
- Las subclases o variantes deben mantener la misma API y los mismos contratos que la clase base.
- En Vue, esto incluye props, eventos y slots.

  1. Extender, no reemplazar:
    • Las variantes pueden añadir nuevas funcionalidades o estilos, pero no deben cambiar el comportamiento base.
  2. Pruebas:
    • Asegúrate de que cualquier componente derivado pueda sustituir al componente base en escenarios existentes sin romper la aplicación.

Conclusión:
En Vue, el LSP fomenta el diseño de componentes coherentes y reutilizables. Respetar este principio asegura que las variantes de un componente base se comporten de manera predecible, lo que facilita la extensión y el mantenimiento del código.

¿Te gustaría que exploremos otro ejemplo, como formularios o listas dinámicas? 😊

29
Q

Principio de Segregación de Interfaces (ISP) en Vue con TypeScript

A

Principio de Segregación de Interfaces (ISP) en Vue con TypeScript

Definición:
El Principio de Segregación de Interfaces (ISP) establece que los clientes (consumidores) de una interfaz no deben estar obligados a depender de métodos o propiedades que no usan. En otras palabras:
- Es mejor tener interfaces pequeñas y específicas en lugar de interfaces grandes y genéricas.

Aplicación del ISP en Vue con TypeScript

En Vue, interfaces en TypeScript se utilizan para definir la estructura de los datos o las props. Aplicar el ISP en Vue significa dividir interfaces en partes más específicas, asegurando que cada componente o función solo dependa de lo que realmente necesita.

Ejemplo práctico: Componente de gráficos

Contexto:
Tienes un componente de gráfico (Chart) que admite múltiples tipos de gráficos: de barras, de líneas y de pastel. Cada tipo de gráfico tiene diferentes props:
- Los gráficos de barras necesitan barWidth.
- Los gráficos de líneas necesitan lineThickness.
- Los gráficos de pastel necesitan radius.

Un diseño incorrecto incluiría todas las props en una sola interfaz, obligando a los gráficos a depender de propiedades que no usan.

Diseño incorrecto (violación del ISP):
Una única interfaz genérica que incluye todas las propiedades.

```typescript
interface ChartProps {
type: “bar” | “line” | “pie”;
data: number[];
barWidth?: number;
lineThickness?: number;
radius?: number;
}
~~~

El componente utiliza esta interfaz:

```vue

<template>
<div>
<!-- Renderiza el gráfico según el tipo -->
<p>{{ type }} chart rendered!</p>
</div>
</template>

<script>
import { defineComponent } from "vue";

export default defineComponent({
  props: {
    type: { type: String, required: true },
    data: { type: Array as () => number[], required: true },
    barWidth: Number,
    lineThickness: Number,
    radius: Number,
  },
});
</script>
**Problemas:**
- Los gráficos de líneas y pasteles pueden incluir `barWidth` aunque no lo necesiten.
- Los consumidores de esta interfaz estarán expuestos a props irrelevantes, lo que puede causar confusión o errores.

---

**Diseño correcto (respetando el ISP):**
Divide la interfaz en partes específicas según el tipo de gráfico.

1. **Interfaces específicas para cada tipo de gráfico:**
```typescript
interface BaseChartProps {
  data: number[];
}

interface BarChartProps extends BaseChartProps {
  barWidth: number;
}

interface LineChartProps extends BaseChartProps {
  lineThickness: number;
}

interface PieChartProps extends BaseChartProps {
  radius: number;
}
  1. Prop props con discriminación de tipos:
    En lugar de depender de una interfaz única, usa un tipo discriminado para combinar las interfaces específicas.

```typescript
type ChartProps =
| ({ type: “bar” } & BarChartProps)
| ({ type: “line” } & LineChartProps)
| ({ type: “pie” } & PieChartProps);
~~~

  1. Componente Chart:
    El componente ahora depende solo de las props relevantes según el tipo de gráfico.

```vue

<template>
<div>
<!-- Renderiza según el tipo -->
<p>Bar chart with barWidth: {{ barWidth }}</p>
<p>Line chart with lineThickness: {{ lineThickness }}</p>
<p>Pie chart with radius: {{ radius }}</p>
</div>
</template>

<script>
import { defineComponent } from "vue";

type ChartProps =
  | ({ type: "bar" } & BarChartProps)
  | ({ type: "line" } & LineChartProps)
  | ({ type: "pie" } & PieChartProps);

export default defineComponent({
  props: {
    type: { type: String, required: true },
    data: { type: Array as () => number[], required: true },
    barWidth: Number,
    lineThickness: Number,
    radius: Number,
  } as unknown as ChartProps, // Asigna el tipo de discriminación
});
</script>

~~~

Ventajas de este diseño:
1. Componentes más claros:
Cada tipo de gráfico depende únicamente de las props que necesita. No hay confusión con props irrelevantes.

  1. Escalabilidad:
    Es fácil agregar un nuevo tipo de gráfico con sus propias props sin afectar los existentes.
  2. Evitación de errores:
    El uso de tipos discriminados asegura que los desarrolladores no puedan pasar props incorrectas según el tipo de gráfico.
  3. Pruebas más específicas:
    Puedes probar cada interfaz y funcionalidad de forma independiente.

Conclusión:
El ISP en Vue con TypeScript fomenta componentes modulares y claros. Al dividir las interfaces en partes específicas, los consumidores solo trabajan con lo que realmente necesitan, lo que mejora la mantenibilidad, la legibilidad y la robustez del código.

¿Quieres ver otro ejemplo, como formularios o manejo de eventos? 😊

30
Q

Principio de Inversión de Dependencias (DIP) en Vue con TypeScript

A

Principio de Inversión de Dependencias (DIP) en Vue con TypeScript

Definición:
El Principio de Inversión de Dependencias (DIP) establece que:
1. Los módulos de alto nivel no deben depender de módulos de bajo nivel; ambos deben depender de abstracciones.
2. Las abstracciones no deben depender de los detalles; los detalles deben depender de las abstracciones.

En Vue, puedes aplicar este principio al separar la lógica de negocio y las dependencias externas (como servicios o API) del componente de presentación. Esto mejora la modularidad, testabilidad y mantenibilidad del código.

Aplicación del DIP en Vue

Escenario: Gestión de tareas con API
Imagina que tienes un componente que:
- Muestra una lista de tareas.
- Permite crear nuevas tareas.
- Usa un servicio para interactuar con una API.

Un diseño acoplado haría que el componente dependa directamente de la implementación del servicio, lo que complica las pruebas y el mantenimiento.

Conclusión:
El Principio de Inversión de Dependencias (DIP) en Vue con TypeScript fomenta un diseño modular, desacoplado y flexible. Al depender de abstracciones, los componentes son más fáciles de probar, mantener y extender.

¿Te gustaría otro ejemplo, como trabajar con múltiples servicios o manejar datos en tiempo real? 😊

31
Q

Abstracción

A

Definición:
Es el proceso de ocultar los detalles complejos de implementación y mostrar solo las funcionalidades esenciales de un objeto o clase.

Metáfora:
Piensa en un coche: no necesitas saber cómo funciona el motor para conducir; solo necesitas el volante, los pedales y el cambio de marchas.

Ejemplo en TypeScript:

```typescript
abstract class Vehicle {
abstract start(): void;
abstract stop(): void;

// Método concreto común
honk(): void {
console.log(“Beep beep!”);
}
}

class Car extends Vehicle {
start(): void {
console.log(“Car started.”);
}
stop(): void {
console.log(“Car stopped.”);
}
}
~~~

Explicación:
La clase Vehicle define la idea general de un vehículo (abstrae), y sus detalles específicos se implementan en las clases concretas como Car.

32
Q

Encapsulación

A

Definición:
Es el concepto de restringir el acceso directo a algunos de los componentes de un objeto y usar métodos para interactuar con esos datos.

Metáfora:
En un cajero automático, no accedes directamente al dinero físico del banco; interactúas con la máquina a través de una interfaz (teclas, pantalla).

Ejemplo en TypeScript:

```typescript
class BankAccount {
private balance: number;

constructor(initialBalance: number) {
this.balance = initialBalance;
}

// Métodos públicos para acceder/modificar el balance
deposit(amount: number): void {
if (amount > 0) this.balance += amount;
}

withdraw(amount: number): void {
if (amount > 0 && amount <= this.balance) this.balance -= amount;
}

getBalance(): number {
return this.balance;
}
}
~~~

Explicación:
La variable balance está protegida (privada), y solo puede ser modificada o consultada a través de métodos públicos como deposit o getBalance.

33
Q

Polimorfismo

A

Definición:
Es la capacidad de que una misma función o método se comporte de manera diferente según el contexto o el tipo de objeto.

Metáfora:
Un interruptor de luz funciona igual para todas las bombillas, pero las bombillas (LED, incandescentes, etc.) se comportan de manera diferente según su diseño.

Ejemplo en TypeScript:

```typescript
class Animal {
makeSound(): void {
console.log(“Some generic sound”);
}
}

class Dog extends Animal {
makeSound(): void {
console.log(“Woof woof!”);
}
}

class Cat extends Animal {
makeSound(): void {
console.log(“Meow!”);
}
}

function playSound(animal: Animal): void {
animal.makeSound();
}

const dog = new Dog();
const cat = new Cat();

playSound(dog); // Output: Woof woof!
playSound(cat); // Output: Meow!
~~~

Explicación:
El método makeSound se comporta de forma distinta dependiendo de si el objeto es un Dog o un Cat, aunque ambos comparten la misma interfaz de Animal.