Perché non è diventato un modello comune per usare setter nel costruttore?

20

Gli accessor e i modificatori (ovvero setters e getter) sono utili per tre ragioni principali:

  1. Limitano l'accesso alle variabili.
    • Ad esempio, è possibile accedere a una variabile, ma non modificata.
  2. Convalidano i parametri.
  3. Potrebbero causare alcuni effetti collaterali.

Università, corsi online, tutorial, articoli di blog ed esempi di codice sul web sottolineano l'importanza degli accessori e dei modificatori, che oggi sembrano quasi un "must" per il codice. Quindi li puoi trovare anche quando non forniscono alcun valore aggiuntivo, come il codice qui sotto.

public class Cat {
    private int age;

    public int getAge() {
        return this.age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

Detto questo, è molto comune trovare più modificatori utili, quelli che effettivamente convalidano i parametri e lanciano un'eccezione o restituiscono un valore booleano se è stato fornito un input non valido, qualcosa del genere:

/**
 * Sets the age for the current cat
 * @param age an integer with the valid values between 0 and 25
 * @return true if value has been assigned and false if the parameter is invalid
 */
public boolean setAge(int age) {
    //Validate your parameters, valid age for a cat is between 0 and 25 years
    if(age > 0 && age < 25) {
        this.age = age;
        return true;
    }
    return false;
}

Ma anche in questo caso, non vedo quasi mai i modificatori chiamati da un costruttore, quindi l'esempio più comune di una classe semplice con cui ho a che fare è:

public class Cat {
    private int age;

    public Cat(int age) {
        this.age = age;
    }

    public int getAge() {
        return this.age;
    }

    /**
     * Sets the age for the current cat
     * @param age an integer with the valid values between 0 and 25
     * @return true if value has been assigned and false if the parameter is invalid
     */
    public boolean setAge(int age) {
        //Validate your parameters, valid age for a cat is between 0 and 25 years
        if(age > 0 && age < 25) {
            this.age = age;
            return true;
        }
        return false;
    }

}

Ma si potrebbe pensare che questo secondo approccio sia molto più sicuro:

public class Cat {
    private int age;

    public Cat(int age) {
        //Use the modifier instead of assigning the value directly.
        setAge(age);
    }

    public int getAge() {
        return this.age;
    }

    /**
     * Sets the age for the current cat
     * @param age an integer with the valid values between 0 and 25
     * @return true if value has been assigned and false if the parameter is invalid
     */
    public boolean setAge(int age) {
        //Validate your parameters, valid age for a cat is between 0 and 25 years
        if(age > 0 && age < 25) {
            this.age = age;
            return true;
        }
        return false;
    }

}

Nella tua esperienza vedi uno schema simile o sono solo sfortunato? E se lo fai, allora cosa pensi che sta causando questo? C'è un evidente svantaggio nell'usare i modificatori dei costruttori o sono considerati solo più sicuri? È qualcos'altro?

    
posta Vlad Spreys 31.08.2016 - 13:45
fonte

5 risposte

35

Ragionamento filosofico molto generale

In genere, chiediamo che un costruttore fornisca (come post-condizioni) delle garanzie sullo stato dell'oggetto costruito.

In genere, ci aspettiamo anche che i metodi di istanza possano assumere (come pre-condizioni) che queste garanzie sono già valide quando vengono chiamate, e devono solo assicurarsi di non romperle.

Chiamare un metodo di istanza dall'interno del costruttore significa che alcune o tutte quelle garanzie potrebbero non essere state ancora stabilite, il che rende difficile ragionare sul fatto che le pre-condizioni del metodo di istanza siano soddisfatte. Anche se hai ragione, può essere molto fragile di fronte, ad es. riordinare le chiamate al metodo di istanza o altre operazioni.

Le lingue variano anche nel modo in cui risolvono le chiamate ai metodi di istanza che sono ereditate dalle classi base / sovrascritte dalle sottoclassi, mentre il costruttore è ancora in esecuzione. Questo aggiunge un altro livello di complessità.

Esempi specifici

  1. Il tuo esempio di come pensi che questo dovrebbe apparire è di per sé sbagliato:

    public Cat(int age) {
        //Use the modifier instead of assigning the value directly.
        setAge(age);
    }
    

    questo non controlla il valore di ritorno da setAge . Dopotutto chiamare il setter non è garanzia di correttezza.

  2. Errori molto semplici come a seconda dell'ordine di inizializzazione, come ad esempio:

    class Cat {
      private Logger m_log;
      private int m_age;
    
      public void setAge(int age) {
        // FIXME temporary debug logging
        m_log.write("=== DEBUG: setting age ===");
        m_age = age;
      }
    
      public Cat(int age, Logger log) {
        setAge(age);
        m_log = log;
      }
    };
    

    dove il mio logging temporaneo ha rotto tutto. Ops!

Ci sono anche linguaggi come C ++ dove chiamare un setter dal costruttore significa un'inizializzazione di default sprecata (che per alcune variabili membro vale almeno la pena di evitare)

Una proposta semplice

È vero che la maggior parte del codice non è scritto in questo modo, ma se si desidera mantenere pulito e prevedibile il costruttore, e riutilizzare ancora la logica pre e post-condizione, la soluzione migliore è :

class Cat {
  private int m_age;
  private static void checkAge(int age) {
    if (age > 25) throw CatTooOldError(age);
  }

  public void setAge(int age) {
    checkAge(age);
    m_age = age;
  }

  public Cat(int age) {
    checkAge(age);
    m_age = age;
  }
};

o anche meglio, se possibile: codifica il vincolo nel tipo di proprietà e fallo convalidare il suo valore sull'assegnazione:

class Cat {
  private Constrained<int, 25> m_age;

  public void setAge(int age) {
    m_age = age;
  }

  public Cat(int age) {
    m_age = age;
  }
};

E infine, per completezza completa, un wrapper auto-validante in C ++. Nota che sebbene stia ancora facendo la noiosa convalida, perché questa classe non nient'altro , è relativamente facile da controllare

template <typename T, T MAX, T MIN=T{}>
class Constrained {
    T val_;
    static T validate(T v) {
        if (MIN <= v && v <= MAX) return v;
        throw std::runtime_error("oops");
    }
public:
    Constrained() : val_(MIN) {}
    explicit Constrained(T v) : val_(validate(v)) {}

    Constrained& operator= (T v) { val_ = validate(v); return *this; }
    operator T() { return val_; }
};

OK, non è veramente completo, ho omesso varie copie e spostato costruttori e assegnazioni.

    
risposta data 31.08.2016 - 19:02
fonte
13

Quando un oggetto viene costruito, per definizione non è completamente formato. Considera come il setter hai fornito:

public boolean setAge(int age) {
    //Validate your parameters, valid age for a cat is between 0 and 25 years
    if(age > 0 && age < 25) {
        this.age = age;
        return true;
    }
    return false;
}

Che cosa succede se parte della convalida in setAge() includeva un controllo contro la proprietà age per assicurare che l'oggetto potesse aumentare solo con l'età? Questa condizione potrebbe essere simile a:

if (age > this.age && age < 25) {
    //...

Sembra un cambiamento piuttosto innocente, dal momento che i gatti possono muoversi solo nel tempo in una direzione. L'unico problema è che non puoi utilizzarlo per impostare il valore iniziale di this.age perché presuppone che this.age abbia già un valore valido.

Evitare i setter nei costruttori chiarisce che la cosa solo che il costruttore sta facendo è impostare la variabile di istanza e saltare qualsiasi altro comportamento che si verifica nel setter.

    
risposta data 31.08.2016 - 14:45
fonte
6

Alcune buone risposte già qui, ma finora nessuno ha notato che stai chiedendo qui due cose in una, il che provoca una certa confusione. IMHO il tuo post è meglio visto come due separate domande:

  1. se c'è una convalida di un vincolo in un setter, non dovrebbe essere anche nel costruttore della classe per assicurarsi che non possa essere aggirato?

  2. perché non chiamare direttamente il setter per questo scopo dal costruttore?

La risposta alle prime domande è chiaramente "sì". Un costruttore, quando non lancia un'eccezione, deve lasciare l'oggetto in uno stato valido e, se certi valori sono vietati per determinati attributi, non ha assolutamente senso lasciare che il censore elimini ciò.

Tuttavia, la risposta alla seconda domanda è in genere "no", a condizione che non si adottino misure per evitare di sovrascrivere il setter in una classe derivata. Pertanto, l'alternativa migliore consiste nell'implementare la convalida dei vincoli in un metodo privato che può essere riutilizzato dal setter e dal costruttore. Non sto andando qui a ripetere @esempio inutile, che mostra esattamente questo design.

    
risposta data 31.08.2016 - 23:44
fonte
2

Ecco un po 'di codice Java che dimostra i tipi di problemi che puoi eseguire utilizzando i metodi non finalizzati nel tuo costruttore:

import java.util.regex.Pattern;

public class Problem
{
  public static final void main(String... args)
  {
    Problem problem = new Problem("John Smith");
    Problem next = new NextProblem("John Smith", " ");

    System.out.println(problem.getName());
    System.out.println(next.getName());
  }

  private String name;

  Problem(String name)
  {
    setName(name);
  }

  void setName(String name)
  {
    this.name = name;
  }

  String getName()
  {
    return name;
  }
}

class NextProblem extends Problem
{
  private String firstName;
  private String lastName;
  private final String delimiter;

  NextProblem(String name, String delimiter)
  {
    super(name);

    this.delimiter = delimiter;
  }

  void setName(String name)
  {
    String[] parts = name.split(Pattern.quote(delimiter));

    this.firstName = parts[0];
    this.lastName = parts[1];
  }

  String getName()
  {
    return firstName + " " + lastName;
  }
}

Quando lo eseguo, ottengo un NPE nel costruttore NextProblem. Questo è un esempio banale, ma le cose possono complicarsi rapidamente se si hanno più livelli di ereditarietà.

Penso che la ragione più grande per cui questo non è diventato comune è che rende il codice un po 'più difficile da capire. Personalmente, non ho quasi mai metodi setter e le mie variabili membro sono quasi invariabilmente (gioco di parole) finale. Per questo motivo, i valori devono essere impostati nel costruttore. Se usi oggetti immutabili (e ci sono molti buoni motivi per farlo) la domanda è discutibile.

In ogni caso, è un buon obiettivo riutilizzare la validazione o altra logica e puoi metterlo in un metodo statico e invocarlo sia dal costruttore che dal setter.

    
risposta data 31.08.2016 - 20:28
fonte
1

Dipende!

Se stai eseguendo una semplice convalida o emetti effetti collaterali nel setter che devono essere colpiti ogni volta che la proprietà è impostata: usa il setter. L'alternativa è generalmente quella di estrarre il setter logica dal setter e invocare il nuovo metodo seguito dall'attuale set dal setter e del costruttore - che è effettivamente lo stesso di usare il setter! (Tranne che adesso mantieni il tuo setter in due righe, certamente piccolo, in due punti.)

Tuttavia , se è necessario eseguire operazioni complesse per organizzare l'oggetto prima un particolare setter (o due!) potrebbe essere eseguito con successo, non usare il setter.

E in entrambi i casi, hanno casi di test . Indipendentemente dalla rotta che segui, se hai dei test unitari, avrai una buona possibilità di esporre le insidie associate a entrambe le opzioni.

Aggiungerò anche, se la mia memoria di quel lontano lontano è anche lontanamente accurata, questo è il tipo di ragionamento che mi è stato insegnato anche al college. E non è del tutto fuori luogo rispetto ai progetti di successo su cui ho avuto il privilegio di lavorare.

Se dovessi indovinare, ho perso un memo di "codice pulito" ad un certo punto che identificava alcuni tipici bug che possono derivare dall'usare setter in un costruttore. Ma, da parte mia, non sono stato vittima di alcuno di questi bug ...

Quindi, direi che questa particolare decisione non ha bisogno di essere radicale e dogmatica.

    
risposta data 01.09.2016 - 00:18
fonte

Leggi altre domande sui tag