C'è mai una ragione per fare tutto il lavoro di un oggetto in un costruttore?


49

Consentitemi di prefigurarlo dicendo che questo non è il mio codice né il codice dei miei colleghi. Anni fa, quando la nostra azienda era più piccola, avevamo alcuni progetti di cui avevamo bisogno per i quali non avevamo la capacità, quindi erano esternalizzati. Ora, non ho nulla contro l'outsourcing o gli appaltatori in generale, ma la base di codice che hanno prodotto è una massa di WTF. Detto questo, funziona (principalmente), quindi suppongo sia tra i primi 10% dei progetti in outsourcing che ho visto.

Man mano che la nostra azienda è cresciuta, abbiamo cercato di sfruttare maggiormente il nostro sviluppo interno. Questo particolare progetto è atterrato sulle mie ginocchia, quindi l'ho ripassato, ripulito, aggiunto test, ecc. Ecc.

C'è un modello che vedo ripetuto molto e sembra così terribilmente terribile che mi chiedevo se forse c'è una ragione e semplicemente non la vedo. Il modello è un oggetto senza metodi o membri pubblici, solo un costruttore pubblico che svolge tutto il lavoro dell'oggetto.

Ad esempio, (il codice è in Java, se è importante, ma spero che questa sia una domanda più generale):

public class Foo {
  private int bar;
  private String baz;

  public Foo(File f) {
    execute(f);
  }

  private void execute(File f) {
     // FTP the file to some hardcoded location, 
     // or parse the file and commit to the database, or whatever
  }
}

Se ti stai chiedendo, questo tipo di codice viene spesso chiamato nel modo seguente:

for(File f : someListOfFiles) {
   new Foo(f);
}

Ora, mi è stato insegnato molto tempo fa che gli oggetti istanziati in un ciclo sono generalmente una cattiva idea e che i costruttori dovrebbero fare un minimo di lavoro. Guardando questo codice sembra che sarebbe meglio eliminare il costruttore e creare executeun metodo statico pubblico.

Ho chiesto all'appaltatore perché è stato fatto in questo modo e la risposta che ho ricevuto è stata "Possiamo cambiarlo se lo desideri". Che non è stato davvero utile.

Ad ogni modo, c'è mai un motivo per fare qualcosa del genere, in qualsiasi linguaggio di programmazione, o è solo un'altra presentazione del Daily WTF?


18
Mi sembra che sia stato scritto da sviluppatori che conoscono public static void main(string[] args)e hanno sentito parlare di oggetti, quindi hanno cercato di mescolarli insieme.
Andy Hunt,

Se non vieni più richiamato, devi farlo lì. Molti framework moderni, ad esempio per l'iniezione di dipendenza, richiedono che i bean vengano creati, impostati proprietà e POI invocati, quindi non è possibile utilizzare questi lavoratori pesanti.

4
Domanda correlata: qualche problema con l'esecuzione del lavoro principale di una classe nel suo costruttore? . Questo è un po 'diverso, però, perché l'istanza creata viene utilizzata in seguito.
sleske,


Non voglio davvero espandere questo a una risposta completa, ma lasciatemi solo dire: ci sono scenari in cui questo è discutibilmente accettabile. Questo è molto lontano da esso. Qui un oggetto viene istanziato senza realmente aver bisogno dell'oggetto. Al contrario, se si creava una sorta di struttura di dati da un file XML, l'avvio dell'analisi dal costruttore non è del tutto terribile. In effetti in linguaggi che consentono costruttori di sovraccarico, non è niente per cui ti ucciderei come manutentore;)
back2dos

Risposte:


60

Ok, scendendo l'elenco:

Mi è stato insegnato molto tempo fa che gli oggetti istanziati in un ciclo sono generalmente una cattiva idea

Non in nessuna delle lingue che ho usato.

In C è una buona idea dichiarare le variabili in anticipo, ma è diverso da quello che hai detto. Potrebbe essere leggermente più veloce se dichiari oggetti sopra il loop e li riutilizzi, ma ci sono molte lingue in cui questo aumento di velocità sarà insignificante (e probabilmente alcuni compilatori là fuori che fanno l'ottimizzazione per te :)).

In generale, se hai bisogno di un oggetto all'interno di un loop, creane uno.

i costruttori dovrebbero fare un minimo di lavoro

I costruttori dovrebbero creare un'istanza dei campi di un oggetto ed eseguire qualsiasi altra inizializzazione necessaria per rendere l'oggetto pronto per l'uso. Questo in genere significa che i costruttori sono piccoli, ma ci sono scenari in cui ciò comporterebbe una notevole quantità di lavoro.

c'è mai un motivo per fare qualcosa del genere, in qualsiasi linguaggio di programmazione, o è solo un'altra presentazione al Daily WTF?

È una presentazione al WTF giornaliero. Certo, ci sono cose peggiori che puoi fare con il codice. Il problema è che l'autore ha un grande fraintendimento su cosa siano le classi e su come usarle. Nello specifico, ecco cosa vedo che è sbagliato in questo codice:

  • Uso improprio delle classi: la classe si comporta sostanzialmente come una funzione. Come hai già detto, dovrebbe essere sostituito con una funzione statica, oppure la funzione dovrebbe essere implementata nella classe che la chiama. Dipende da cosa fa e da dove viene utilizzato.
  • Sovraccarico delle prestazioni: a seconda della lingua, la creazione di un oggetto può essere più lenta rispetto alla chiamata di una funzione.
  • Confusione generale: è generalmente fonte di confusione per il programmatore come utilizzare questo codice. Senza vederlo usato, nessuno avrebbe saputo come l'autore intendesse utilizzare il codice in questione.

Grazie per la risposta. In riferimento a "un'istanza di oggetti in un ciclo", questo è qualcosa che molti strumenti di analisi statica mettono in guardia e l'ho sentito anche da alcuni professori universitari. Certo, questo potrebbe essere un ostacolo dagli anni '90 quando il successo delle prestazioni era maggiore. Certo, se ne hai bisogno, fallo, ma sono sorpreso di sentire un punto di vista opposto. Grazie per allargare i miei orizzonti.
Kane,

8
L'istanza di oggetti in un ciclo è un odore di codice minore; dovresti guardarlo e chiedere "devo davvero istanziare questo oggetto più volte?". Se stai creando un'istanza dello stesso oggetto con gli stessi dati e gli stai dicendo di fare la stessa cosa, salverai i cicli (ed eviterai di distruggere la memoria) istanziandolo una volta e poi ripetendo l'istruzione con qualsiasi modifica incrementale che potrebbe essere necessaria allo stato dell'oggetto . Tuttavia, se, ad esempio, si sta leggendo un flusso di dati da un DB o altro input e si sta creando una serie di oggetti per conservare tali dati, senza dubbio un'istanza.
Keith

3
Versione breve: non utilizzare le istanze degli oggetti come se fossero una funzione.
Erik Reppen,

27

Per me, è un po 'una sorpresa quando si ottiene un oggetto che fa molto lavoro nel costruttore, quindi solo che lo sconsiglio (principio della minima sorpresa).

Un tentativo per un buon esempio :

Non sono sicuro che questo utilizzo sia un anti-pattern, ma in C # ho visto il codice che era sulla falsariga di (esempio un po 'inventato):

using (ImpersonationContext administrativeContext = new ImpersonationContext(ADMIN_USER))
{
    // Perform actions here under the administrator's security context
}
// We're back to "normal" privileges here

In questo caso, la ImpersonationContextclasse fa tutto il suo lavoro nel costruttore e nel metodo Dispose. Sfrutta l' usingistruzione C # , che è abbastanza ben compresa dagli sviluppatori C #, e quindi garantisce che l'installazione che si verifica nel costruttore viene ripristinata una volta fuori usingdall'istruzione, anche se si verifica un'eccezione. Questo è probabilmente il miglior utilizzo che ho visto per questo (cioè "lavoro" nel costruttore), anche se non sono ancora sicuro che lo considererei "buono". In questo caso, è abbastanza autoesplicativo, funzionale e succinto.

Un esempio di un modello buono e simile sarebbe il modello di comando . Sicuramente non esegui la logica nel costruttore lì, ma è simile nel senso che l'intera classe è equivalente a una chiamata al metodo (inclusi i parametri necessari per invocare il metodo). In questo modo, le informazioni sull'invocazione del metodo possono essere serializzate o passate per un uso successivo, il che è immensamente utile. Forse i tuoi appaltatori stavano tentando qualcosa di simile, anche se ne dubito fortemente.

Commenti sul tuo esempio:

Direi che è un anti-pattern molto definito. Non aggiunge nulla, va contro ciò che ci si aspetta ed esegue un'elaborazione troppo lunga nel costruttore (FTP nel costruttore? WTF in effetti, anche se immagino che le persone siano state chiamate chiamare servizi Web in questo modo).

Faccio fatica a trovare la citazione, ma il libro di Grady Booch "Object Solutions" ha un aneddoto su un team di sviluppatori C che passano al C ++. Apparentemente avevano seguito l'addestramento e gli era stato detto dal management che dovevano assolutamente fare cose "orientate agli oggetti". Costruito a capire cosa c'è che non va, l'autore ha usato uno strumento di metrica del codice e ha scoperto che il numero medio di metodi per classe era esattamente 1 e che erano variazioni della frase "Do It". Devo dire che non ho mai riscontrato questo particolare problema prima, ma a quanto pare può essere un sintomo di persone costrette a creare codice orientato agli oggetti, senza davvero capire come farlo, e perché.


1
Il tuo buon esempio è un'istanza di Allocazione risorse è Inizializzazione . È un modello consigliato in C ++ perché semplifica molto la scrittura di codice sicuro per le eccezioni. Non so se questo si ripercuota su C #, comunque. (In questo caso la "risorsa" assegnata è privilegi amministrativi.)
zwol

@Zack Non ci avevo mai pensato in questi termini, anche se ero a conoscenza di RAII in C ++. Nel mondo C #, è indicato come modello usa e getta ed è molto raccomandato per qualsiasi cosa che abbia risorse (in genere non gestite) da liberare dopo la sua vita. La differenza principale di questo esempio è che in genere non si eseguono operatori costosi nel costruttore. Ad esempio, un metodo Open () di SqlConnection deve ancora essere chiamato dopo il costruttore.
Daniel B,

14

No.

Se tutto il lavoro viene svolto nel costruttore significa che l'oggetto non è mai stato necessario in primo luogo. Molto probabilmente, l'appaltatore stava semplicemente usando l'oggetto per modularità. In tal caso, una classe non istanziata con un metodo statico avrebbe ottenuto lo stesso vantaggio senza creare istanze di oggetti superflui.

C'è solo un caso in cui è accettabile fare tutto il lavoro in un costruttore di oggetti. Questo è quando lo scopo dell'oggetto è specificamente quello di rappresentare un'identità unica a lungo termine, piuttosto che eseguire lavori. In tal caso, l'oggetto verrebbe tenuto in giro per motivi diversi dall'esecuzione di lavori significativi. Ad esempio, un'istanza singleton.


7

Ho sicuramente creato molte classi che fanno molto lavoro per calcolare qualcosa nel costruttore, ma l'oggetto è immutabile, quindi rende disponibili i risultati di quel calcolo tramite proprietà e quindi non fa mai nient'altro. Questa è la normale immutabilità in azione.

Penso che la cosa strana qui sia mettere un codice con effetti collaterali in un costruttore. Costruire un oggetto e gettarlo via senza accedere alle proprietà o ai metodi sembra strano (ma almeno in questo caso è abbastanza ovvio che sta succedendo qualcos'altro).

Ciò sarebbe meglio se la funzione fosse spostata in un metodo pubblico e quindi fosse iniettata un'istanza della classe, quindi il ciclo for dovrebbe chiamare ripetutamente il metodo.


4

C'è mai una ragione per fare tutto il lavoro di un oggetto in un costruttore?

No.

  • Un costruttore non dovrebbe avere effetti collaterali.
    • Qualsiasi cosa oltre all'inizializzazione del campo privato dovrebbe essere vista come un effetto collaterale.
    • Un costruttore con effetti collaterali infrange il principio di responsabilità singola (SRP) e va contro lo spirito della programmazione orientata agli oggetti (OOP).
  • Un costruttore dovrebbe essere leggero e non dovrebbe mai fallire.
    • Ad esempio, rabbrividisco sempre quando vedo un blocco try-catch all'interno di un costruttore. Un costruttore non dovrebbe generare eccezioni o errori di registro.

Si potrebbe ragionevolmente mettere in discussione queste linee guida e dire: "Ma io non seguo queste regole e il mio codice funziona bene!" A quello, risponderei, "Beh, potrebbe essere vero, fino a quando non lo è."

  • Eccezioni ed errori all'interno di un costruttore sono molto inaspettati. A meno che non gli venga detto di farlo, i futuri programmatori non saranno inclini a circondare queste chiamate del costruttore con un codice difensivo.
  • Se qualcosa non riesce nella produzione, la traccia dello stack generata potrebbe essere difficile da analizzare. La parte superiore della traccia dello stack può puntare alla chiamata del costruttore, ma molte cose accadono nel costruttore e potrebbe non puntare al LOC effettivo non riuscito.
    • Ho analizzato molte tracce dello stack .NET in cui questo era il caso.

1
"A meno che non gli venga detto di farlo, i futuri programmatori non saranno inclini a circondare queste chiamate del costruttore con un codice difensivo." - Buon punto.
Graham,

2

Mi sembra che la persona che stava facendo questo stava cercando di aggirare la limitazione di Java che non consente di passare funzioni come cittadini di prima classe. Ogni volta che devi passare una funzione, devi racchiuderla in una classe con un metodo simile applyo simile.

Quindi immagino abbia appena usato una scorciatoia e usato direttamente una classe come sostituto della funzione. Ogni volta che si desidera eseguire la funzione, è sufficiente creare un'istanza della classe.

Non è sicuramente un buon modello, almeno perché stiamo cercando di capirlo, ma immagino che possa essere il modo meno prolisso per fare qualcosa di simile alla programmazione funzionale in Java.


3
Sono abbastanza sicuro che questo particolare programmatore non abbia mai sentito la frase "programmazione funzionale".
Kane,

Non penso che il programmatore stesse tentando di creare un'astrazione di chiusura. L'equivalente Java richiederebbe un metodo astratto (pianificazione per un possibile) polimorfismo. Non ci sono prove qui.
Kevin A. Naudé,

1

Per me la regola empirica è di non fare nulla nel costruttore, tranne per l'inizializzazione delle variabili membro. Il primo motivo è che aiuta a seguire il principio SRP, poiché di solito un lungo processo di inizializzazione è un'indicazione che la classe fa più di quanto dovrebbe fare e l'inizializzazione dovrebbe essere fatta da qualche parte al di fuori della classe. Il secondo motivo è che in questo modo vengono passati solo i parametri necessari, quindi si crea meno codice accoppiato. Un costruttore con un'inizializzazione complicata di solito necessita di parametri che vengono utilizzati solo per costruire un altro oggetto, ma che non vengono utilizzati dalla classe originale.


1

Ho intenzione di andare controcorrente qui - in lingue in cui tutto deve essere in una classe E non puoi avere una classe statica, puoi imbatterti nella situazione in cui hai un po 'di funzionalità isolata che deve essere mettere da qualche parte e non si adatta con nient'altro. Le tue scelte sono una classe di utilità che copre vari bit di funzionalità non correlati o una classe che fa sostanzialmente solo questa cosa.

Se hai solo un po 'come questo, allora la tua classe statica è la tua classe specifica. Quindi, o hai un costruttore che fa tutto il lavoro, o una singola funzione che fa tutto il lavoro. Il vantaggio di fare tutto nel costruttore è che succede una sola volta, ti permette di usare campi, proprietà e metodi privati ​​senza preoccuparti che vengano riutilizzati o thread. Se hai un costruttore / metodo potresti essere tentato di lasciare che i parametri vengano passati al singolo metodo, ma se usi campi privati ​​o proprietà che introducono potenziali problemi di threading.

Anche con una classe di utilità, la maggior parte delle persone non penserà di avere classi statiche nidificate (e ciò potrebbe non essere possibile neanche a seconda della lingua).

Fondamentalmente questo è un modo relativamente comprensibile di isolare il comportamento dal resto del sistema, all'interno di ciò che è consentito dalla lingua, pur consentendo allo stesso tempo di trarre il massimo vantaggio dalla lingua.

Francamente avrei risposto molto come ha fatto il tuo appaltatore, è un piccolo aggiustamento per trasformarlo in due chiamate, non c'è molto da raccomandare l'una sull'altra. Quali dis / advatange ci sono, sono probabilmente più ipotetici che reali, perché provare a giustificare la decisione quando non ha importanza e probabilmente non è stata decisa tanto quanto l'azione predefinita?


1

Esistono schemi comuni nei linguaggi in cui funzioni e classi sono molto più simili, come Python, in cui uno usa un oggetto sostanzialmente per funzionare con troppi effetti collaterali o con più oggetti nominati restituiti.

In questo caso, la maggior parte, se non tutto il lavoro può essere fatto nel costruttore, poiché l'oggetto stesso è solo un involucro attorno a un singolo pacchetto di lavoro, e non vi è alcuna buona ragione per inserirlo in una funzione separata. Qualcosa di simile a

var parser = XMLParser()
var x = parser.parse(string)
string a = x.LookupTag(s)

non è davvero meglio di

 var x = ParsedXML(string)
 string a = x.LookupTag(s)

Invece di avere direttamente gli effetti collaterali, puoi chiamare l'oggetto per applicare l'effetto collaterale al momento giusto, che ti dà una migliore visibilità. Se avrò un brutto effetto collaterale su un oggetto, potrò fare tutto il mio lavoro e poi far passare l'oggetto che farò male e danneggiarlo. Identificare l'oggetto e isolare la chiamata in questo modo è più chiaro che passare più oggetti simili in una funzione contemporaneamente.

x.UpdateView(v)

È possibile creare più valori di ritorno tramite gli accessori sull'oggetto. Tutto il lavoro è fatto, ma voglio che le cose siano tutte nel contesto e non voglio davvero passare più riferimenti nella mia funzione.

var x = new ComputeStatistics(inputFile)
int max = x.MaxOptions;
int mean = x.AverageOption;
int sd = x.StandardDeviation;
int k = x.Kurtosis;
...

Quindi, in quanto programmatore congiunto di Python / C #, questo non mi sembra affatto strano.


Devo chiarire che l'esempio dato rimane fuorviante. Nell'esempio senza accessi e senza valore di ritorno, questo è probabilmente rudimentale. Forse i rendimenti di debug forniti originale o qualcosa del genere.
Jon Jay Obermark,

1

Viene in mente un caso in cui il costruttore / distruttore fa tutto il lavoro:

Serrature. Normalmente non interagisci mai con un lucchetto durante la sua vita. L'uso dell'architettura di C # è utile per gestire tali casi senza fare esplicito riferimento al distruttore. Tuttavia, non tutti i blocchi sono implementati in questo modo.

Ho anche scritto ciò che equivaleva a un bit di codice solo costruttore per correggere un bug della libreria di runtime. (La correzione effettiva del codice avrebbe causato altri problemi.) Non vi era alcun distruttore perché non era necessario annullare la patch.


-1

Sono d'accordo con AndyBursh. Sembra una certa mancanza di conoscenza.

Tuttavia, è importante menzionare quadri come NHibernate che richiedono solo di programmare in costruttore (durante la creazione di classi di mappe). Tuttavia, questa non è una limitazione del framework, è proprio ciò di cui hai bisogno. Quando si tratta di riflessione, il costruttore è l'unico metodo che esisterà sempre (anche se potrebbe essere privato) e questo potrebbe essere utile se si deve chiamare dinamicamente un metodo di una classe.


-4

"Mi è stato insegnato molto tempo fa che gli oggetti istanziati in un ciclo sono generalmente una cattiva idea"

Sì, forse stai ricordando perché abbiamo bisogno di StringBuilder.

Ci sono due scuole di Java.

Il costruttore è stato promosso dal primo , che ci insegna a riutilizzare (perché condividere = identità per efficienza). Tuttavia, all'inizio del 2000 ho iniziato a leggere che la creazione di oggetti in Java è così economica (al contrario del C ++) e GC è così efficiente che il riutilizzo è inutile e l'unica cosa che conta è la produttività del programmatore. Per produttività, deve scrivere un codice gestibile, il che significa modularizzazione (ovvero restringere l'ambito). Così,

questo non va bene

byte[] array = new byte[kilos or megas]
for_loop:
  use(array)

questo è buono.

for_loop:
  byte[] array = new byte[kilos or megas]
  use(array)

Ho posto questa domanda quando esisteva forum.java.sun e le persone hanno concordato che avrebbero preferito dichiarare l'array il più stretto possibile.

Il programmatore java moderno non esita mai a dichiarare una nuova classe o creare un oggetto. È incoraggiato a farlo. Java fornisce tutte le ragioni per farlo, con la sua idea che "tutto è un oggetto" e una creazione economica.

Inoltre, il moderno stile di programmazione aziendale è, quando è necessario eseguire un'azione, si crea una classe speciale (o ancora meglio, un framework) che eseguirà quella semplice azione. Buoni IDE Java, che aiutano qui. Generano tonnellate di schifezze automaticamente, come la respirazione.

Quindi, penso che i tuoi oggetti non abbiano il modello Builder o Factory. Non è decente creare istanze direttamente, per costruttore. Si consiglia una classe di fabbrica speciale per ogni oggetto.

Ora, quando entra in gioco la programmazione funzionale, la creazione di oggetti diventa ancora più semplice. Creerai oggetti ad ogni passo come respirando, senza nemmeno accorgertene. Quindi, mi aspetto che il tuo codice diventerà ancora più "efficiente" nella nuova era

"non fare nulla nel tuo costruttore. È solo per l'inizializzazione"

Personalmente, non vedo alcun motivo nelle linee guida. Lo considero solo un altro metodo. Il codice di factoring è necessario solo quando è possibile condividerlo.


3
la spiegazione non è ben strutturata e difficile da seguire. Basta andare dappertutto con un'affermazione contestabile che non ha collegamenti al contesto.
Nuova Alessandria,

L'affermazione veramente contestabile è "I costruttori dovrebbero creare un'istanza dei campi di un oggetto e fare qualsiasi altra inizializzazione necessaria per rendere l'oggetto pronto per l'uso". Dillo al contributore più popolare.
Val
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.