Clean code – oggetti vs. strutture dati

Il motivo reale per cui manteniamo le variabili (proprietà) della nostra classe private, dovrebbe essere quello di rendere la classe stessa il più indipendente possibile  da qualsiasi altra entità esterna.

Perché allora quasi in automatico creiamo dei metodi accessori getter e setter, che in realtà le rendono pubbliche ???? .. la risposta è dentro di noi .. ma è sbagliata o almeno era cosi per me. Prendiamo spunto da questi due listati:

public class Point {
    public double x;
    public double y;
}
public interface Point {
    double getX();
    double getY();
    void setCartesian(double x, double y);
    double getR();
    double getTheta();
    void setPolar(double r, double theta);
}

Nel listato di destra non abbiamo modo di sapere se il punto verrà rappresentato in coordinate angolari o cartesiane o nessuna delle due.  Differentemente il listato di sinistra invece ci svela che la rappresentazione sarà cartesiana in quanto l’implementazione viene esposta chiaramente. Ora la differenza non sta nel fatto che il listato di sinistra non ha i metodi accessori getter e setter per gestire la politica di accesso alle proprietà x e y, in quanto non cambierebbe nulla accedere alle due proprietà tramite questi metodi. La vera information hiding non viene data dall’incapsulamento delle proprietà all’interno di metodi accessori, ma viene data dall’astrazione. Un classe non deve semplicemente inserire le proprietà all’interno di metodi get e set, deve invece esporre interfacce che permettano di manipolare la vera essenza del dato senza che chi la utilizza sappia nulla dell’implementazione.

Da questo concetto derivano le definizioni di “oggetto” e “struttura dati“. Il primo è un entità che nasconde il dato dietro un astrazione ed espone metodi (ha dei comportamenti) che permettono di operare sul dato. Il secondo invece espone direttamente il dato (con o senza metodi accessori) e non ha nessun comportamento. Viene anche chiamato DTO data transfert object proprio perché tipicamente viene utilizzato per memorizzare dati grezzi ad esempio provenienti dal DB che poi verranno utilizzati dalle classi di dominio responsabili della logica dell’applicazione.

“Go back and read that again!!” .. by Uncle Bob.

Esaminiamo altri esempi che uncle bob ci mette a disposizione per chiarire bene il concetto. Consideriamo i due listati:

public class Square {
    public Point topLeft;
    public double side;
}

public class Rectangle {

    public Point topLeft;
    public double height;
    public double width;
}

public class Circle {

   public Point center;
   public double radius;
}public class Geometry {
   public final PI = 3.14;

public double area(Object shape) throws ShException {
   if(shape istanceof Square){
      Square s = (Square) shape;
      return s.side * s.side;
   } else if(shape istanceof Rectangle){
      Rectangle r = (Rectangle) shape;
      return r.width * r.height;
   } else if(shape istanceof Circle){
      Circle c = (Circle) shape;
      return c.radius * c.radius * PI;
   }
throw new ShException();
}
}
public class Square implements Shape{
   private Point topLeft;
   private double side;public double area() {
    return side * side;
}
}
public class Rectangle implements Shape{

   private Point topLeft;
   private double width;
   private double height;public double area() {
    return width * height;
}public class Circle implements Shape{
   private Point center;
   private double radius;
   public final PI = 3.14;public double area() {
    return radius * radius * PI;
}
}

Nel listato di sinistra (procedurale), abbiamo la classe Geometry che opera sulle tre classi Shape. In questo caso le classi Shape sono delle semplici strutture dati. Tutto il comportamento (la logica) è demandata alla classe Geometry. Se vogliamo aggiungere un metodo “perimeter()” alla classe Geometry le tre classi Shape non verranno minimamente modificate. Ogni altra classe che dipende da Shape non verrà influenzata. D’altra parte se voglio aggiungere una nuova shape dovrò modificare tutti i metodi di Geometry che utilizzano le shape e aggiungere if a cascata.

Nel listato di destra (a oggetti) abbiamo che il metodo area è polimorfico. Non abbiamo bisogno di una classe Geometry per demandare la logica dato che ogni classe che implementa l’interfaccia Shape è un oggetto e implementa un metodo area che ne descrive il proprio comportamento. Questo ci da il grande vantaggio di poter aggiungere altre classi che implementano Shape senza che nessun altra classe ne venga influenzata (nessuna classe verrà modificata). Il lato “oscuro” o complementare sta nel fatto che se vogliamo aggiungere un metodo all’interfaccia, tutte le shape dovranno essere modificate.

Da queste considerazioni scaturisce la natura complementare che c’è tra oggetti e strutture dati :

“Il codice procedurale rende difficile l’aggiunta di nuove strutture dati (ma facile l’aggiunta di una funzione) perché tutte le funzioni delle classi che gestiscono la logica dovranno essere modificate. Il codice orientato agli oggetti (OOP) rende difficile l’aggiunta di nuove funzioni dato che tutte le classi dovranno essere modificate”.

Quindi ci sono situazioni nelle quali è bene utilizzare un approccio piuttosto che  l’altro. In sistemi complessi nei quali ci sarà un esigenza maggiore di aggiungere nuovi tipi di dati (nuove classi) di quella di aggiungere funzioni, sarà giusto utilizzare un approccio OOP. Mentre in situazioni nelle quali abbiamo l’esigenza di dover aggiungere funzioni l’approccio procedurale potrebbe essere quello azzeccato… che dubbi!!

Questo articolo lo trovi sotto articoli e taggato come , , , , .

I commenti sono chiusi.