È una cattiva pratica di programmazione passare i parametri come oggetti? [duplicare]

97

Quindi, abbiamo un ragazzo a cui piace scrivere metodi che prendono Oggetti come parametri, in modo che possano essere "molto flessibili". Quindi, internamente, esegue direttamente il casting, la riflessione o il sovraccarico del metodo per gestire i diversi tipi.

Ritengo che questa sia una cattiva pratica, ma non so spiegare perché, a parte il fatto che rende più difficile la lettura. Ci sono altre ragioni più concrete per cui questa sarebbe una cattiva pratica? Cosa sarebbero questi?

Quindi, alcune persone hanno chiesto un esempio. Ha un'interfaccia definita, qualcosa come:

public void construct(Object input, Object output);

E ha intenzione di usarli inserendone diversi in una lista, in modo da creare dei bit e aggiungerli all'oggetto di output, in questo modo:

for (ConstructingThing thing : constructingThings)
    thing.construct(input, output);
return output;

Quindi, nella cosa che implementa construct , c'è una cosa di riflessione sgangherata che trova il metodo giusto che corrisponde all'ingresso / uscita e lo chiama, passando l'input / output.

Continuo a dirglielo, funziona, capisco che funzioni, ma così mette tutto in una singola classe. Stiamo parlando di riutilizzo e manutenibilità, e penso che in realtà si sta costringendo e ha meno riutilizzo di quello che pensa. La manutenibilità, anche se probabilmente alta per lui in questo momento, sarà probabilmente molto bassa per lui e per chiunque altro in futuro.

    
posta Risser 20.09.2013 - 19:57
fonte

18 risposte

103

Da un punto di vista pratico vedo questi problemi:

  • Una manciata di possibili errori del tipo di esecuzione - a meno di un sacco di controllo dinamico dei tipi che potrebbe essere evitato con il correttore di tipi forti incluso Java
  • Un sacco di cast inutili
  • Difficoltà a capire cosa fa un metodo con la sua firma

Da un punto di vista teorico vedo questi problemi:

  • Una mancanza di contratti dell'interfaccia di una classe. Se tutti i parametri sono di tipo Object , non dichiarerai nulla di informativo per le classi client.
  • Una mancanza di possibilità di sovraccarico
  • L'errata esecuzione dell'override. Sei in grado di sovrascrivere un metodo e cambiare i suoi tipi di parametri rompendo tutto ciò che è legato all'ereditarietà.
risposta data 20.09.2013 - 21:22
fonte
107

Il metodo viola il principio di sostituzione di Liskov .

Se un metodo accetta un parametro di tipo Object , dovrei aspettarmi che qualsiasi oggetto che passo al metodo funzioni.

Ciò che sembra è che sono consentite solo alcune sottoclassi di Object . L'unico modo per sapere quali tipi sono consentiti è conoscere i dettagli del metodo. In altre parole, il metodo è meno flessibile di quanto pubblicizzato.

    
risposta data 14.05.2013 - 20:14
fonte
53

Considererei le seguenti implicazioni dirette:

La leggibilità

Credo che lavorare con lui non sia esattamente l'esperienza più piacevole del mondo. Non si sa mai quale sarà il tipo alla fine o cosa succede ai parametri. Dubito che ti piaccia sprecare il tuo tempo con quello.

Digita firma

In Java, tutto è un oggetto. Quindi cosa fa questo metodo alla fine?

Velocità

Reflection, cast, type check, ecc. contribuiscono a una performance più lenta. Perché? Perché a suo parere rende le cose flessibili.

Contro la natura

Java è stato strongmente digitato sin dalla nascita. Digli di iniziare a scrivere JavaScript o Python o Ruby se vuole un dialetto digitato in modo dinamico. Il suo background è probabilmente strongmente basato su uno di essi.

Flessibilità

Per usare lo stesso termine del collega in questione, Java ha già capito la flessibilità molto tempo fa. Non ha senso guardarlo con gli occhiali da cavallo messi da un linguaggio tipizzato dinamicamente.

Interfacce, polimorfismo, covarianza e amp; contro-varianza, generici, ereditarietà, classi astratte e overloading di metodi sono solo alcune delle cose che contribuiscono alla cosiddetta "flessibilità". La flessibilità è data dal fatto che è banale specializzare praticamente qualsiasi cosa, da un dato tipo ad un altro. (Il solito caso significa specializzare un tipo superiore diretto o un tipo inferiore).

Documentazione

Uno degli obiettivi principali della programmazione è scrivere codice riutilizzabile e stabile. Come appare la documentazione del tuo esempio? Qual è il tempo di apprendimento per un altro sviluppatore prima di poter usare quella roba? Suggerirei al tuo ragazzo di scrivere codice assembler auto-modificante e rimanere responsbile per questo.

    
risposta data 15.05.2013 - 14:51
fonte
41

Ci sono già molte buone risposte qui. Voglio aggiungere un pensiero secondario:

Questo schema è molto probabilmente un " odore di codice ."

In altre parole, se devi passare un oggetto al tuo metodo per una flessibilità "definitiva", molto probabilmente hai un metodo molto poco definito . Avere parametri strongmente digitati indica un "contratto" per il chiamante (ho bisogno di cose che assomigliano a "questo"). Non essere in grado di specificare un contratto di questo tipo significa molto probabilmente che il tuo metodo fa troppo o ha uno scope definito male.

    
risposta data 20.09.2013 - 21:33
fonte
22

Elimina il tipo di controllo che il compilatore esegue e può portare a più eccezioni di runtime quando l'oggetto viene convertito nel tipo concreto. Pertanto, l'utente finale vedrà l'eccezione non gestita, a meno che i cast non validi vengano tutti catturati e gestiti correttamente.

    
risposta data 14.05.2013 - 18:55
fonte
16

Sono d'accordo con le altre risposte sul fatto che questa è generalmente una cattiva pratica.

C'è un caso specifico in cui trovo che passare un oggetto possa essere superiore: quando alla fine si ha a che fare con le stringhe, e.g. se si sta creando XML o JSON. In tal caso, invece di qualcosa di simile:

public void addAttribute(String s) { 
  //... add s to the XML
}

Preferisco:

public void addAttribute(Object o) {
   String s = String.valueOf(o);  // perhaps check for null explicitly
   // now add s to the XML
}

Ciò consente di risparmiare infinitamente% di% in% nel codice chiamante. Inoltre, funziona bene con String.valueOf di s. E, con l'autoboxing, puoi passare vararg , ints , ecc ... Poiché tutti gli oggetti hanno un floats , questo, probabilmente, non viola Liskov ecc ...

    
risposta data 20.09.2013 - 21:47
fonte
5

Sì. java dovrebbe essere rigorosamente typecast ed. Ma avere Object come parametro infrange virtualmente questa regola. Inoltre, renderà il tuo programma più soggetto a bug.

E questo è uno dei motivi principali, perché è stato introdotto generics .

    
risposta data 14.05.2013 - 18:57
fonte
5

Essere flessibili e in grado di gestire più tipi di dati è esattamente ciò per cui sono stati creati l'overloading e le interfacce del metodo. Java ha già le caratteristiche che semplificano la gestione di queste situazioni, garantendo al tempo stesso la sicurezza della compilazione tramite il controllo dei tipi.

Inoltre, se è impossibile definire in modo restrittivo su quali dati opera il metodo, allora quel metodo potrebbe fare troppo e deve essere scomposto in metodi più piccoli.

Mi piace una regola empirica di cui ho letto (nel codice completo penso). La regola afferma che dovresti essere in grado di dire quale funzione o metodo fa solo leggendo il suo nome. Se è davvero difficile definire chiaramente un metodo \ metodo usando quell'idea (o devi usare E nel nome), la funzione / metodo sta facendo davvero troppo.

    
risposta data 14.05.2013 - 19:29
fonte
2

Innanzitutto, se si passano i parametri come oggetto concreto, qualsiasi errore di utilizzo dell'oggetto errato (istanza di una classe non prevista) verrà rilevato in fase di compilazione, quindi sarà più veloce da correggere. Nell'altro caso, l'errore verrà mostrato in fase di esecuzione e sarà più difficile da rilevare e correggere.

In secondo luogo, il codice di controllo della classe reale dell'istanza, della riflessione, ecc. sarà più lento di una chiamata diretta

    
risposta data 14.05.2013 - 18:57
fonte
2

La regola che seguo è ... le definizioni dei parametri dovrebbero essere specifiche come richiesto dal metodo, né più né meno.

Se tutto ciò di cui ho bisogno è di iterare un enumerabile, il param è definito come enumerabile, non come array o elenco. Ciò consente di utilizzare qualsiasi tipo di enumerable, evitando la conversione in un tipo specifico.

Inoltre, non lo definirò come oggetto perché ho bisogno di un enumerabile. Qualsiasi oggetto che non sia enumerabile non funzionerà in questo metodo. È confuso leggere una firma del metodo e non sapere cosa significa, e solo i test di runtime possono trovare l'errore.

    
risposta data 14.05.2013 - 20:38
fonte
2

Quello che fa è gettare via tutti i vantaggi di avere un sistema di tipo statico.

Semplicemente: È molto meno chiaro a che cosa facciano tali metodi, sia per un compilatore che per le persone che leggono il codice.

Uno dei principali motivi per un sistema di tipi statici è di avere garanzie in fase di compilazione su ciò che un programma fa . Il compilatore può controllare che una parte di codice sia utilizzata correttamente e in caso contrario emettere un errore di compilazione. Metodi come quello che il collega sta scrivendo sono molto più inclini agli errori di run-time, perché mancano i controlli del compilatore. Preparati a errori in fase di esecuzione e per scrivere molti più test.

Un altro grosso problema è che non è del tutto chiaro cosa sia un metodo. Leggere un tale codice, mantenerlo o usarlo è un grande dolore.

Java ha aggiunto generici per fornire maggiori garanzie in fase di compilazione sul codice, Scala sta andando ancora oltre con il suo approccio funzionale e un sofisticato sistema di tipi con tipi covarianti e controvarianti. Il tuo collega sta andando indietro contro questa linea di sviluppo, gettando via tutti i suoi vantaggi.

Se invece usasse i metodi correttamente digitati e sovraccaricati, lui (e tutti voi) otterrebbe:

  • Verifica del tempo del compilatore per l'uso corretto di questi metodi.
  • Indica chiaramente quali combinazioni di argomenti sono consentite e quali no.
  • La possibilità di documentare correttamente e separatamente ciascuna di queste combinazioni.
  • Leggibilità e manutenibilità del codice.
  • Lo aiuterebbe a dividere i suoi metodi in parti più piccole, che sono più facili da eseguire il debug, testare e mantenere. (Vedi anche Come convincere il tuo collega sviluppatore a scrivere metodi brevi? .)
risposta data 23.05.2017 - 14:40
fonte
2

In realtà ho dovuto lavorare su un pacchetto del genere una volta, era un pacchetto GUI - il designer ha detto che proveniva da un background Pascal e pensava di non far passare nulla, ma una classe base ovunque era un'idea fantastica.

In 20 anni, lavorare con questo toolkit è stata una delle esperienze più fastidiose della mia carriera.

Finché non devi lavorare con un codice del genere, non ti accorgi di quanto ti basi sui tipi di dati nei tuoi parametri per guidarti. Se ho detto che avevo un metodo "connect (Object dest)", cosa significa? Anche se vedi "connect (sito web dell'oggetto)" non è di grande aiuto, ma "connect (sito web String)" o "connect (sito web URL)" non devi nemmeno pensarci.

Si potrebbe pensare che "connect (Object website)" sia un'ottima idea perché può prendere una stringa o un URL, ma ciò significa solo che devo indovinare quali casi il programmatore ha deciso di supportare e quali no. Nel migliore dei casi sposta la maggior parte della parte di sviluppo e di errore dello sviluppo dal tempo di codifica al tempo di compilazione, nel peggiore dei casi rende l'intero processo di sviluppo fangoso e fastidioso.

I (e la maggior parte dei programmatori Java, credo) constata costantemente catene di chiamate API esaminando un gruppo di parametri ai costruttori per vedere quale costruttore utilizzare, quindi come costruire gli oggetti che devi passare fino ad arrivare agli oggetti avere o può fare - è come mettere insieme un puzzle invece di cercare una documentazione schifosa per esempi incompleti (la mia esperienza pre-java con C).

Sebbene il codice di costruzione come un puzzle sembri ad hoc, funziona incredibilmente bene - per tutto il tempo.

So che a questa domanda è stata data risposta, ma questa è stata un'esperienza abbastanza noiosa che mi sono sentito davvero motivato a fare in modo che il minor numero possibile di persone avesse bisogno di riviverlo.

    
risposta data 15.05.2013 - 07:06
fonte
1

Per dare qualcosa di tipo dare:

  1. Gli utenti sono un'interfaccia per codificare con una descrizione semantica.
  2. Compilatori un'interfaccia da analizzare per consentire ai loro ottimizzatori di aumentare il tempo di esecuzione del codice o ridurre le dimensioni del codice più facilmente.
  3. Binding (statico) precoce che cattura gli errori di codice all'inizio del ciclo di sviluppo.

L'uso di un solo oggetto non trasmette quasi nessuna informazione, rimuovendo quindi tutte queste funzionalità che sono state inserite nella lingua per i motivi sopra elencati.

Inoltre, il sistema di tipi è stato progettato per la flessibilità con il suo è-una relazione che consente tutte queste condizioni. Quindi usalo a tuo vantaggio. Non sprecarlo creando codice non gestibile.

Ci può essere una ragione particolare per fare qualcosa di simile in un momento particolare, ma per fare questo questo in generale è sempre una cattiva pratica. Se lo fai, chiedi agli altri di esaminare il problema e vedere se può essere fatto in un altro modo. Nella maggior parte dei casi (99,999%) può essere.

In questo 0,001% che non può essere, almeno sei a bordo e sai cosa sta succedendo (anche se probabilmente hai solo bisogno di occhi nuovi sul problema).

    
risposta data 14.05.2013 - 20:31
fonte
1

Sì, questa è una cattiva pratica perché non garantisce che il metodo sia in grado di fare ciò di cui ha bisogno per ogni particolare input passato. L'obiettivo di rendere un metodo largamente utilizzabile non è un cattivo obiettivo, ma ci sono modi migliori per realizzarlo.

Per questo caso particolare, sembra una situazione perfetta per l'uso delle interfacce. Un'interfaccia garantirebbe che gli oggetti passati al metodo forniscano le informazioni necessarie e consentano di evitare la trasmissione. Ci sono alcune situazioni in cui questo non funzionerà bene (come se dovesse funzionare con classi sigillate o di base a cui non è possibile aggiungere un'implementazione dell'interfaccia), ma nel complesso un metodo dovrebbe prendere solo input validi per esso i parametri.

    
risposta data 14.05.2013 - 23:11
fonte
0

Finora risposte approfondite approfondite, devo davvero dire che, prima ancora di entrare nella filosofia e nella metodologia di alto livello, sono pigri, brevi e semplici.

Non stanno fornendo buone informazioni nel loro codice affinché altri possano mantenerlo e comprenderlo, e sono contrari al design del linguaggio di programmazione. Adoro PERL, adoro Objective-C. Tuttavia, non mi sforzo di piegare Java, C # o C ++ per quanto riguarda le mie preferenze a scapito di un codice gestibile, leggibile ed efficiente.

Sì, se hanno bisogno di cambiare il modo in cui una funzione funziona, o quali sono gli input, salva qui alcune modifiche alla linea e alcuni passaggi di refactoring. Tuttavia, il costo per gli altri che devono mantenere il codice è maggiore, per non parlare degli IDE più moderni come eclipse e Xcode in grado di gestire automaticamente il refactoring con un semplice comando del tasto destro. Inoltre, come altri hanno menzionato, il costo di elaborazione è aumentato a ogni cast, con ogni tipo di oggetto sconosciuto passato, e immagino degli acceleratori di base incorporati che il runtime eseguirà sapendo che le classi corrette vengono lanciate fuori dalla finestra.

Lo zucchero sintassi è sempre meglio di sale per lo sviluppatore, ma una dieta equilibrata di entrambi garantisce la salute del progetto e gli altri coinvolti (cliente, applicazione, altri sviluppatori, ecc.)

    
risposta data 14.05.2013 - 22:22
fonte
-2

Non ci sono problemi con questo. Nelle lingue che non hanno variabili digitate, questo è ciò che accade in ogni funzione. I parametri formali sono solo valori e il tipo è nell'oggetto che viene passato, non nel parametro formale.

Le persone ricevono Stuff Done (TM) in Lisp, Python, Ruby, ...

Sebbene tu tragga meno benefici dal controllo del tipo in fase di compilazione, anche tu risenta meno del controllo del tipo a tempo di compilazione.

La vera soluzione è usare un linguaggio dinamico che si rivolge alla macchina virtuale Java, invece di scrivere un brutto codice Java in cui ogni terzo identificatore è la parola Object .

Ho senso avere una funzione completamente generica chiamata construct che prende oggetti arbitrari.

Penso che mi piacerebbe collaborare con questo programmatore. Ci vorrebbe un po 'di pungiglione usando un linguaggio di programmazione bleroso.

Si noti che questo tipo di codice si verifica in C nell'implementazione del supporto di runtime per le lingue dinamiche implementate in C. Es. potresti vedere qualcosa di simile in un Lisp implementato in C:

/* Map each object in a list through a function, in order to produce
   a list of the return values. */

val mapcar(val function, val list)
{
   val iter = car(list);
   val out = nil;  /* nil is really a null pointer */

   for (iter = car(list); iter; iter = cdr(iter))
      push(funcall_1(function, car(iter)), out);  /* push is a C macro */

   return nreverse(out); /* destructively reverse list */
}

Il tipo val è in realtà qualcosa di simile a:

typedef struct object *val;

e struct object è una struttura complessa che si sovrappone a vari tipi di informazioni di tipo per vari tipi di oggetti.

Ho sviluppato un linguaggio di programmazione molto carino, i cui interni sono tutti fatti in questo stile, che ho deliberatamente mantenuto il più pulito possibile: più che negli interni di qualche altra implementazione linguistica che ha un approccio simile.

In questo stile, cose sorprendenti sono possibili in una piccola quantità di codice C.

    
risposta data 14.05.2013 - 23:35
fonte
-3

Supponiamo per un minuto che "Object guy" abbia ragione di usare un generico oggetto "vagamente" tipizzato. Non conosciamo le sue ragioni, ma mi ricorda il valore dell'utilizzo dell'ereditarietà e di Generics (Templates). Nella remota possibilità che Object-guy stia effettivamente cercando di dare un senso, questo è il senso che potrebbe provare a fare:

Ho sempre basato i miei oggetti su un singolo oggetto base. Questo oggetto base gestisce compiti addizionali come la gestione delle eccezioni, la registrazione, la profilazione e la garbage collection.

Ho sempre basato su oggetti matematici (quelli che potrebbero supportare operatori matematici) su una singola classe base. Questo aiuta a fornire utility per gestire le eccezioni nel caso in cui una classe derivata non gestisca una determinata operazione. Ad esempio, utilizzando una serie di Taylor nel caso in cui una rappresentazione esplicita per una funzione non sia disponibile.

Quindi non conosco le ragioni di Object-guys: forse sono solo perché gli piace la digitazione dinamica offerta da altre lingue. Ma forse sta cercando di apprezzare il valore dell'eredità.

Potrebbe anche voler esplorare Generics (Templates). Li ho usati in C ++ per definire immagini a due, tre e quattro dimensioni, in cui i pixel possono essere byte, interi, float, complessi o spettri. Ho dovuto fare un bel po 'di casting, ma il pay-off è stato significativo in termini di separazione della geometria delle immagini dal calcolo dei pixel.

    
risposta data 14.05.2013 - 23:18
fonte
-6

Non dimenticare che questo è un buon modello per linguaggi come JavaScript che non supportano il passaggio dei parametri per nome. Considera una funzione

function dialog( message, cancelButton, okText, notOkText ) { ... } 

che deve essere chiamato come

dialog( "...", true, "...", "..." );

Quando studi il codice cliente, non è facile ricordare il significato, per esempio, del terzo parametro della chiamata. La leggibilità del codice cliente soffre. Pertanto, è meglio dichiararlo con un oggetto param

function dialog( options ) { ... }

che ti consente di chiamarlo con parametri denominati:

dialog({ 
  message : "...",
  cancelButton: true,
  okText: "...",
  notOkText: "..."
  })

Principio: dovremmo ridurre il numero di parametri di una funzione (zero è eccellente, uno è frequente, due e più dovrebbe essere raro)!

    
risposta data 14.05.2013 - 22:23
fonte

Leggi altre domande sui tag