Salta al contenuto principale

04 - Polimorfismo: Flessibilità attraverso le Interfacce

Padroneggia il polimorfismo in OOP: polimorfismo di sottotipo, method overriding, interfacce multiple, duck typing e generics in TypeScript.

Cosa ne pensi di questo articolo?

In Breve

Padroneggia il polimorfismo in OOP: polimorfismo di sottotipo, method overriding, interfacce multiple, duck typing e generics in TypeScript.

Polimorfismo: Una Interfaccia, Molti Comportamenti

Il polimorfismo (dal greco "molte forme") è la capacità di oggetti di classi diverse di rispondere allo stesso messaggio in modi differenti. È uno dei pilastri della OOP che permette di scrivere codice flessibile, estensibile e manutenibile. Con il polimorfismo, scriviamo codice che lavora con interfacce astratte, non con implementazioni concrete.

💡 Concetti Chiave

  • Polimorfismo di sottotipo: Trattare oggetti di classi derivate come oggetti della classe base
  • Method overriding: Ridefinire comportamenti nelle sottoclassi
  • Duck typing: "Se cammina come un'anatra e starnazza come un'anatra, allora è un'anatra"
  • Interfacce: Contratti che garantiscono comportamenti comuni

Polimorfismo di Sottotipo

Il polimorfismo di sottotipo permette di usare un oggetto di una classe derivata ovunque sia atteso un oggetto della classe base. Questo è possibile grazie all'ereditarietà.

TypeScript - Polimorfismo Base
class Animal {{ '{' }}
  constructor(protected name: string) {{ '{' }}{{ '}' }}

  makeSound(): void {{ '{' }}
    console.log(`${{ '{' }}this.name{{ '}' }} fa un suono generico`);
  {{ '}' }}
{{ '}' }}

class Dog extends Animal {{ '{' }}
  // Override del metodo makeSound
  makeSound(): void {{ '{' }}
    console.log(`${{ '{' }}this.name{{ '}' }} abbaia: Woof!`);
  {{ '}' }}
{{ '}' }}

class Cat extends Animal {{ '{' }}
  makeSound(): void {{ '{' }}
    console.log(`${{ '{' }}this.name{{ '}' }} miagola: Meow!`);
  {{ '}' }}
{{ '}' }}

// Polimorfismo in azione: stesso metodo, comportamenti diversi
function playWithAnimal(animal: Animal): void {{ '{' }}
  animal.makeSound(); // Non sappiamo quale animale sia!
{{ '}' }}

const animals: Animal[] = [
  new Dog("Rex"),
  new Cat("Whiskers"),
  new Dog("Buddy")
];

animals.forEach(animal => playWithAnimal(animal));
// Output:
// Rex abbaia: Woof!
// Whiskers miagola: Meow!
// Buddy abbaia: Woof!

Il metodo playWithAnimal accetta un Animal, ma funziona correttamente con Dog e Cat perché sono sottotipi di Animal. Il metodo corretto viene chiamato a runtime in base al tipo reale dell'oggetto.

Polimorfismo con Interfacce

Le interfacce definiscono contratti che le classi devono rispettare. Classi completamente diverse possono implementare la stessa interfaccia, permettendo polimorfismo senza ereditarietà.

TypeScript - Interfacce Polimorfiche
interface Drawable {{ '{' }}
  draw(): void;
  getArea(): number;
{{ '}' }}

class Circle implements Drawable {{ '{' }}
  constructor(private radius: number) {{ '{' }}{{ '}' }}

  draw(): void {{ '{' }}
    console.log(`Disegno un cerchio di raggio ${{ '{' }}this.radius{{ '}' }}`);
  {{ '}' }}

  getArea(): number {{ '{' }}
    return Math.PI * this.radius ** 2;
  {{ '}' }}
{{ '}' }}

class Square implements Drawable {{ '{' }}
  constructor(private side: number) {{ '{' }}{{ '}' }}

  draw(): void {{ '{' }}
    console.log(`Disegno un quadrato di lato ${{ '{' }}this.side{{ '}' }}`);
  {{ '}' }}

  getArea(): number {{ '{' }}
    return this.side ** 2;
  {{ '}' }}
{{ '}' }}

class Triangle implements Drawable {{ '{' }}
  constructor(private base: number, private height: number) {{ '{' }}{{ '}' }}

  draw(): void {{ '{' }}
    console.log(`Disegno un triangolo ${{ '{' }}this.base{{ '}' }}x${{ '{' }}this.height{{ '}' }}`);
  {{ '}' }}

  getArea(): number {{ '{' }}
    return (this.base * this.height) / 2;
  {{ '}' }}
{{ '}' }}

// Funzione polimorfica: lavora con qualsiasi Drawable
function renderShape(shape: Drawable): void {{ '{' }}
  shape.draw();
  console.log(`Area: ${{ '{' }}shape.getArea().toFixed(2){{ '}' }}`);
{{ '}' }}

const shapes: Drawable[] = [
  new Circle(5),
  new Square(4),
  new Triangle(6, 3)
];

shapes.forEach(renderShape);
// Output:
// Disegno un cerchio di raggio 5
// Area: 78.54
// Disegno un quadrato di lato 4
// Area: 16.00
// Disegno un triangolo 6x3
// Area: 9.00

Method Overriding e Super

L'override permette alle sottoclassi di ridefinire metodi ereditati, mentre super consente di accedere all'implementazione della classe padre.

TypeScript - Override e Super
class Logger {{ '{' }}
  log(message: string): void {{ '{' }}
    console.log(`[LOG] ${{ '{' }}message{{ '}' }}`);
  {{ '}' }}
{{ '}' }}

class TimestampedLogger extends Logger {{ '{' }}
  // Override con funzionalità aggiuntiva
  override log(message: string): void {{ '{' }}
    const timestamp = new Date().toISOString();
    // Chiama il metodo del padre e aggiunge timestamp
    super.log(`[${{ '{' }}timestamp{{ '}' }}] ${{ '{' }}message{{ '}' }}`);
  {{ '}' }}
{{ '}' }}

class ColoredLogger extends Logger {{ '{' }}
  override log(message: string): void {{ '{' }}
    // Comportamento completamente diverso
    console.log(`\x1b[32m[LOG] ${{ '{' }}message{{ '}' }}\x1b[0m`);
  {{ '}' }}
{{ '}' }}

function logMessage(logger: Logger, msg: string): void {{ '{' }}
  logger.log(msg);
{{ '}' }}

const basicLogger = new Logger();
const timeLogger = new TimestampedLogger();
const colorLogger = new ColoredLogger();

logMessage(basicLogger, "Messaggio base");
// [LOG] Messaggio base

logMessage(timeLogger, "Messaggio con timestamp");
// [LOG] [2024-12-26T10:30:00.000Z] Messaggio con timestamp

logMessage(colorLogger, "Messaggio colorato");
// [LOG] Messaggio colorato (in verde)

Duck Typing in TypeScript

TypeScript usa structural typing (duck typing): se un oggetto ha le proprietà e i metodi richiesti, è compatibile con un tipo, anche senza implementare esplicitamente un'interfaccia.

TypeScript - Duck Typing
interface Quackable {{ '{' }}
  quack(): void;
{{ '}' }}

class Duck {{ '{' }}
  quack(): void {{ '{' }}
    console.log("Quack!");
  {{ '}' }}
{{ '}' }}

class Person {{ '{' }}
  name: string;

  constructor(name: string) {{ '{' }}
    this.name = name;
  {{ '}' }}

  // Person ha un metodo quack, quindi è compatibile con Quackable!
  quack(): void {{ '{' }}
    console.log(`${{ '{' }}this.name{{ '}' }} imita un'anatra: Quack!`);
  {{ '}' }}
{{ '}' }}

// Accetta qualsiasi oggetto con metodo quack()
function makeItQuack(quacker: Quackable): void {{ '{' }}
  quacker.quack();
{{ '}' }}

const duck = new Duck();
const person = new Person("Mario");

makeItQuack(duck);    // "Quack!"
makeItQuack(person);  // "Mario imita un'anatra: Quack!"

// Anche oggetti literal funzionano!
makeItQuack({{ '{' }}
  quack: () => console.log("Oggetto anonimo: Quack!")
{{ '}' }});
// "Oggetto anonimo: Quack!"

Polimorfismo Parametrico (Generics)

I generics permettono di scrivere codice che funziona con tipi diversi mantenendo type safety. È una forma di polimorfismo a tempo di compilazione.

TypeScript - Generics Polimorfici
// Repository generico che funziona con qualsiasi tipo
class Repository<T> {{ '{' }}
  private items: T[] = [];

  add(item: T): void {{ '{' }}
    this.items.push(item);
  {{ '}' }}

  findById(id: number): T | undefined {{ '{' }}
    return this.items[id];
  {{ '}' }}

  getAll(): T[] {{ '{' }}
    return [...this.items];
  {{ '}' }}

  filter(predicate: (item: T) => boolean): T[] {{ '{' }}
    return this.items.filter(predicate);
  {{ '}' }}
{{ '}' }}

interface User {{ '{' }}
  id: number;
  name: string;
{{ '}' }}

interface Product {{ '{' }}
  id: number;
  title: string;
  price: number;
{{ '}' }}

// Stesso codice, tipi diversi
const userRepo = new Repository<User>();
userRepo.add({{ '{' }} id: 1, name: "Alice" {{ '}' }});
userRepo.add({{ '{' }} id: 2, name: "Bob" {{ '}' }});

const productRepo = new Repository<Product>();
productRepo.add({{ '{' }} id: 1, title: "Laptop", price: 999 {{ '}' }});
productRepo.add({{ '{' }} id: 2, title: "Mouse", price: 25 {{ '}' }});

// Type-safe: TypeScript sa che users è User[]
const users = userRepo.getAll();
const expensiveProducts = productRepo.filter(p => p.price > 500);

Polimorfismo nei Design Patterns

Il polimorfismo è fondamentale in molti design pattern. Vediamo lo Strategy Pattern:

Strategy Pattern con Polimorfismo
// Interfaccia comune per tutte le strategie
interface SortStrategy {{ '{' }}
  sort(data: number[]): number[];
{{ '}' }}

class BubbleSort implements SortStrategy {{ '{' }}
  sort(data: number[]): number[] {{ '{' }}
    console.log("Ordinamento con Bubble Sort");
    const arr = [...data];
    // Implementazione bubble sort (semplificata)
    for (let i = 0; i < arr.length; i++) {{ '{' }}
      for (let j = 0; j < arr.length - i - 1; j++) {{ '{' }}
        if (arr[j] > arr[j + 1]) {{ '{' }}
          [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
        {{ '}' }}
      {{ '}' }}
    {{ '}' }}
    return arr;
  {{ '}' }}
{{ '}' }}

class QuickSort implements SortStrategy {{ '{' }}
  sort(data: number[]): number[] {{ '{' }}
    console.log("Ordinamento con Quick Sort");
    // Implementazione quick sort (semplificata con sort nativo)
    return [...data].sort((a, b) => a - b);
  {{ '}' }}
{{ '}' }}

class DataSorter {{ '{' }}
  private strategy!: SortStrategy;

  // Cambia strategia a runtime (polimorfismo!)
  setStrategy(strategy: SortStrategy): void {{ '{' }}
    this.strategy = strategy;
  {{ '}' }}

  sortData(data: number[]): number[] {{ '{' }}
    if (!this.strategy) {{ '{' }}
      throw new Error("Strategia non impostata");
    {{ '}' }}
    return this.strategy.sort(data);
  {{ '}' }}
{{ '}' }}

const sorter = new DataSorter();
const data = [5, 2, 8, 1, 9];

sorter.setStrategy(new BubbleSort());
console.log(sorter.sortData(data));
// "Ordinamento con Bubble Sort"
// [1, 2, 5, 8, 9]

sorter.setStrategy(new QuickSort());
console.log(sorter.sortData(data));
// "Ordinamento con Quick Sort"
// [1, 2, 5, 8, 9]

Vantaggi del Polimorfismo

✅ Benefici

  • Estensibilità: Aggiungi nuove classi senza modificare codice esistente
  • Manutenibilità: Codice più pulito e organizzato
  • Riutilizzo: Stesse funzioni per tipi diversi
  • Testabilità: Mock e stub tramite interfacce
  • Flessibilità: Cambia comportamenti a runtime

Conclusione

Il polimorfismo è il meccanismo che rende la OOP davvero potente. Ci permette di scrivere codice generico che lavora con astrazioni, non con implementazioni concrete. Che sia attraverso ereditarietà, interfacce, duck typing o generics, il polimorfismo rende il software flessibile, estensibile e manutenibile.

🎯 Punti Chiave

  • Polimorfismo = "stessa interfaccia, comportamenti diversi"
  • Override ridefinisce metodi nelle sottoclassi
  • Interfacce permettono polimorfismo senza ereditarietà
  • TypeScript usa structural typing (duck typing)
  • Generics = polimorfismo type-safe a compile-time

Domande frequenti

Cos'è il polimorfismo in programmazione?

Il polimorfismo è la capacità di oggetti di classi diverse di rispondere allo stesso messaggio in modi differenti.

Qual è lo scopo principale del polimorfismo nella programmazione orientata agli oggetti (OOP)?

Lo scopo principale del polimorfismo nella OOP è permettere la scrittura di codice flessibile, estensibile e manutenibile.

Come funziona il polimorfismo di sottotipo?

Il polimorfismo di sottotipo permette l'uso di un oggetto di una classe derivata ovunque sia atteso un oggetto della classe base, grazie all'ereditarietà.

Che cosa sono i generics e come contribuiscono al polimorfismo?

I generics sono una forma di polimorfismo a tempo di compilazione che permette la scrittura di codice che funziona con tipi diversi mantenendo la sicurezza dei tipi.

Fonti e approfondimenti

Prova Questi Strumenti

Strumenti gratuiti correlati a questo argomento.

Commenti

Caricamento commenti...

Accedi per commentare

Discuti questo articolo nella community

Unisciti al gruppo Telegram per discutere con altri sviluppatori, fare domande e condividere le tue esperienze.

Ti è piaciuto questo articolo?

Esplora altri contenuti sul blog o scopri i miei progetti