Ereditarietà a carattere ontologico

Una delle motivazioni con cui il concetto di ereditarietà viene presentato (e spesso l'unico genere di esempio che lo riguarda) ha a che fare con entità che, nella realtà, sono dal punto di vista ontologico in una relazione di più specifico/meno specifico o di "è un" (is a), ossia in cui l'ereditarietà è vista dal punto di vista ontologico.

Uno degli esempi più classici è tratto dalla geometria e si basa sul fatto che le varie figure piane (cerchi, rettangoli, quadrati…) sono tutte esempi di un'entità più generica che è, appunto, la figura piana. In tale contesto si fa spesso l'esempio del quadrato come sottotipo di rettangolo (in simboli quadrato $\prec$ rettangolo): tutti i quadrati, infatti, sono a ben vedere dei rettangoli (che hanno la base uguale all'altezza), ma non è vero il viceversa.

Nell'ambito dei tipi di dato astratti però, abbiamo visto che l'estensione è di solito sviluppata nella direzione opposta: il rettangolo ha infatti un attributo in più (l'altezza) del quadrato (che è identificato dalla sola base).

Risulta molto naturale infatti scrivere il tipo Square con un solo attributo

public class Square {
  private int base;

  public Square(int base) {
    this.base = base;
  }

  public int base() {
    return this.base;
  }

  public void base(int base) {
    this.base = base;
  }

  @Override
  public String toString() {
    return String.format("Square, base = %d", base);
  }
}

[sorgente]

e definire per estensione il tipo Rectangle

public class Rectangle extends Square {
  private int height;

  public Rectangle(int base, int height) {
    super(base);
    this.height = height;
  }

  public int height() {
    return this.height;
  }

  public void height(int height) {
    this.height = height;
  }

  @Override
  public String toString() {
    return String.format("Rectangle: base = %d, height = %d", base(), height);
  }
}

[sorgente]

Ma in questo modo la relazione di sottotipo Rectangle $\prec$ Square è l'opposto della relazione ontologica per cui quadrato $\prec$ rettangolo!

Si potrebbe certamente pensare che questa inversione sia fittizia e sia legata alla scelta implementativa; si potrebbe infatti pensare che è possibile ribaltare la direzione in cui un tipo estende l'altro pur di sovrascrivere i metodi mutazionali del rettangolo in modo che, nel caso del quadrato, adeguino la base qualora venga cambiata l'altezza.

Si potrebbe cioè pensare di porre Rectangle come supertipo

public class Rectangle {
  private int base, height;

  public Rectangle(int base, int height) {
    this.base = base;
    this.height = height;
  }

  public int base() {
    return this.base;
  }

  public void base(int base) {
    this.base = base;
  }

  public int height() {
    return this.height;
  }

  public void height(int height) {
    this.height = height;
  }

  @Override
  public String toString() {
    return String.format("WrongFigures.Rectangle: base = %d, height = %d", base(), height);
  }
}

[sorgente]

con due metodi mutazionali per base e altezza, e implementare Square per estensione, facendo attenzione a sovrascrivere i metodi mutazionali baseheight in modo che mantengano coerenti base e altezza

public class Square extends Rectangle {

  public Square(int base) {
    super(base, base);
  }

  public void base(int base) {
    super.base(base);
    super.height(base);
  }

  public void height(int height) {
    super.base(height);
    super.height(height);
  }

  @Override
  public String toString() {
    return String.format("Square, base = %d", base());
  }
}

[sorgente]

Il principio di sostituzione

Immaginiamo ora di voler costruire un istogramma di rettangoli in cui i rettangoli aggiunti all'istogramma sono mantenuti in ordine crescente d'altezza. Supponiamo inoltre che per ragioni "tipografiche" sia sensato alterare la base dei rettangoli anche una volta che sono parte dell'istogramma (ad esempio, per restringerne l'ampiezza complessiva).

public class Histogram {

  List<Rectangle> rectangles = new LinkedList<>();

  public void add(Rectangle r) {
    int i;
    for (i = 0; i < rectangles.size(); i++) if (rectangles.get(i).height() > r.height()) break;
    rectangles.add(i, r);
  }

  public void changeBase(Rectangle o, int base) throws NoSuchElementException {
    final int idx = rectangles.indexOf(o);
    if (idx != -1) rectangles.get(idx).base(base);
    else throw new NoSuchElementException();
  }

  @Override
  public String toString() {
    return rectangles.toString();
  }
}

[sorgente]

Evidentemente, ciò che accade se sostituissimo il sottotipo Square al posto di Rectangle il metodo changeBase avrebbe l'effetto (inatteso) di modificare anche l'altezza, non preservando il contratto di Histogram. Si consideri il seguente client

public class MainSR {

  public static void main(String[] args) {

    Histogram hist1 = new Histogram();
    Rectangle r1 = new Rectangle(4, 4), r2 = new Rectangle(3, 3);
    hist1.add(r1);
    hist1.add(r2);
    System.out.println(hist1);
    hist1.changeBase(r2, 6);
    System.out.println(hist1);

    Histogram hist2 = new Histogram();
    Square s1 = new Square(4), s2 = new Square(3);
    hist2.add(s1);
    hist2.add(s2);
    System.out.println(hist2);
    hist2.changeBase(s2, 6);
    System.out.println(hist2);
  }
}

[sorgente]

L'uso in hist1 mostra il comportamento inteso, ma non è così per quello di hist2, come risulta dall'esecuzione del codice, che genera l'output

['[WrongFigures.Rectangle: base = 3, height = 3, WrongFigures.Rectangle: base = 4, height = 4]',
 '[WrongFigures.Rectangle: base = 6, height = 3, WrongFigures.Rectangle: base = 4, height = 4]',
 '[Square, base = 3, Square, base = 4]',
 '[Square, base = 6, Square, base = 4]']

Come si nota, infatti, i rettangoli restano ordinati per altezza anche dopo il cambiamento della base (prime due righe), ma così non è per i quadarti, che, evidentemente, dopo il cambiamento di base hanno anche le altezze in ordine decrescente (ultime due righe).