Il costruttore non dovrebbe generalmente chiamare metodi


12

Ho descritto a un collega perché un costruttore che chiama un metodo può essere un antipattern.

esempio (nel mio arrugginito C ++)

class C {
public :
    C(int foo);
    void setFoo(int foo);
private:
    int foo;
}

C::C(int foo) {
    setFoo(foo);
}

void C::setFoo(int foo) {
    this->foo = foo
}

Vorrei motivare meglio questo fatto attraverso il vostro contributo aggiuntivo. Se hai esempi, riferimenti a libri, pagine di blog o nomi di principi, sarebbero i benvenuti.

Modifica: sto parlando in generale, ma stiamo programmando in Python.


È una regola generale o specifica per particolari lingue?
ChrisF

Quale lingua? In C ++ è più di un anti-pattern: parashift.com/c++-faq-lite/strange-inheritance.html#faq-23.5
LennyProgrammers,

@ Lenny222, l'OP parla di "metodi di classe", che - almeno per me - significa metodi non di istanza . Che quindi non può essere virtuale.
Péter Török,

3
@Alb In Java è perfettamente ok. Quello che non dovresti fare è passare esplicitamente thisa uno dei metodi che chiami dal costruttore.
biziclop,

3
@Stefano Borini: se stai programmando in Python, perché non mostrare l'esempio in Python invece che in C ++ arrugginito? Inoltre, spiega perché questa è una cosa negativa. Lo facciamo sempre.
S.Lott

Risposte:


26

Non hai specificato una lingua.

In C ++ un costruttore deve fare attenzione quando chiama una funzione virtuale, in quanto la funzione effettiva che sta chiamando è l'implementazione della classe. Se si tratta di un metodo virtuale puro senza implementazione, si tratterà di una violazione di accesso.

Un costruttore può chiamare funzioni non virtuali.

Se la tua lingua è Java, dove le funzioni sono generalmente virtuali per impostazione predefinita, è logico che tu debba fare molta attenzione.

C # sembra gestire la situazione come ti aspetteresti: puoi chiamare metodi virtuali nei costruttori e chiama la versione più definitiva. Quindi in C # non un anti-pattern.

Un motivo comune per chiamare i metodi dai costruttori è che si hanno più costruttori che vogliono chiamare un metodo "init" comune.

Nota che i distruttori avranno lo stesso problema con i metodi virtuali, quindi non puoi avere un metodo di "pulizia" virtuale che si trova al di fuori del tuo distruttore e ti aspetti che venga chiamato dal distruttore di classe base.

Java e C # non hanno distruttori, hanno finalizzatori. Non conosco il comportamento con Java.

A questo proposito, C # sembra gestire correttamente la pulizia.

(Si noti che sebbene Java e C # abbiano Garbage Collection, che gestisce solo l'allocazione di memoria. Esiste un altro clean-up che il distruttore deve fare che non sta rilasciando memoria).


13
Ci sono alcuni piccoli errori qui. I metodi in C # non sono virtuali per impostazione predefinita. C # ha una semantica diversa rispetto a C ++ quando si chiama un metodo virtuale in un costruttore; verrà chiamato il metodo virtuale sul tipo più derivato, non il metodo virtuale sulla parte del tipo attualmente in costruzione. C # chiama i suoi metodi di finalizzazione "distruttori" ma hai ragione nel dire che hanno la semantica dei finalizzatori. I metodi virtuali chiamati nei distruttori C # funzionano allo stesso modo dei costruttori; viene chiamato il metodo più derivato.
Eric Lippert,

@Péter: intendevo metodi di istanza. dispiace per la confusione.
Stefano Borini,

1
@Eric Lippert. Grazie per la tua esperienza su C #, ho modificato la mia risposta di conseguenza. Sono inconsapevole di quel linguaggio, conosco molto bene C ++ e Java meno bene.
CashCow,

5
Prego. Nota che chiamare un metodo virtuale in un costruttore di classi base in C # è ancora una pessima idea.
Eric Lippert,

Se chiami un metodo (virtuale) in Java da un costruttore, invocherà sempre l'override più derivato. Tuttavia, ciò che chiami "il modo in cui ti aspetteresti" è ciò che definirei confuso. Perché mentre Java invoca l'override più derivato, quel metodo vedrà solo gli inizializzatori archiviati elaborati ma non il costruttore della propria esecuzione di classe. Invocare un metodo su una classe che non ha ancora stabilito il suo invariante può essere pericoloso. Quindi penso che C ++ abbia fatto la scelta migliore qui.
5gon12eder

18

OK, ora che la confusione per quanto riguarda i metodi della classe vs metodi di istanza è chiarito, posso dare una risposta :-)

Il problema non sta nel chiamare metodi di istanza in generale da un costruttore; è con la chiamata di metodi virtuali (direttamente o indirettamente). E la ragione principale è che all'interno del costruttore l'oggetto non è ancora completamente costruito . E soprattutto le sue parti della sottoclasse non sono affatto costruite mentre il costruttore della classe base è in esecuzione. Quindi il suo stato interno è incoerente in modo dipendente dalla lingua e ciò può causare diversi bug sottili in diverse lingue.

C ++ e C # sono già stati discussi da altri. In Java, verrà chiamato il metodo virtuale del tipo più derivato, tuttavia quel tipo non è ancora inizializzato. Pertanto, se tale metodo utilizza campi del tipo derivato, tali campi potrebbero non essere ancora inizializzati correttamente in quel momento. Questo problema è discusso in dettaglio in Effecive Java 2nd Edition , Item 17: Progettare e documentare l'ereditarietà oppure vietarlo .

Si noti che questo è un caso speciale del problema generale della pubblicazione prematura dei riferimenti agli oggetti . I metodi di istanza hanno un thisparametro implicito , ma il passaggio thisesplicito a un metodo può causare problemi simili. Soprattutto nei programmi simultanei in cui se il riferimento all'oggetto è pubblicato prematuramente su un altro thread, quel thread può già richiamare i metodi su di esso prima che il costruttore nel primo thread termini.


3
(+1) "all'interno del costruttore, l'oggetto non è ancora completamente costruito." Come "metodi di classe vs istanza". Alcuni linguaggi di programmazione lo considerano costruito quando si entra nel contrettore, come se il programmatore assegnasse valori al costruttore.
Umlcat,

7

Non considererei le chiamate di metodo qui come un antipattern in sé, più un odore di codice. Se una classe fornisce un resetmetodo, questo restituisce un oggetto al suo stato originale, quindi chiamare reset()nel costruttore è DRY. (Non sto facendo alcuna dichiarazione sui metodi di ripristino).

Ecco un articolo che potrebbe aiutare a soddisfare il tuo appello all'autorità: http://misko.hevery.com/code-reviewers-guide/flaw-constructor-does-real-work/

In realtà non si tratta di chiamare metodi, ma di costruttori che fanno troppo. IMHO, chiamare i metodi in un costruttore è un odore che potrebbe indicare che un costruttore è troppo pesante.

Ciò è legato alla facilità con cui è possibile testare il codice. I motivi includono:

  1. I test unitari implicano molta creazione e distruzione, quindi la costruzione dovrebbe essere rapida.

  2. A seconda di ciò che fanno questi metodi, può essere difficile testare unità di codice discrete senza fare affidamento su alcune condizioni preliminari (potenzialmente non verificabili) impostate nel costruttore (ad es. Ottenere informazioni da una rete).


3

Filosoficamente, lo scopo del costruttore è quello di trasformare un pezzo grezzo di memoria in un'istanza. Mentre il costruttore viene eseguito, l'oggetto non esiste ancora, quindi chiamare i suoi metodi è una cattiva idea. Potresti non sapere cosa fanno internamente dopo tutto, e possono giustamente considerare l'oggetto almeno esistere (duh!) Quando vengono chiamati.

Tecnicamente, potrebbe non esserci nulla di sbagliato in questo, in C ++ e specialmente in Python, sta a te fare attenzione.

In pratica, dovresti limitare le chiamate solo a tali metodi che inizializzano i membri della classe.


2

Non è un problema di uso generale. È un problema in C ++, in particolare quando si utilizzano l'ereditarietà e i metodi virtuali, perché la costruzione degli oggetti avviene all'indietro e i puntatori vtable vengono ripristinati con ogni livello di costruttore nella gerarchia dell'ereditarietà, quindi se si chiama un metodo virtuale è possibile che non si finisce per ottenere quello che corrisponde effettivamente alla classe che stai cercando di creare, il che vanifica l'intero scopo dell'uso dei metodi virtuali.

In lingue con supporto OOP sano, che imposta correttamente il puntatore vtable dall'inizio, questo problema non esiste.


2

Esistono due problemi con la chiamata di un metodo:

  • chiamando un metodo virtuale, che può fare qualcosa di inaspettato (C ++) o usare parti degli oggetti che non sono ancora state inizializzate
  • chiamare un metodo pubblico (che dovrebbe imporre gli invarianti di classe), poiché l'oggetto non è ancora necessariamente completo (e quindi il suo invariante potrebbe non essere valido)

Non c'è nulla di sbagliato nel chiamare una funzione di aiuto, purché non rientri nei due casi precedenti.


1

Non lo compro. In un sistema orientato agli oggetti, chiamare un metodo è praticamente l'unica cosa che puoi fare. In effetti, questa è più o meno la definizione di "orientato agli oggetti". Quindi, se un costruttore non può chiamare alcun metodo, allora cosa può fare?


Inizializza l'oggetto.
Stefano Borini,

@Stefano Borini: come? In un sistema orientato agli oggetti, l'unica cosa che puoi fare è chiamare i metodi. O guardarlo dall'angolo opposto: tutto viene fatto chiamando metodi. E "qualunque cosa" include ovviamente l'inizializzazione dell'oggetto. Quindi, se, per inizializzare l'oggetto, è necessario chiamare metodi, ma i costruttori non possono chiamare metodi, come può un costruttore inizializzare l'oggetto?
Jörg W Mittag,

non è assolutamente vero che l'unica cosa che puoi fare è chiamare i metodi. Puoi solo inizializzare lo stato senza alcuna chiamata, direttamente all'interno del tuo oggetto ... Il punto del costruttore è di rendere un oggetto in uno stato coerente. Se si chiamano altri metodi, questi potrebbero avere problemi nella gestione di un oggetto in uno stato parziale, a meno che non siano metodi specificamente fatti per essere chiamati dal costruttore (in genere come metodi di supporto)
Stefano Borini,

@Stefano Borini: "Puoi solo inizializzare lo stato senza alcuna chiamata, direttamente all'interno del tuo oggetto." Purtroppo, quando ciò comporta un metodo, cosa fai? Copia e incolla il codice?
S.Lott

1
@ S.Lott: no, lo chiamo, ma provo a mantenerlo una funzione del modulo invece di un metodo a oggetti, e faccio in modo che fornisca i dati di ritorno che posso inserire nello stato dell'oggetto nel costruttore. Se devo davvero avere un metodo oggetto, lo renderò privato e chiarirò che è per l'inizializzazione, come dare un nome proprio. Non chiamerei mai un metodo pubblico per impostare lo stato dell'oggetto dal costruttore.
Stefano Borini,

0

Nella teoria OOP, non dovrebbe importare, ma in pratica, ogni linguaggio di programmazione OOP gestisce costruttori diversi . Non uso metodi statici molto spesso.

In C ++ e Delphi, se dovessi fornire valori iniziali ad alcune proprietà ("membri del campo") e il codice è molto esteso, aggiungo alcuni metodi secondari come estensione dei costruttori.

E non chiamare altri metodi che fanno cose più complesse.

Per quanto riguarda i metodi "getter" e "setter" delle proprietà, di solito uso variabili private / protette per memorizzare il loro stato, oltre ai metodi "getter" e "setter".

Nel costruttore assegno valori "predefiniti" ai campi dello stato delle proprietà, SENZA chiamare gli "accessori".

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.