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


23

Gli accessori e i modificatori (ovvero setter e getter) sono utili per tre motivi principali:

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

Università, corsi online, tutorial, articoli di blog ed esempi di codice sul web sottolineano l'importanza degli accessor e dei modificatori, al giorno d'oggi sembrano quasi un "must" per il codice. Quindi si possono 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 modificatori più utili, quelli che effettivamente convalidano i parametri e generano 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 allora, non vedo quasi mai i modificatori chiamati da un costruttore, quindi l'esempio più comune di una semplice classe che devo affrontare è questo:

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;
    }

}

Vedi uno schema simile nella tua esperienza o sono solo io a essere sfortunato? E se lo fai, cosa pensi che stia causando? Esiste un evidente svantaggio nell'uso dei modificatori dai costruttori o sono considerati più sicuri? È qualcos'altro?


1
@stg, grazie, punto interessante! Potresti ampliarlo e inserirlo in una risposta con un esempio di una cosa negativa che potrebbe accadere forse?
Vlad Spreys,


8
@stg In realtà, è un anti-pattern usare metodi scavalcabili nel costruttore e molti strumenti di qualità del codice lo indicano come un errore. Non sai cosa farà la versione sovrascritta e potrebbe fare cose brutte come lasciare "questo" scappare prima che il costruttore finisca, il che può portare a tutti i tipi di problemi strani.
Michał Kosmulski,

1
@gnat: Non credo che questo sia un duplicato di I metodi di una classe dovrebbero chiamare i suoi getter e setter? , a causa della natura delle chiamate del costruttore invocate su un oggetto che non è completamente formato.
Greg Burghardt,

3
Nota anche che la tua "convalida" lascia l'età non inizializzata (o zero, o ... qualcos'altro, a seconda della lingua). Se si desidera che il costruttore fallisca, è necessario controllare il valore restituito e generare un'eccezione in caso di errore. Allo stato attuale, la tua validazione ha appena introdotto un bug diverso.
Inutile

Risposte:


37

Ragionamento filosofico molto generale

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

In genere, ci aspettiamo anche che i metodi di istanza possano assumere (come pre-condizioni) che queste garanzie siano 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 queste 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 lo fai bene, può essere molto fragile di fronte, ad es. riordinare le chiamate al metodo dell'istanza o altre operazioni.

Le lingue variano anche nel modo in cui risolvono le chiamate ai metodi di istanza ereditati dalle classi di base / sovrascritti 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 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. Apparentemente chiamare il setter non è affatto una garanzia di correttezza.

  2. Errori molto facili come dipendere dall'ordine di inizializzazione, come:

    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 la mia registrazione temporanea ha rotto tutto. Ops!

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

Una proposta semplice

È vero che la maggior parte del codice non è scritta in questo modo, ma se si desidera mantenere il costruttore pulito e prevedibile 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 meglio, se possibile: codificare il vincolo nel tipo di proprietà e convalidare il proprio valore al momento dell'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 eseguendo la noiosa convalida, poiché questa classe non fa 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 è davvero completo, ho tralasciato varie copie e spostato costruttori e incarichi.


4
Risposta fortemente sottovalutata! Consentitemi di aggiungere solo perché l'OP ha visto la situazione descritta in un libro di testo non la rende una buona soluzione. Se in una classe ci sono due modi per impostare un attributo (per costruttore e setter) e si vuole imporre un vincolo su quell'attributo, tutti e due i modi devono controllare il vincolo, altrimenti è un bug di progettazione .
Doc Brown,

Quindi considereresti di usare i metodi setter in una cattiva pratica del costruttore se non c'è 1) nessuna logica nei setter o 2) la logica nei setter è indipendente dallo stato? Non lo faccio spesso ma personalmente ho usato i metodi setter in un costruttore esattamente per quest'ultima ragione (quando c'è un tipo di condizione attiva nel setter che deve sempre essere applicata alla variabile membro
Chris Maggiulli

Se esiste un tipo di condizione che deve sempre essere applicata, questa è la logica nel setter. Penso certamente che sia meglio, se possibile, codificare questa logica nel membro, quindi è costretto a essere autonomo e non può acquisire dipendenze dal resto della classe.
Inutile il

13

Quando un oggetto viene costruito, per definizione non è completamente formato. Considera come il setter che 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;
}

Cosa accadrebbe se parte della convalida setAge()includesse un controllo sulla ageproprietà per garantire che l'oggetto potesse solo aumentare di età? Tale condizione potrebbe apparire come:

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

Sembra un cambiamento abbastanza innocente, poiché i gatti possono muoversi nel tempo solo in una direzione. L'unico problema è che non è possibile utilizzarlo per impostare il valore iniziale di this.ageperché presuppone che this.ageabbia già un valore valido.

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


OK ... ma, se in realtà non testate la costruzione di oggetti ed esponete i potenziali bug, ci sono potenziali bug in entrambi i modi . E ora hai due problemi: hai quel potenziale bug nascosto e devi imporre controlli della fascia d'età in due posti.
svidgen,

Non ci sono problemi, poiché in Java / C # tutti i campi vengono azzerati prima dell'invocazione del costruttore.
Deduplicatore

2
Sì, può essere difficile pensare ai metodi di istanza dai costruttori e non è generalmente preferito. Ora, se vuoi spostare la tua logica di validazione in un metodo privato, di classe finale / statico e chiamarla sia dal costruttore che dal setter, andrebbe bene.
Inutile

1
No, i metodi non di istanza evitano la delicatezza dei metodi di istanza, perché non toccano un'istanza parzialmente costruita. Ovviamente, devi ancora controllare il risultato della convalida per decidere se la costruzione è riuscita.
Inutile

1
Questa risposta è IMHO che manca il punto. "Quando un oggetto viene costruito, per definizione non è completamente formato" - nel mezzo del processo di costruzione, ovviamente, ma quando il costruttore è fatto, tutti i vincoli previsti sugli attributi devono essere mantenuti, altrimenti si può aggirare quel vincolo. Lo definirei un grave difetto di progettazione.
Doc Brown,

6

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

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

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

La risposta alle prime domande è chiaramente "sì". Un costruttore, quando non genera un'eccezione, dovrebbe lasciare l'oggetto in uno stato valido e se determinati valori sono proibiti per determinati attributi, allora non ha assolutamente senso lasciare che il ctor lo elimini.

Tuttavia, la risposta alla seconda domanda è in genere "no", purché non si adottino misure per evitare di ignorare il setter in una classe derivata. Pertanto, la migliore alternativa è implementare la convalida del vincolo in un metodo privato che può essere riutilizzato sia dal setter che dal costruttore. Non vado qui a ripetere l'esempio @Useless, che mostra esattamente questo disegno.


Ancora non capisco perché sia ​​meglio estrarre la logica dal setter in modo che il costruttore possa usarla. ... In che modo è davvero diverso dal chiamare semplicemente i setter in un ordine ragionevole ?? Soprattutto considerando che, se i tuoi setter sono semplici come "dovrebbero" essere, l'unica parte del setter che il tuo costruttore, a questo punto, non sta invocando è l'impostazione effettiva del campo sottostante ...
svidgen

2
@svidgen: vedi l'esempio di JimmyJames: in breve, non è una buona idea usare metodi scavalcabili in un ctor, che causerà problemi. È possibile utilizzare il setter in ctor se è definitivo e non chiama altri metodi sostituibili. Inoltre, separare la convalida dal modo di gestirla quando fallisce ti offre l'opportunità di gestirla in modo diverso nel ctor e nel setter.
Doc Brown,

Bene, ora sono ancora meno convinto! Ma grazie per avermi indicato l'altra risposta ...
svidgen

2
Con un ordine ragionevole intendi con una dipendenza fragile e invisibile per gli ordini facilmente spezzata da cambiamenti dall'aspetto innocente , giusto?
Inutile

@useless beh, no ... Voglio dire, è divertente che tu suggerisca che l'ordine in cui viene eseguito il tuo codice non dovrebbe avere importanza. ma non era quello il punto. Al di là degli esempi inventati, non riesco proprio a ricordare un'istanza nella mia esperienza in cui ho pensato che fosse una buona idea avere convalide tra proprietà in un setter - quel genere di cose porta necessariamente a requisiti di ordine di invocazione "invisibili" . Indipendentemente dal fatto che tali invocazioni si verifichino in un costruttore ..
svidgen

2

Ecco un po 'sciocco di codice Java che dimostra i tipi di problemi che puoi incontrare utilizzando metodi non finali 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 eseguo questo, ottengo un NPE nel costruttore NextProblem. Questo è ovviamente un esempio banale, ma le cose possono complicarsi rapidamente se si hanno più livelli di eredità.

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

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


In che modo questo è un problema di utilizzo dei setter nel costruttore invece di un problema di eredità mal implementata ??? In altre parole, se stai effettivamente invalidando il costruttore di base, perché dovresti persino chiamarlo !?
svidgen,

1
Penso che il punto sia che l'override di un setter non dovrebbe invalidare il costruttore.
Chris Wohlert,

@ChrisWohlert Ok ... ma, se cambi la logica del setter in conflitto con la logica del costruttore, è un problema indipendentemente dal fatto che il costruttore usi il setter. O fallisce in fase di costruzione, fallisce più tardi o fallisce in modo invisibile (b / c hai bypassato le tue regole aziendali).
svidgen,

Spesso non sai come viene implementato il costruttore della classe base (potrebbe essere in una libreria di terze parti da qualche parte). L'override di un metodo scavalcabile non dovrebbe interrompere il contratto dell'implementazione di base, tuttavia (e probabilmente questo esempio lo fa non impostando il namecampo).
Hulk,

@svidgen il problema qui è che chiamare metodi scavalcabili dal costruttore riduce l'utilità di sovrascriverli - non è possibile utilizzare nessun campo della classe figlio nelle versioni sostituite perché potrebbero non essere ancora inizializzati. Quel che è peggio, non devi passare un thisriferimento a qualche altro metodo, perché non puoi essere sicuro che sia ancora completamente inizializzato.
Hulk,

1

Dipende!

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

Tuttavia , se è necessario eseguire operazioni complesse per disporre l'oggetto prima che un determinato setter (o due!) Possa essere eseguito correttamente, non utilizzare il setter.

E in entrambi i casi, avere casi di test . Indipendentemente dal percorso che segui, se hai dei test unitari, avrai buone probabilità di esporre le insidie ​​associate a entrambe le opzioni.


Aggiungerò anche, se la mia memoria di quel lontano passato è persino lontanamente accurata, questo è il tipo di ragionamento che mi è stato insegnato anche al college. E non è affatto fuori linea con i progetti di successo a cui ho avuto il privilegio di lavorare.

Se dovessi indovinare, a un certo punto ho perso un promemoria "codice pulito" che ha identificato alcuni bug tipici che possono derivare dall'uso dei setter in un costruttore. Ma, da parte mia, non sono stato vittima di tali bug ...

Quindi, direi che questa particolare decisione non deve essere ampia e dogmatica.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.