Grokking cultura Java - perché le cose sono così pesanti? Per cosa ottimizza? [chiuso]


288

In Python scrivevo molto codice. Ora, per motivi di lavoro, codice in Java. I progetti che faccio sono piuttosto piccoli, e forse Python funzionerebbe meglio, ma ci sono validi motivi non ingegneristici per usare Java (non posso entrare nei dettagli).

La sintassi Java non è un problema; è solo un'altra lingua. Ma a parte la sintassi, Java ha una cultura, una serie di metodi di sviluppo e pratiche considerate "corrette". E per ora non riesco completamente a "grok" quella cultura. Quindi apprezzerei davvero le spiegazioni o i suggerimenti nella giusta direzione.

Un esempio minimo completo è disponibile in una domanda Stack Overflow che ho iniziato: https://stackoverflow.com/questions/43619566/returning-a-result-with-several-values-the-java-way/43620339

Ho un compito - analizzare (da una singola stringa) e gestire un insieme di tre valori. In Python è un one-liner (tupla), in Pascal o C un record / struttura da 5 righe.

Secondo le risposte, l'equivalente di una struttura è disponibile nella sintassi Java e una tripla è disponibile in una libreria Apache ampiamente utilizzata - tuttavia il modo "corretto" di farlo è in realtà creando una classe separata per il valore, completa di Getter e setter. Qualcuno è stato molto gentile nel fornire un esempio completo. Aveva 47 righe di codice (beh, alcune di queste righe erano vuote).

Capisco che un'enorme comunità di sviluppo non è probabilmente "sbagliata". Quindi questo è un problema con la mia comprensione.

Le pratiche Python ottimizzano per la leggibilità (che, in quella filosofia, porta alla manutenibilità) e, successivamente, alla velocità di sviluppo. Le pratiche C ottimizzano per l'utilizzo delle risorse. Per cosa ottimizzano le pratiche Java? La mia ipotesi migliore è la scalabilità (tutto dovrebbe essere in uno stato pronto per un progetto da milioni di LOC), ma è un'ipotesi molto debole.


1
non risponderà agli aspetti tecnici della domanda ma ancora nel contesto: softwareengineering.stackexchange.com/a/63145/13899
Newtopian

74
Potresti apprezzare il saggio di Steve Yegge Execution in the Kingdom of Nouns
Colonel Panic,


3
Ecco cosa penso sia la risposta giusta. Non stai lavorando su Java perché pensi di programmare come un amministratore di sistema, non un programmatore. Per te, il software è qualcosa che usi per realizzare un determinato compito nel modo più opportuno. Ho scritto codice Java per 20 anni e alcuni dei progetti a cui ho lavorato hanno richiesto un team di 20, 2 anni per raggiungere. Java non sostituisce Python né viceversa. Entrambi fanno il loro lavoro, ma i loro lavori sono completamente diversi.
Richard,

1
Il motivo per cui Java è lo standard di fatto è perché è semplicemente giusto. È stato creato, la prima volta, da persone serie che avevano la barba prima che la barba fosse di moda. Se sei abituato a rovesciare gli script di shell per manipolare i file, Java sembra intollerabilmente gonfio. Ma se si desidera creare un cluster di server ridondante doppio in grado di servire 50 milioni di persone al giorno, supportato da cache di redis in cluster, 3 sistemi di fatturazione e un cluster di database oracle da 20 milioni di sterline. Non si utilizzerà Python, anche se si accede a il database in Python ha meno di 25 righe di codice.
Richard,

Risposte:


237

Il linguaggio Java

Credo che tutte queste risposte manchino il punto cercando di attribuire l'intento al modo in cui funziona Java. La verbosità di Java non deriva dal fatto che è orientata agli oggetti, poiché anche Python e molti altri linguaggi hanno ancora una sintassi più terser. La verbosità di Java non deriva nemmeno dal suo supporto ai modificatori di accesso. Invece, è semplicemente come Java è stato progettato e si è evoluto.

Java è stato originariamente creato come C leggermente migliorato con OO. In quanto tale, Java ha una sintassi degli anni '70. Inoltre, Java è molto prudente nell'aggiungere funzionalità al fine di mantenere la compatibilità con le versioni precedenti e consentirle di superare la prova del tempo. Java aveva aggiunto funzionalità di tendenza come i letterali XML nel 2005, quando XML era di gran moda il linguaggio sarebbe stato gonfiato con funzionalità fantasma che a nessuno importa e che ne limitano l'evoluzione 10 anni dopo. Pertanto a Java manca semplicemente molta sintassi moderna per esprimere concettualmente concetti.

Tuttavia, non c'è nulla di fondamentale che impedisce ad Java di adottare quella sintassi. Ad esempio, Java 8 ha aggiunto lambda e riferimenti ai metodi, riducendo notevolmente la verbosità in molte situazioni. Allo stesso modo Java potrebbe aggiungere il supporto per dichiarazioni di tipi di dati compatti come le classi case di Scala. Ma Java semplicemente non l'ha fatto. Si noti che i tipi di valore personalizzati sono all'orizzonte e questa funzione potrebbe introdurre una nuova sintassi per dichiararli. Suppongo che vedremo.


La cultura di Java

La storia dello sviluppo Java aziendale ci ha portato in gran parte alla cultura che vediamo oggi. Alla fine degli anni '90 / inizio anni '00, Java divenne un linguaggio estremamente popolare per le applicazioni aziendali lato server. Allora quelle applicazioni erano in gran parte scritte ad hoc e incorporavano molte preoccupazioni complesse, come API HTTP, database ed elaborazione di feed XML.

Negli anni 00 divenne chiaro che molte di queste applicazioni avevano molto in comune e le strutture per gestire queste preoccupazioni, come l'Hibernate ORM, il parser XML di Xerces, JSP e l'API servlet e EJB, divennero popolari. Tuttavia, mentre questi framework hanno ridotto gli sforzi per lavorare nel dominio particolare che hanno impostato per automatizzare, hanno richiesto la configurazione e il coordinamento. All'epoca, per qualsiasi motivo, era popolare scrivere framework per soddisfare il caso d'uso più complesso e quindi queste librerie erano complicate da configurare e integrare. E nel tempo sono diventati sempre più complessi man mano che accumulavano funzionalità. Lo sviluppo dell'impresa Java gradualmente divenne sempre più legato al collegamento di librerie di terze parti e meno alla scrittura di algoritmi.

Alla fine la noiosa configurazione e gestione degli strumenti aziendali divenne abbastanza dolorosa che i quadri, in particolare il quadro Spring, arrivarono per gestire la gestione. Potresti mettere tutta la tua configurazione in un unico posto, la teoria andava e lo strumento di configurazione avrebbe quindi configurato i pezzi e li avrebbe collegati. Sfortunatamente questi "quadri di riferimento" hanno aggiunto maggiore astrazione e complessità all'intera sfera di cera.

Negli ultimi anni sono cresciute in popolarità più biblioteche più leggere. Tuttavia, un'intera generazione di programmatori Java ha raggiunto la maggiore età durante la crescita di pesanti framework aziendali. I loro modelli di ruolo, quelli che sviluppano i framework, hanno scritto factory factory e caricatori di bean di configurazione proxy. Dovevano configurare e integrare queste mostruosità quotidianamente. Di conseguenza, la cultura della comunità nel suo insieme ha seguito l'esempio di questi schemi e tendeva a progettare male.


15
La cosa divertente è che altre lingue sembrano raccogliere alcuni "java-ismi". Le funzionalità di PHP OO sono fortemente ispirate a Java, e oggigiorno JavaScript è spesso criticato per la sua enorme quantità di framework e per quanto sia difficile raccogliere tutte le dipendenze e avviare una nuova applicazione.
marcus,

14
@marcus forse perché alcune persone hanno finalmente imparato la cosa "non reinventare la ruota"? Le dipendenze sono il costo di non reinventare la ruota dopo tutto
Walfrat,

9
"Terse" si traduce spesso in arcane, "intelligenti" , code-golf-line.
Tulains Córdova,

7
Risposta premurosa e ben scritta. Contesto storico. Relativamente nonopinionated. Ben fatto.
Cheezmeister,

10
@ TulainsCórdova Concordo sul fatto che dovremmo "evitare ... trucchi intelligenti come la peste" (Donald Knuth), ma ciò non significa scrivere un forciclo quando mapsarebbe più appropriato (e leggibile). C'è un mezzo felice.
Brian McCutchon,

73

Credo di avere una risposta per uno dei punti sollevati che non sono stati sollevati da altri.

Java ottimizza il rilevamento degli errori del programmatore in fase di compilazione senza mai fare ipotesi

In generale, Java tende a dedurre fatti sul codice sorgente solo dopo che il programmatore ha già esplicitamente espresso il suo intento. Il compilatore Java non fa mai ipotesi sul codice e utilizzerà l'inferenza per ridurre il codice ridondante.

Il motivo dietro questa filosofia è che il programmatore è solo umano. Ciò che scriviamo non è sempre ciò che realmente intendiamo fare per il programma. Il linguaggio Java cerca di mitigare alcuni di questi problemi costringendo gli sviluppatori a dichiarare sempre esplicitamente i loro tipi. Questo è solo un modo per ricontrollare che il codice che è stato scritto fa effettivamente ciò che era previsto.

Alcune altre lingue spingono ulteriormente questa logica controllando pre-condizioni, post-condizioni e invarianti (anche se non sono sicuro che lo facciano al momento della compilazione). Questi sono modi ancora più estremi per il programmatore per fare in modo che il compilatore ricontrolla il proprio lavoro.

Nel tuo caso, ciò significa che, affinché il compilatore possa garantire che stai effettivamente restituendo i tipi che ritieni di restituire, devi fornire tali informazioni al compilatore.

In Java ci sono due modi per farlo:

  1. Uso Triplet<A, B, C>come tipo di ritorno (che in realtà dovrebbe essere in java.util, e non posso davvero spiegare perché non lo è. Tanto più che JDK8 introdurre Function, BiFunction, Consumer, BiConsumer, ecc ... sembra proprio che Paire Tripletalmeno avrebbe senso. Ma sto divagando )

  2. Crea il tuo tipo di valore a tale scopo, in cui ogni campo è nominato e digitato correttamente.

In entrambi i casi, il compilatore può garantire che la funzione restituisca i tipi dichiarati e che il chiamante realizzi quale sia il tipo di ciascun campo restituito e li utilizzi di conseguenza.

Alcune lingue forniscono allo stesso tempo il controllo del tipo statico e l'inferenza del tipo, ma ciò lascia la porta aperta a una sottile classe di problemi di mancata corrispondenza del tipo. Laddove lo sviluppatore intendesse restituire un valore di un certo tipo, ma in realtà ne restituisca un altro e il compilatore STILL accetta il codice perché accade che, per coincidenza, sia la funzione che il chiamante utilizzano solo metodi che possono essere applicati a entrambi e i tipi reali.

Prendi in considerazione qualcosa del genere in Typescript (o tipo di flusso) in cui viene utilizzata l'inferenza di tipo anziché la digitazione esplicita.

function parseDurationInMillis(csvLine){
    // here the developer intends to return a Number, 
    // but actually it's a String
    return csv.firstField();
}

// Compiler infers that parseDurationInMillis is String, so it does
// string concatenation and infers that plusTwoSeconds is String
// Developer actually intended Number
var plusTwoSeconds = 2000 + parseDurationInMillis(csvLine);

Questo è un caso insignificante, ovviamente, ma può essere molto più sottile e portare a problemi di debug difficili, perché il codice sembra giusto. Questo tipo di problemi è completamente evitato in Java, ed è ciò su cui è progettato l'intero linguaggio.


Si noti che seguendo i principi orientati agli oggetti e la modellazione guidata dal dominio, il caso di analisi della durata nella domanda collegata potrebbe anche essere restituito come java.time.Durationoggetto, il che sarebbe molto più esplicito di entrambi i casi di cui sopra.


38
Affermare che Java ottimizza per la correttezza del codice potrebbe essere un punto valido, ma mi sembra (programmando molte lingue, recentemente un po 'di Java) che la lingua non funziona o è estremamente inefficiente nel farlo. Certo, Java è vecchio e molte cose non esistevano allora, ma ci sono alternative moderne che forniscono probabilmente un migliore controllo della correttezza, come Haskell (e oserei dire? Rust o Go). Tutta la leggerezza di Java non è necessaria per questo obiettivo. - E inoltre, questo non spiega affatto la cultura, ma Solomonoff ha rivelato che la cultura è comunque BS.
tomsmeding,

8
Quell'esempio non dimostra che l'inferenza del tipo sia il problema, ma solo la decisione di JavaScript di consentire conversioni implicite in un linguaggio dinamico è stupida. Qualsiasi linguaggio dinamico sano o linguaggio propagandato staticamente con inferenza di tipo non lo permetterebbe. Ad esempio per entrambi i tipi: python genererebbe un runtime tranne, Haskell non compilerebbe.
Voo,

39
@tomsmeding Vale la pena notare che questo argomento è particolarmente sciocco perché Haskell è in circolazione da 5 anni più di Java. Java potrebbe avere un sistema di tipi, ma non aveva nemmeno la verifica della correttezza allo stato dell'arte quando è stato rilasciato.
Alexis King,

21
"Java ottimizza per il rilevamento degli errori in fase di compilazione" - No. Lo fornisce ma, come altri hanno notato, certamente non ottimizza per esso in alcun senso della parola. Inoltre, questo argomento è del tutto estraneo alla domanda di OP perché altre lingue che fanno in realtà ottimizzano per il rilevamento degli errori in fase di compilazione hanno molto più leggero sintassi per scenari simili. La verbosità esagerata di Java non ha esattamente nulla a che fare con la sua verifica in fase di compilazione.
Konrad Rudolph,

19
@KonradRudolph Sì, questa risposta è completamente priva di senso, anche se suppongo sia emotivamente attraente. Non sono sicuro del perché abbia così tanti voti. Haskell (o forse anche più significativamente, Idris) ottimizza molto di più per la correttezza rispetto a Java, e ha tuple con una sintassi leggera. Inoltre, la definizione di un tipo di dati è di tipo one-liner e ottieni tutto ciò che la versione Java otterrebbe. Questa risposta è una scusa per il design del linguaggio scadente e la verbosità inutile, ma suona bene se ti piace Java e non hai familiarità con altre lingue.
Alexis King,

50

Java e Python sono le due lingue che utilizzo di più ma vengo dall'altra direzione. Cioè, ero nel profondo del mondo Java prima di iniziare a usare Python, quindi potrei essere in grado di aiutare. Penso che la risposta alla domanda più ampia del "perché le cose sono così pesanti" si riduce a 2 cose:

  • I costi relativi allo sviluppo tra i due sono come l'aria in un pallone aerostatico a palloncino lungo. Puoi spremere una parte del palloncino e un'altra parte si gonfia. Python tende a spremere la prima parte. Java stringe la parte successiva.
  • Java non ha ancora alcune funzionalità che eliminerebbero parte di questo peso. Java 8 ha fatto un grosso problema in questo, ma la cultura non ha completamente digerito i cambiamenti. Java potrebbe usare alcune altre cose come yield.

Java "ottimizza" per software di alto valore che verrà mantenuto per molti anni da grandi gruppi di persone. Ho avuto l'esperienza di scrivere cose in Python e di guardarle un anno dopo e di essere sconcertato dal mio codice. In Java posso guardare piccoli frammenti di codice di altre persone e sapere immediatamente cosa fa. In Python non puoi davvero farlo. Non è che uno sia meglio come sembri rendersi conto, hanno solo costi diversi.

Nel caso specifico menzionato, non ci sono tuple. La soluzione semplice è creare una classe con valori pubblici. Quando uscì Java per la prima volta, la gente lo faceva abbastanza regolarmente. Il primo problema è che si tratta di un mal di testa da mantenimento. Se hai bisogno di aggiungere un po 'di sicurezza logica o di thread o vuoi usare il polimorfismo, dovrai almeno toccare ogni classe che interagisce con quell'oggetto "tuple-esque". In Python ci sono soluzioni come questa, __getattr__ecc. Quindi non è così terribile.

Ci sono alcune cattive abitudini (IMO) intorno a questo però. In questo caso, se vuoi una tupla, mi chiedo perché lo trasformi in un oggetto mutevole. Dovresti aver bisogno solo di getter (in una nota a margine, odio la convenzione get / set ma è quello che è.) Penso che una classe nuda (mutabile o no) possa essere utile in un contesto privato o pacchetto-privato in Java . Cioè, limitando i riferimenti nel progetto alla classe, è possibile eseguire il refactoring in seguito, se necessario, senza modificare l'interfaccia pubblica della classe. Ecco un esempio di come è possibile creare un semplice oggetto immutabile:

public class Blah 
{
  public static Blah blah(long number, boolean isInSeconds, boolean lessThanOneMillis)
  {
    return new Blah(number, isInSeconds, lessThanOneMillis);
  }

  private final long number;
  private final boolean isInSeconds;
  private final boolean lessThanOneMillis;

  public Blah(long number, boolean isInSeconds, boolean lessThanOneMillis)
  {
    this.number = number;
    this.isInSeconds = isInSeconds;
    this.lessThanOneMillis = lessThanOneMillis;
  }

  public long getNumber()
  {
    return number;
  }

  public boolean isInSeconds()
  {
    return isInSeconds;
  }

  public boolean isLessThanOneMillis()
  {
    return lessThanOneMillis;
  }
}

Questo è un tipo di modello che uso. Se non stai utilizzando un IDE, dovresti iniziare. Genererà i getter (e i setter se ne hai bisogno) per te, quindi non è così doloroso.

Mi dispiacerebbe se non facessi notare che esiste già un tipo che sembra soddisfare la maggior parte delle tue esigenze qui . A parte questo, l'approccio che stai usando non è adatto a Java in quanto gioca ai suoi punti deboli, non ai suoi punti di forza. Ecco un semplice miglioramento:

public class Blah 
{
  public static Blah fromSeconds(long number)
  {
    return new Blah(number * 1000_000);
  }

  public static Blah fromMills(long number)
  {
    return new Blah(number * 1000);
  }

  public static Blah fromNanos(long number)
  {
    return new Blah(number);
  }

  private final long nanos;

  private Blah(long nanos)
  {
    this.nanos = nanos;
  }

  public long getNanos()
  {
    return nanos;
  }

  public long getMillis()
  {
    return getNanos() / 1000; // or round, whatever your logic is
  }

  public long getSeconds()
  {
    return getMillis() / 1000; // or round, whatever your logic is
  }

  /* I don't really know what this is about but I hope you get the idea */
  public boolean isLessThanOneMillis()
  {
    return getMillis() < 1;
  }
}

I commenti non sono per una discussione estesa; questa conversazione è stata spostata in chat .
maple_shaft

2
Vorrei suggerire di guardare anche Scala rispetto alle "più moderne" funzionalità Java ...
MikeW,

1
@MikeW In realtà ho iniziato con Scala già nelle prime versioni e sono stato attivo nei forum scala per un po '. Penso che sia un grande risultato nella progettazione del linguaggio, ma sono giunto alla conclusione che non era proprio quello che stavo cercando e che ero un perno quadrato in quella comunità. Dovrei guardarlo di nuovo perché probabilmente è cambiato in modo significativo da quel momento.
JimmyJames,

Questa risposta non affronta l'aspetto di valore approssimativo sollevato dall'OP.
ErikE

@ErikE Le parole "valore approssimativo" non compaiono nel testo della domanda. Se puoi indicarmi la parte specifica della domanda che non ho affrontato, posso provare a farlo.
JimmyJames,

41

Prima di arrabbiarti troppo con Java, leggi la mia risposta nel tuo altro post .

Una delle tue lamentele è la necessità di creare una classe solo per restituire una serie di valori come risposta. Questa è una preoccupazione valida che credo mostri che le tue intuizioni di programmazione sono sulla buona strada! Tuttavia, penso che ad altre risposte manchi il segno rimanendo fedele all'anti-schema dell'ossessione primitiva a cui ti sei impegnato. E Java non ha la stessa facilità di lavoro con più primitive di Python, dove puoi restituire più valori in modo nativo e assegnarli facilmente a più variabili.

Ma una volta che inizi a pensare a ciò che un ApproximateDurationtipo fa per te, ti rendi conto che non è così limitato a "solo una classe apparentemente inutile per restituire tre valori". Il concetto rappresentato da questa classe è in realtà uno dei concetti di business del tuo dominio principale: la necessità di essere in grado di rappresentare i tempi in modo approssimativo e confrontarli. Questo deve far parte del linguaggio onnipresente del nucleo dell'applicazione, con un buon supporto di oggetti e domini, in modo che possa essere testato, modulare, riutilizzabile e utile.

Il tuo codice che somma insieme le durate approssimative (o le durate con un margine di errore, comunque tu lo rappresenti) è interamente procedurale o c'è qualche oggettività ad esso? Proporrei che un buon design intorno alla somma delle durate approssimative avrebbe dettato farlo al di fuori di qualsiasi codice di consumo, all'interno di una classe che può essere testata. Penso che l'utilizzo di questo tipo di oggetto di dominio abbia un effetto a catena nel tuo codice che ti aiuta ad allontanarti dalle fasi procedurali linea per linea per realizzare un singolo compito di alto livello (anche se con molte responsabilità), verso classi a responsabilità singola che sono liberi dai conflitti di preoccupazioni diverse.

Ad esempio, supponiamo che tu impari di più su quale precisione o scala sono effettivamente necessari per il corretto funzionamento della somma e del confronto della durata e scopri che hai bisogno di un flag intermedio per indicare "errore di circa 32 millisecondi" (vicino al quadrato radice di 1000, quindi a metà logaritmicamente tra 1 e 1000). Se ti sei limitato al codice che utilizza le primitive per rappresentarlo, dovrai trovare ogni posizione nel codice in cui ti trovi is_in_seconds,is_under_1mse cambiarlo inis_in_seconds,is_about_32_ms,is_under_1ms. Tutto dovrebbe cambiare dappertutto! Creare una classe la cui responsabilità è quella di registrare il margine di errore in modo che possa essere consumato altrove libera i tuoi consumatori dalla conoscenza dei dettagli di quali margini di errore contano o da qualsiasi cosa su come si combinano e consente loro di specificare solo il margine di errore che è rilevante al momento. (Cioè, nessun codice che consuma il cui margine di errore è corretto è costretto a cambiare quando si aggiunge un nuovo margine di livello di errore nella classe, poiché tutti i vecchi margini di errore sono ancora validi).

Dichiarazione di chiusura

La lamentela sulla pesantezza di Java sembra quindi scomparire man mano che ci si avvicina ai principi di SOLID e GRASP e all'ingegneria del software più avanzata.

appendice

Aggiungerò completamente gratuitamente e ingiustamente che le proprietà automatiche di C # e la capacità di assegnare proprietà get-only nei costruttori aiuta a ripulire ulteriormente il codice un po 'disordinato che "il modo Java" richiederà (con campi di supporto privati ​​espliciti e funzioni getter / setter) :

// Warning: C# code!
public sealed class ApproximateDuration {
   public ApproximateDuration(int lowMilliseconds, int highMilliseconds) {
      LowMilliseconds = lowMilliseconds;
      HighMilliseconds = highMilliseconds;
   }
   public int LowMilliseconds { get; }
   public int HighMilliseconds { get; }
}

Ecco un'implementazione Java di quanto sopra:

public final class ApproximateDuration {
  private final int lowMilliseconds;
  private final int highMilliseconds;

  public ApproximateDuration(int lowMilliseconds, int highMilliseconds) {
    this.lowMilliseconds = lowMilliseconds;
    this.highMilliseconds = highMilliseconds;
  }

  public int getLowMilliseconds() {
    return lowMilliseconds;
  }

  public int getHighMilliseconds() {
    return highMilliseconds;
  }
}

Questo è dannatamente pulito. Nota l'uso molto importante e intenzionale dell'immutabilità - questo sembra cruciale per questo particolare tipo di classe di valore.

Del resto, questa classe è anche un candidato decente per essere un structtipo di valore. Alcuni test mostreranno se il passaggio a una struttura ha un vantaggio in termini di prestazioni di runtime (potrebbe).


9
Penso che questa sia la mia risposta preferita. Trovo che quando promuovo una classe di detestabili usa e getta come questa in qualcosa di cresciuto, diventa la casa di tutte le responsabilità correlate e zing! tutto diventa più pulito! E di solito imparo allo stesso tempo qualcosa di interessante sullo spazio del problema. Ovviamente c'è un'abilità nell'evitare l'ingegneria eccessiva ... ma Gosling non è stato stupido quando ha lasciato fuori la sintassi delle tuple, come con i GOTO. La tua dichiarazione di chiusura è eccellente. Grazie!
SusanW,

2
Se ti piace questa risposta, leggi su Domain-Driven Design. Su questo argomento ha molto da dire sulla rappresentazione dei concetti di dominio nel codice.
neontapir,

@neontapir Sì, grazie! Sapevo che c'era un nome per questo! :-) Anche se devo dire che lo preferisco quando un concetto di dominio appare organicamente da un po 'di refactoring ispirato (come questo!) ... è un po' come quando riesci a risolvere il tuo problema di gravità del 19 ° secolo scoprendo Nettuno .. .
SusanW

@SusanW Sono d'accordo. Il refactoring per rimuovere l'ossessione primitiva può essere un modo eccellente per scoprire concetti di dominio!
neontapir,

Ho aggiunto una versione Java dell'esempio come discusso. Non mi occupo di tutto lo zucchero sintattico magico in C #, quindi se manca qualcosa, fammi sapere.
JimmyJames

24

Sia Python che Java sono ottimizzati per la manutenibilità secondo la filosofia dei loro progettisti, ma hanno idee molto diverse su come raggiungere questo obiettivo.

Python è un linguaggio multi-paradigma che ottimizza per chiarezza e semplicità del codice (facile da leggere e scrivere).

Java è (tradizionalmente) un linguaggio OO basato su classi a paradigma singolo che ottimizza per esplicitare e coerenza , anche a costo di un codice più dettagliato.

Una tupla Python è una struttura di dati con un numero fisso di campi. La stessa funzionalità può essere raggiunta da una classe regolare con campi dichiarati esplicitamente. In Python è naturale fornire le tuple come alternativa alle classi perché consente di semplificare notevolmente il codice, soprattutto grazie al supporto di sintassi integrato per le tuple.

Ma questo non corrisponde realmente alla cultura Java per fornire tali scorciatoie, dal momento che puoi già usare classi dichiarate esplicite. Non è necessario introdurre un diverso tipo di struttura dati solo per salvare alcune righe di codice ed evitare alcune dichiarazioni.

Java preferisce un singolo concetto (classi) applicato coerentemente con un minimo di zucchero sintattico per casi speciali, mentre Python fornisce più strumenti e un sacco di zucchero sintattico per consentire di scegliere il più conveniente per uno scopo particolare.


+1, ma come si combina con linguaggi digitati staticamente con classi, come Scala, che ha tuttavia trovato la necessità di introdurre le tuple? Penso che le tuple siano solo la soluzione più ordinata in alcuni casi, anche per le lingue con le classi.
Andres F.

@AndresF .: Cosa intendi con mesh? Scala è un linguaggio distinto da Java e ha diversi principi di progettazione e modi di dire.
Jacques B

Lo intendevo in risposta al tuo quinto paragrafo, che inizia con "ma questo non corrisponde alla cultura Java [...]". In effetti, sono d'accordo che la verbosità fa parte della cultura di Java, ma la mancanza di tuple non può essere perché "usano già le classi dichiarate esplicitamente" - dopotutto, Scala ha anche classi esplicite (e introduce le classi di casi più concise ), ma permette anche le tuple! Alla fine, penso che probabilmente Java non abbia introdotto le tuple non solo perché le classi possono ottenere lo stesso (con più disordine), ma anche perché la sua sintassi deve essere familiare ai programmatori C e C non ha tuple.
Andres F.

@AndresF .: Scala non ha un'avversione nei confronti di molteplici modi di fare le cose, combina le caratteristiche sia del paradigma funzionale che dell'OO classico. In questo modo è più vicino alla filosofia Python.
Jacques B

@AndresF .: Sì, Java ha iniziato con una sintassi vicina a C / C ++, ma semplificata molto. C # è iniziato come una copia quasi esatta di Java, ma negli anni ha aggiunto molte funzionalità tra cui le tuple. Quindi è davvero una differenza nella filosofia del design del linguaggio. (Le versioni successive di Java sembrano però essere meno dogmatiche. Le funzioni di ordine superiore sono state inizialmente respinte perché si potevano fare le stesse classi, ma ora sono state introdotte. Quindi forse stiamo assistendo a un cambiamento nella filosofia.)
JacquesB,

16

Non cercare pratiche; di solito è una cattiva idea, come detto in Best practice BAD, modelli BUONO? . So che non stai chiedendo le migliori pratiche, ma penso ancora che troverai alcuni elementi pertinenti.

Cercare una soluzione al tuo problema è meglio di una pratica e il tuo problema non è una tupla per restituire rapidamente tre valori in Java:

  • Ci sono array
  • È possibile restituire un array come elenco in un one-liner: Arrays.asList (...)
  • Se si desidera mantenere il lato oggetto con il minor numero possibile di boilerplate (e senza lombok):

class MyTuple {
    public final long value_in_milliseconds;
    public final boolean is_in_seconds;
    public final boolean is_under_1ms;
    public MyTuple(long value_in_milliseconds,....){
        ...
    }
 }

Qui hai un oggetto immutabile contenente solo i tuoi dati e pubblico, quindi non c'è bisogno di getter. Si noti che tuttavia se si utilizzano alcuni strumenti di serializzazione o livello di persistenza come un ORM, comunemente usano getter / setter (e possono accettare un parametro per utilizzare i campi anziché getter / setter). Ed è per questo che queste pratiche sono usate molto. Quindi, se vuoi conoscere le pratiche, è meglio capire perché sono qui per un loro migliore utilizzo.

Infine: uso i getter perché utilizzo molti strumenti di serializzazione, ma non li scrivo nemmeno; Uso lombok: utilizzo le scorciatoie fornite dal mio IDE.


9
C'è ancora valore nella comprensione dei modi di dire comuni che si incontrano in varie lingue, poiché diventano uno standard di fatto più accessibile. Questi idiomi tendono a rientrare nel campo delle "migliori pratiche" per qualsiasi motivo.
Berin Loritsch,

3
Ehi, una soluzione ragionevole al problema. Sembra abbastanza ragionevole scrivere una classe (/ struct) con pochi membri pubblici invece di una soluzione OOP completamente ingegnerizzata con [gs] etters.
tomsmeding,

3
Questo porta a gonfiarsi perché, a differenza di Python, la ricerca o una soluzione più breve ed elegante non sono incoraggiate. Ma il lato positivo è che il gonfio sarà più o meno lo stesso tipo per qualsiasi sviluppatore Java. Pertanto, esiste una certa maggiore manutenibilità perché gli sviluppatori sono più intercambiabili e se due progetti vengono uniti o due team divergono involontariamente, c'è meno differenza di stile da combattere.
Mikhail Ramendik,

2
@MikhailRamendik È un compromesso tra YAGNI e OOP. OOP ortodossa afferma che ogni singolo oggetto dovrebbe essere sostituibile; l'unica cosa che conta è l'interfaccia pubblica dell'oggetto. Ciò consente di scrivere codice meno focalizzato su oggetti concreti e più su interfacce; e poiché i campi non possono essere parti di un'interfaccia, non li si espone mai. Questo può rendere il codice molto più facile da mantenere a lungo termine. Inoltre, ti consente di prevenire stati non validi. Nel tuo esempio originale, è possibile avere un oggetto che sia "<ms" e "s", che si escludono a vicenda. Questo è male.
Luaan,

2
@PeterMortensen È una libreria Java che fa molte cose abbastanza indipendenti. È molto buono però. Ecco le caratteristiche
Michael,

11

A proposito di idiomi Java in generale:

Ci sono vari motivi per cui Java ha classi per tutto. Per quanto ne so, il motivo principale è:

Java dovrebbe essere facile da imparare per i principianti. Più le cose sono esplicite, più è difficile perdere dettagli importanti. Accade meno magia che sarebbe difficile da comprendere per i principianti.


Per quanto riguarda il tuo esempio specifico: la linea di argomento per una classe separata è questa: se queste tre cose sono sufficientemente correlate tra loro da essere restituite come un valore, vale la pena nominare quella "cosa". E introdurre un nome per un gruppo di cose strutturate in modo comune significa definire una classe.

Puoi ridurre la piastra della caldaia con strumenti come Lombok:

@Value
class MyTuple {
    long value_in_milliseconds;
    boolean is_in_seconds;
    boolean is_under_1ms;
}

5
È anche il progetto AutoValue di Google: github.com/google/auto/blob/master/value/userguide/index.md
Ivan

Questo è anche il motivo per cui i programmi Java possono essere mantenuti
Thorbjørn Ravn Andersen,

7

Ci sono molte cose che si potrebbero dire sulla cultura Java, ma penso che nel caso in cui ti trovi di fronte in questo momento, ci sono alcuni aspetti significativi:

  1. Il codice della biblioteca viene scritto una volta ma usato molto più spesso. Mentre è bello ridurre al minimo il sovraccarico di scrivere la libreria, probabilmente è più utile nel lungo periodo scrivere in un modo che minimizzi il sovraccarico dell'uso della libreria.
  2. Ciò significa che i tipi di auto-documentazione sono fantastici: i nomi dei metodi aiutano a chiarire cosa sta succedendo e cosa stai uscendo da un oggetto.
  3. La digitazione statica è uno strumento molto utile per eliminare determinate classi di errori. Certamente non risolve tutto (alla gente piace scherzare su Haskell che una volta che il sistema dei tipi accetta il tuo codice, probabilmente è corretto), ma rende molto facile rendere certi tipi di cose sbagliate impossibili.
  4. Scrivere il codice della biblioteca significa specificare i contratti. La definizione di interfacce per argomenti e tipi di risultati rende più chiari i confini dei contratti. Se qualcosa accetta o produce una tupla, non si può dire se sia il tipo di tupla che dovresti effettivamente ricevere o produrre, e c'è ben poco in termini di vincoli su un tipo così generico (ha anche il giusto numero di elementi? quelli del tipo che ti aspettavi?).

Classi "Struct" con campi

Come hanno già detto altre risposte, puoi semplicemente usare una classe con campi pubblici. Se li rendi definitivi, otterrai una classe immutabile e li inizializzeresti con il costruttore:

   class ParseResult0 {
      public final long millis;
      public final boolean isSeconds;
      public final boolean isLessThanOneMilli;

      public ParseResult0(long millis, boolean isSeconds, boolean isLessThanOneMilli) {
         this.millis = millis;
         this.isSeconds = isSeconds;
         this.isLessThanOneMilli = isLessThanOneMilli;
      }
   }

Ovviamente, questo significa che sei legato a una particolare classe e tutto ciò che deve mai produrre o consumare un risultato di analisi deve usare questa classe. Per alcune applicazioni, va bene. Per altri, ciò può causare dolore. Gran parte del codice Java riguarda la definizione dei contratti e questo ti porterà in genere alle interfacce.

Un altro inconveniente è che con un approccio basato sulla classe, stai esponendo campi e tutti questi campi devono avere valori. Ad esempio, isSeconds e millis devono sempre avere un certo valore, anche se isLessThanOneMilli è true. Quale dovrebbe essere l'interpretazione del valore del campo millis quando isLessThanOneMilli è vero?

"Strutture" come interfacce

Con i metodi statici consentiti nelle interfacce, in realtà è relativamente facile creare tipi immutabili senza un sacco di sovraccarico sintattico. Ad esempio, potrei implementare il tipo di struttura dei risultati di cui stai parlando come qualcosa del genere:

   interface ParseResult {
      long getMillis();

      boolean isSeconds();

      boolean isLessThanOneMilli();

      static ParseResult from(long millis, boolean isSeconds, boolean isLessThanOneMill) {
         return new ParseResult() {
            @Override
            public boolean isSeconds() {
               return isSeconds;
            }

            @Override
            public boolean isLessThanOneMilli() {
               return isLessThanOneMill;
            }

            @Override
            public long getMillis() {
               return millis;
            }
         };
      }
   }

È ancora un sacco di punti di forza, sono assolutamente d'accordo, ma ci sono anche un paio di vantaggi e penso che questi inizino a rispondere ad alcune delle tue domande principali.

Con una struttura come questo risultato di analisi, il contratto del tuo parser è definito in modo molto chiaro. In Python, una tupla non è veramente distinta da un'altra tupla. In Java, la tipizzazione statica è disponibile, quindi escludiamo già alcune classi di errori. Ad esempio, se stai restituendo una tupla in Python e vuoi restituire la tupla (millis, isSeconds, isLessThanOneMilli), puoi accidentalmente fare:

return (true, 500, false)

quando intendevi:

return (500, true, false)

Con questo tipo di interfaccia Java, non è possibile compilare:

return ParseResult.from(true, 500, false);

affatto. Devi fare:

return ParseResult.from(500, true, false);

Questo è un vantaggio delle lingue tipicamente statiche in generale.

Questo approccio inizia anche a darti la possibilità di limitare quali valori puoi ottenere. Ad esempio, quando si chiama getMillis (), è possibile verificare se isLessThanOneMilli () è true e, in caso affermativo, generare un IllegalStateException (ad esempio), poiché in questo caso non esiste un valore significativo di millis.

Rendendo difficile fare la cosa sbagliata

Nell'esempio di interfaccia sopra, hai ancora il problema che potresti scambiare accidentalmente gli argomenti isSeconds e isLessThanOneMilli, poiché hanno lo stesso tipo.

In pratica, potresti voler utilizzare TimeUnit e la durata, in modo da ottenere un risultato come:

   interface Duration {
      TimeUnit getTimeUnit();

      long getDuration();

      static Duration from(TimeUnit unit, long duration) {
         return new Duration() {
            @Override
            public TimeUnit getTimeUnit() {
               return unit;
            }

            @Override
            public long getDuration() {
               return duration;
            }
         };
      }
   }

   interface ParseResult2 {

      boolean isLessThanOneMilli();

      Duration getDuration();

      static ParseResult2 from(TimeUnit unit, long duration) {
         Duration d = Duration.from(unit, duration);
         return new ParseResult2() {
            @Override
            public boolean isLessThanOneMilli() {
               return false;
            }

            @Override
            public Duration getDuration() {
               return d;
            }
         };
      }

      static ParseResult2 lessThanOneMilli() {
         return new ParseResult2() {
            @Override
            public boolean isLessThanOneMilli() {
               return true;
            }

            @Override
            public Duration getDuration() {
               throw new IllegalStateException();
            }
         };
      }
   }

Sta diventando molto più codice, ma devi solo scriverlo una volta e (supponendo che tu abbia documentato correttamente le cose), le persone che finiscono per usare il tuo codice non devono indovinare il significato del risultato, e non può fare accidentalmente cose come result[0]quando significano result[1]. Puoi comunque creare le istanze in modo piuttosto succinto, e ottenere i dati da esse non è poi così difficile:

  ParseResult2 x = ParseResult2.from(TimeUnit.MILLISECONDS, 32);
  ParseResult2 y = ParseResult2.lessThanOneMilli();

Nota che potresti effettivamente fare qualcosa del genere anche con l'approccio basato sulla classe. Basta specificare i costruttori per i diversi casi. Tuttavia, hai ancora il problema su cosa inizializzare gli altri campi e non puoi impedirne l'accesso.

Un'altra risposta ha affermato che la natura di tipo enterprise di Java significa che per la maggior parte del tempo, stai componendo altre librerie già esistenti o scrivendo librerie che altre persone possono usare. L' API pubblica non dovrebbe richiedere molto tempo a consultare la documentazione per decifrare i tipi di risultati se può essere evitato.

Hai solo scrivere queste strutture una volta, ma si creano loro molte volte, quindi è ancora voglio che la creazione concisa (che si ottiene). La digitazione statica si assicura che i dati che stai ottenendo da loro siano quelli che ti aspetti.

Ora, tutto ciò detto, ci sono ancora posti in cui semplici tuple o liste possono avere molto senso. Potrebbero esserci meno costi generali nel restituire una matrice di qualcosa, e se questo è il caso (e tale sovraccarico è significativo, cosa che determineresti con la profilazione), allora usare una semplice matrice di valori internamente potrebbe avere molto senso. Probabilmente l'API pubblica dovrebbe avere tipi chiaramente definiti.


Ho usato il mio enum invece di TimeUnit alla fine, perché ho appena aggiunto UNDER1MS insieme alle unità di tempo effettive. Quindi ora ho solo due campi, non tre. (In teoria, potrei usare impropriamente TimeUnit.NANOSECONDS, ma sarebbe molto confuso.)
Mikhail Ramendik,

Non ho creato un getter, ma ora vedo come un getter mi consentirebbe di generare un'eccezione se, con originalTimeUnit=DurationTimeUnit.UNDER1MS, il chiamante provasse a leggere il valore in millisecondi.
Mikhail Ramendik,

So che l'hai menzionato, ma ricorda che il tuo esempio con le tuple di pitone riguarda in realtà le tuple dinamiche contro quelle statiche; non dice molto sulle tuple stesse. È possibile avere digitato staticamente tuple, che non riuscirà a compilare, come sono sicuro di sapere :)
Andres F.

1
@Veedrac Non sono sicuro di cosa intendi. Il mio punto era che è OK scrivere il codice della libreria in modo più verbale in questo modo (anche se impiega più tempo), poiché si impiegherà più tempo a utilizzare il codice che a scriverlo .
Joshua Taylor,

1
@Veedrac Sì, è vero. Ma mi piacerebbe sostengo che non v'è una distinzione tra il codice che verrà riutilizzata per significato, e il codice che non lo farà. Ad esempio, in un pacchetto Java, alcune delle classi sono pubbliche e vengono utilizzate da altre posizioni. Tali API dovrebbero essere ben ponderate. Internamente, ci sono classi che potrebbero aver bisogno solo di parlarsi. Questi accoppiamenti interni sono i luoghi in cui le strutture veloci e sporche (ad es. Un oggetto [] che dovrebbe avere tre elementi) potrebbero andare bene. Per l'API pubblica, di solito dovrebbe essere preferito qualcosa di più significativo ed esplicito.
Joshua Taylor,

7

Il problema è che si confrontano le mele con le arance . Hai chiesto come simulare la restituzione di più di un singolo valore dando un esempio di pitone veloce e sporco con tupla non tipizzata e hai effettivamente ricevuto la risposta praticamente di una riga .

La risposta accettata fornisce una soluzione aziendale corretta. Nessuna soluzione temporanea rapida che dovresti buttare via e implementare correttamente la prima volta che dovresti fare qualcosa di pratico con il valore restituito, ma una classe POJO che è compatibile con un ampio set di librerie, tra cui persistenza, serializzazione / deserializzazione, strumentazione e tutto il possibile.

Anche questo non è affatto lungo. L'unica cosa che devi scrivere sono le definizioni dei campi. Possono essere generati setter, getter, hashCode e uguali. Quindi la tua vera domanda dovrebbe essere: perché i getter e i setter non sono auto-generati ma è un problema di sintassi (problema dello zucchero sintattico, direbbe qualcuno) e non un problema culturale.

E infine, stai pensando troppo ad accelerare qualcosa che non è affatto importante. Il tempo impiegato per scrivere le classi DTO è insignificante rispetto al tempo impiegato per la manutenzione e il debug del sistema. Pertanto nessuno ottimizza per meno verbosità.


2
"Nessuno" è in realtà errato - ci sono un sacco di strumenti che ottimizzano per meno verbosità, le espressioni regolari sono un esempio estremo. Ma avresti potuto significare "nessuno nel mondo Java tradizionale", e in quanto leggere la risposta ha molto senso, grazie. La mia preoccupazione per la verbosità non è il tempo impiegato a scrivere codice ma il tempo trascorso a leggerlo; l'IDE non farà risparmiare tempo di lettura; ma sembra che l'idea del 99% delle volte si legga solo la definizione dell'interfaccia, quindi QUESTO dovrebbe essere conciso?
Mikhail Ramendik,

5

Sono tre diversi fattori che contribuiscono a ciò che stai osservando.

Tuple contro campi con nome

Forse il più banale: nelle altre lingue hai usato una tupla. Discutere se le tuple sia una buona idea non è proprio il punto - ma in Java hai usato una struttura più pesante, quindi è un confronto leggermente ingiusto: avresti potuto usare una matrice di oggetti e un tipo di casting.

Sintassi del linguaggio

Potrebbe essere più facile dichiarare la classe? Non sto parlando di rendere pubblici i campi o di usare una mappa, ma qualcosa come le classi di casi di Scala, che offrono tutti i vantaggi della configurazione descritta, ma molto più concisi:

case class Foo(duration: Int, unit: String, tooShort: Boolean)

Potremmo averlo, ma c'è un costo: la sintassi diventa più complicata. Naturalmente potrebbe valerne la pena per alcuni casi, o anche per la maggior parte dei casi, o addirittura per la maggior parte dei casi per i prossimi 5 anni, ma deve essere valutato. A proposito, questo è sulle cose carine con le lingue che puoi modificare tu stesso (ad es. Lisp) - e nota come questo diventa possibile grazie alla semplicità della sintassi. Anche se in realtà non si modifica la lingua, una semplice sintassi abilita strumenti più potenti; ad esempio molte volte mi mancano alcune opzioni di refactoring disponibili per Java ma non per Scala.

Filosofia del linguaggio

Ma il fattore più importante è che una lingua dovrebbe consentire un certo modo di pensare. A volte può sembrare opprimente (ho spesso desiderato il supporto per una determinata funzionalità), ma rimuovere le funzionalità è importante quanto averle. Potresti supportare tutto? Certo, ma allora potresti anche scrivere un compilatore che compili ogni singola lingua. In altre parole, non avrai una lingua: avrai un superset di lingue e ogni progetto adotterà sostanzialmente un sottoinsieme.

È ovviamente possibile scrivere un codice che vada contro la filosofia del linguaggio e, come hai osservato, i risultati sono spesso brutti. Avere una classe con solo un paio di campi in Java è come usare un var in Scala, degenerare i predicati del prologo in funzioni, fare un insicuroPerformIO in haskell ecc. Le classi Java non sono pensate per essere leggere - non sono lì per passare i dati . Quando qualcosa sembra difficile, è spesso fruttuoso fare un passo indietro e vedere se c'è un altro modo. Nel tuo esempio:

Perché la durata è separata dalle unità? Ci sono molte librerie di tempo che ti consentono di dichiarare una durata - qualcosa come Durata (5, secondi) (la sintassi varierà), che ti permetterà quindi di fare quello che vuoi in un modo molto più robusto. Forse vuoi convertirlo - perché controllare se il risultato [1] (o [2]?) È un 'ora' e moltiplicato per 3600? E per il terzo argomento: qual è il suo scopo? Immagino che a un certo punto dovrai stampare "meno di 1ms" o l'ora effettiva - questa è una logica che appartiene naturalmente ai dati temporali. Cioè dovresti avere una lezione come questa:

class TimeResult {
    public TimeResult(duration, unit, tooShort)
    public String getResult() {
        if tooShort:
           return "too short"
        else:
           return format(duration)
}

}

o qualunque cosa tu voglia realmente fare con i dati, incapsulando quindi la logica.

Certo, potrebbe esserci un caso in cui questo modo non funzionerà - non sto dicendo che questo è l'algoritmo magico per convertire i risultati delle tuple in codice Java idiomatico! E potrebbero esserci casi in cui è molto brutto e brutto e forse avresti dovuto usare una lingua diversa - ecco perché ce ne sono così tanti dopo tutto!

Ma la mia opinione sul perché le classi siano "strutture pesanti" in Java è che non si intende utilizzarle come contenitori di dati ma come celle logiche autonome.


Quello che voglio fare con le durate è in realtà fare una somma di esse e poi confrontarle con un'altra. Nessun output coinvolto. Il confronto deve tenere conto dell'arrotondamento, quindi ho bisogno di conoscere le unità di tempo originali al momento del confronto.
Mikhail Ramendik,

Probabilmente sarebbe possibile creare un modo per aggiungere e confrontare istanze della mia classe, ma questo potrebbe diventare enormemente pesante. Soprattutto dal momento che ho anche bisogno di dividere e moltiplicare per float (ci sono percentuali da verificare in quella UI). Quindi per ora ho fatto una classe immutabile con campi finali, che sembra un bel compromesso.
Mikhail Ramendik,

La parte più importante di questa particolare domanda è stata la parte più generale delle cose, e da tutte le risposte sembra che un'immagine si stia riunendo.
Mikhail Ramendik,

@MikhailRamendik forse una sorta di accumulatore sarebbe un'opzione - ma conosci meglio il problema. In effetti non si tratta di risolvere questo particolare problema, scusate se mi sono distratto un po '- il mio punto principale è che il linguaggio scoraggia l'uso di dati "semplici". a volte, questo ti aiuta a ripensare il tuo approccio - altre volte un int è solo un int
Thanos Tintinidis

@MikhailRamendik La cosa bella del modo boilerplatey è che una volta che hai una lezione, puoi aggiungere un comportamento ad essa. E / o rendilo immutabile come hai fatto (questa è una buona idea il più delle volte; puoi sempre restituire un nuovo oggetto come somma). Alla fine, la tua classe "superflua" può incapsulare tutto il comportamento necessario e quindi il costo per i vincitori diventa trascurabile. Inoltre, potresti averne bisogno o meno. Prendi in considerazione l'uso di lombok o autovalue .
maaartinus,

5

A mio avviso, le ragioni principali sono

  1. Le interfacce sono il modo Java di base per sottrarre classi.
  2. Java può restituire solo un singolo valore da un metodo: un oggetto o un array o un valore nativo (int / long / double / float / boolean).
  3. Le interfacce non possono contenere campi, solo metodi. Se si desidera accedere a un campo, è necessario seguire un metodo, quindi getter e setter.
  4. Se un metodo restituisce un'interfaccia, è necessario disporre di una classe di implementazione per restituire effettivamente.

Questo ti dà il "Devi scrivere una classe per tornare per qualsiasi risultato non banale" che a sua volta è piuttosto pesante. Se hai usato una classe anziché l'interfaccia, potresti avere semplicemente dei campi e usarli direttamente, ma questo ti lega a un'implementazione specifica.


Per aggiungere a questo: se ne hai bisogno solo in un piccolo contesto (per esempio, un metodo privato in una classe), va bene usare un record nudo - idealmente immutabile. Ma davvero non dovrebbe mai infiltrarsi nel contratto pubblico della classe (o peggio ancora, nella biblioteca!). Puoi decidere contro i record semplicemente per assicurarti che ciò non accada mai con future modifiche al codice, ovviamente; è spesso la soluzione più semplice.
Luaan,

E sono d'accordo con la vista "Le tuple non funzionano bene in Java". Se usi i generici, questo finirà rapidamente per diventare brutto.
Thorbjørn Ravn Andersen,

@ ThorbjørnRavnAndersen Funzionano abbastanza bene in Scala, un linguaggio tipicamente statico con generici e tuple. Quindi forse è colpa di Java?
Andres F.

@AndresF. Nota la parte "Se usi generici". Il modo in cui Java fa questo non si presta a costruzioni complesse rispetto ad esempio a Haskell. Non conosco Scala abbastanza bene per fare un confronto, ma è vero che Scala consente valori di ritorno multipli? Questo potrebbe essere di grande aiuto.
Thorbjørn Ravn Andersen,

@ ThorbjørnRavnAndersen Le funzioni Scala hanno un singolo valore di ritorno che può essere di tipo tupla (ad es. def f(...): (Int, Int)È una funzione fche restituisce un valore che risulta essere una tupla di numeri interi). Non sono sicuro che i generici in Java ne siano il problema; si noti che Haskell digita anche la cancellazione, ad esempio. Non penso che ci sia un motivo tecnico per cui Java non abbia tuple.
Andres F.

3

Sono d'accordo con JacquesB che rispondo

Java è (tradizionalmente) un linguaggio OO basato su classi a paradigma singolo che ottimizza per esplicitazione e coerenza

Ma la chiarezza e la coerenza non sono gli obiettivi finali per cui ottimizzare. Quando dici "python è ottimizzato per la leggibilità", dici immediatamente che l'obiettivo finale è "manutenibilità" e "velocità di sviluppo".

Cosa ottieni quando hai esplicito e coerenza, fatto in modo Java? La mia opinione è che si è evoluto come un linguaggio che pretende di fornire un modo prevedibile, coerente e uniforme per risolvere qualsiasi problema software.

In altre parole, la cultura Java è ottimizzata per far credere ai manager di comprendere lo sviluppo del software.

O, come ha detto un saggio molto tempo fa ,

Il modo migliore per giudicare una lingua è guardare il codice scritto dai suoi sostenitori. "Radix enim omnium malorum est cupiditas" - e Java è chiaramente un esempio di programmazione orientata al denaro (MOP). Come il principale sostenitore di Java alla SGI mi disse: "Alex, devi andare dove sono i soldi". Ma non voglio particolarmente andare dove sono i soldi - di solito non ha un buon odore lì.


3

(Questa risposta non è una spiegazione per Java in particolare, ma affronta invece la domanda generale di "Cosa potrebbero ottimizzare le pratiche [pesanti]?")

Considera questi due principi:

  1. Va bene quando il tuo programma fa la cosa giusta. Dovremmo semplificare la scrittura di programmi che fanno la cosa giusta.
  2. È male quando il tuo programma fa la cosa sbagliata. Dovremmo rendere più difficile la scrittura di programmi che fanno la cosa sbagliata.

Cercare di ottimizzare uno di questi obiettivi può talvolta ostacolare l'altro (ovvero rendere più difficile fare la cosa sbagliata può anche rendere più difficile fare la cosa giusta , o viceversa).

Quali compromessi vengono effettuati in ciascun caso particolare dipende dall'applicazione, dalle decisioni dei programmatori o del team in questione e dalla cultura (dell'organizzazione o della comunità linguistica).

Ad esempio, se un bug o un'interruzione di qualche ora nel tuo programma potrebbe comportare la perdita di vite umane (sistemi medici, aeronautica) o anche solo denaro (come milioni di dollari, ad esempio i sistemi pubblicitari di Google), faresti diversi compromessi (non solo nella tua lingua ma anche in altri aspetti della cultura ingegneristica) rispetto a quanto faresti per una sceneggiatura unica: è probabilmente incline al lato "pesante".

Altri esempi che tendono a rendere il tuo sistema più "pesante":

  • Quando per molti anni si lavora su una base di codice estesa da molti team, una delle maggiori preoccupazioni è che qualcuno possa utilizzare l'API di qualcun altro in modo errato. Una funzione chiamata con argomenti nell'ordine sbagliato, o chiamata senza garantire alcuni presupposti / vincoli previsti, potrebbe essere catastrofica.
  • Ad esempio, supponiamo che il tuo team mantenga una particolare API o libreria e desideri modificarla o riformattarla. Quanto più "vincolato" è il modo in cui i tuoi utenti utilizzano il tuo codice, tanto più facile è cambiarlo. (Si noti che sarebbe preferibile qui avere garanzie reali che nessuno potrebbe usarlo in modo insolito.)
  • Se lo sviluppo è suddiviso tra più persone o team, potrebbe sembrare una buona idea avere una persona o un team che "specifichi" l'interfaccia e che altri la implementino effettivamente. Perché funzioni, devi essere in grado di acquisire un certo grado di sicurezza quando l'implementazione viene eseguita, che l'implementazione corrisponde effettivamente alla specifica.

Questi sono solo alcuni esempi, per darti un'idea di casi in cui rendere le cose "pesanti" (e rendere più difficile la scrittura di un codice veloce) può essere veramente intenzionale. (Si potrebbe anche sostenere che se scrivere codice richiede molto sforzo, potrebbe portarti a pensare più attentamente prima di scrivere codice! Naturalmente questa linea di discussione diventa rapidamente ridicola.)

Un esempio: il sistema Python interno di Google tende a rendere le cose "pesanti" in modo tale che non puoi semplicemente importare il codice di qualcun altro, devi dichiarare la dipendenza in un file BUILD , il team il cui codice che vuoi importare deve avere la loro biblioteca dichiarata come visibile al tuo codice, ecc.


Nota : tutto quanto sopra riguarda il momento in cui le cose tendono a diventare "pesanti". Io assolutamente non pretendo che Java o Python (sia i linguaggi stessi, o le loro culture) fare compromessi ottimali per ogni caso specifico; questo è per te a cui pensare. Due collegamenti correlati su tali compromessi:


1
Che ne dici di leggere il codice? C'è un altro compromesso lì. Il codice appesantito da bizzarri incantesimi e rituali profusi è più difficile da leggere; con testo caldaia e testo inutile (e gerarchie di classi!) nel tuo IDE, diventa più difficile vedere cosa sta realmente facendo il codice. Vedo l'argomento secondo cui il codice non dovrebbe essere necessariamente facile da scrivere (non sono sicuro che io sia d'accordo, ma ha qualche merito), ma dovrebbe sicuramente essere facile da leggere . Ovviamente un codice complesso può ed è stato costruito con Java - sarebbe sciocco affermare il contrario - ma è stato grazie alla sua verbosità, o nonostante ciò?
Andres F.

@AndresF. Ho modificato la risposta per chiarire che non riguarda specificamente Java (di cui non sono un grande fan). Ma sì, compromessi durante la lettura del codice: ad un'estremità vuoi essere in grado di leggere facilmente "cosa sta realmente facendo il codice" come hai detto. Dall'altro lato, vuoi essere in grado di vedere facilmente le risposte a domande come: in che modo questo codice si collega ad altro codice? Qual è la cosa peggiore che questo codice possa fare: quale può essere il suo impatto su altri stati, quali sono i suoi effetti collaterali? Da quali fattori esterni potrebbe dipendere questo codice? (Naturalmente idealmente vogliamo risposte rapide ad entrambi i gruppi di domande.)
ShreevatsaR,

2

La cultura Java si è evoluta nel tempo con forti influenze da entrambi i background di software open source e enterprise, il che è uno strano mix se ci pensate davvero. Le soluzioni aziendali richiedono strumenti pesanti e l'open source richiede semplicità. Il risultato finale è che Java è da qualche parte nel mezzo.

Parte di ciò che sta influenzando la raccomandazione è ciò che è considerato leggibile e mantenibile in Python e Java è molto diverso.

  • In Python, la tupla è una funzione del linguaggio .
  • Sia in Java che in C #, la tupla è (o sarebbe) una funzione di libreria .

Cito solo C # perché la libreria standard ha un insieme di classi Tuple <A, B, C, .. n>, ed è un perfetto esempio di quanto siano ingombranti le tuple se il linguaggio non le supporta direttamente. In quasi tutte le istanze, il codice diventa più leggibile e gestibile se hai scelto le classi per gestire il problema. Nell'esempio specifico nella domanda StackTranslate.it collegata, gli altri valori sarebbero facilmente espressi come getter calcolati sull'oggetto return.

Una soluzione interessante che ha fatto la piattaforma C # che fornisce una felice via di mezzo è l'idea di oggetti anonimi (rilasciati in C # 3.0 ) che graffiano abbastanza bene questo prurito. Sfortunatamente, Java non ha ancora un equivalente.

Fino a quando le funzionalità del linguaggio Java non verranno modificate, la soluzione più leggibile e gestibile è avere un oggetto dedicato. Ciò è dovuto ai vincoli nel linguaggio che risalgono ai suoi inizi nel 1995. Gli autori originali avevano in programma molte più funzioni linguistiche che non lo hanno mai fatto, e la retrocompatibilità è uno dei principali vincoli che hanno caratterizzato l'evoluzione di Java nel tempo.


4
Nell'ultima versione di c # le nuove tuple sono cittadini di prima classe piuttosto che classi di biblioteca. Ci sono volute troppe versioni per arrivarci.
Igor Soloydenko,

Gli ultimi 3 paragrafi possono essere riassunti come "Lingua X ha la funzionalità che cerchi che Java non ha" e non riesco a vedere cosa aggiungono alla risposta. Perché menzionare C # in un argomento da Python a Java? No, non vedo proprio il punto. Un po 'strano da un ragazzo di 20k + rappresentante.
Olivier Grégoire,

1
Come ho detto, "Cito solo C # perché le Tuple sono una funzione di libreria" simile a come funzionerebbe in Java se avesse Tuple nella libreria principale. È molto ingombrante da usare e la risposta migliore è quasi sempre quella di avere una lezione dedicata. Tuttavia, gli oggetti anonimi graffiano un prurito simile a quello per cui sono progettate le tuple. Senza il supporto linguistico per qualcosa di meglio, devi essenzialmente lavorare con ciò che è più sostenibile.
Berin Loritsch,

@IgorSoloydenko, forse c'è speranza per Java 9? Questa è solo una di quelle caratteristiche che sono il prossimo passo logico per lambda.
Berin Loritsch,

1
@IgorSoloydenko Non sono sicuro che sia del tutto vero (cittadini di prima classe) - sembrano estensioni di sintassi per creare e scartare istanze di classi dalla libreria, piuttosto che avere un supporto di prima classe nel CLR come fanno classe, struttura, enum.
Pete Kirkham,

0

Penso che una delle cose fondamentali sull'uso di una classe in questo caso sia che ciò che va insieme dovrebbe stare insieme.

Ho avuto questa discussione al contrario, sugli argomenti del metodo: considera un metodo semplice che calcola l'IMC:

CalculateBMI(weight,height)
{
  System.out.println("BMI: " + (( weight / height ) x 703));
}

In questo caso, vorrei contestare questo stile perché peso e altezza sono correlati. Il metodo "comunica" quelli sono due valori separati quando non lo sono. Quando calcoleresti un BMI con il peso di una persona e l'altezza di un'altra? Non avrebbe senso.

CalculateBMI(Person)
{
  System.out.println("BMI: " + (( Person.weight / Person.height ) x 703));
}

Ha molto più senso perché ora comunichi chiaramente che l'altezza e il peso provengono dalla stessa fonte.

Lo stesso vale per la restituzione di più valori. Se sono chiaramente collegati, restituiscono un piccolo pacchetto ordinato e usano un oggetto, se non restituiscono più valori.


4
Ok, ma supponi di avere un compito (ipotetico) per mostrare un grafico: in che modo l'IMC dipende dal peso per una determinata altezza fissa. Quindi, non puoi farlo senza creare una persona per ciascun punto dati. E, portandolo all'estremo, non è possibile creare un'istanza della classe Person senza data di nascita e numero di passaporto validi. E adesso? Non ho votato in senso contrario, ci sono validi motivi per raggruppare le proprietà in alcune classi.
Artem

1
@artem è il prossimo passo in cui entrano in gioco le interfacce. Quindi un'interfaccia ad altezza peso personale potrebbe essere qualcosa del genere. Vieni sorpreso nell'esempio che ho dato per sottolineare che ciò che va insieme dovrebbe essere insieme.
Pieter B,

0

Ad essere sinceri, la cultura è che i programmatori Java tendevano originariamente a uscire dalle università in cui venivano insegnati principi orientati agli oggetti e principi di progettazione software sostenibile.

Come dice ErikE in più parole nella sua risposta, sembra che tu non stia scrivendo un codice sostenibile. Quello che vedo dal tuo esempio è che c'è un groviglio molto imbarazzante di preoccupazioni.

Nella cultura Java tenderai a essere consapevole di quali librerie sono disponibili e ciò ti consentirà di ottenere molto di più della tua programmazione standard. Quindi venderesti le tue idiosincrasie per i modelli e gli stili di design che erano stati provati e testati in ambienti industriali fondamentali.

Ma come dici tu, questo non è privo di inconvenienti: oggi, avendo usato Java per oltre 10 anni, tendo a usare Node / Javascript o Go per nuovi progetti, perché entrambi consentono uno sviluppo più rapido, e con architetture in stile microservizio sono spesso è sufficiente. A giudicare dal fatto che Google per primo usava pesantemente Java, ma è stato il creatore di Go, immagino che potrebbero fare lo stesso. Ma anche se uso Go e Javascript ora, utilizzo ancora molte delle capacità di progettazione che ho acquisito da anni nell'uso e nella comprensione di Java.


1
In particolare, lo strumento di reclutamento foobar utilizzato da google consente di utilizzare due lingue per le soluzioni: java e python. Trovo interessante che Go non sia un'opzione. Mi è capitato in questa presentazione su Go e ha alcuni punti interessanti su Java e Python (così come su altre lingue.) Ad esempio c'è un punto elenco che si è distinto: "Forse, di conseguenza, i programmatori non esperti hanno confuso" facilità di usa "con interpretazione e digitazione dinamica". Merita una lettura.
JimmyJames,

@JimmyJames è appena arrivato alla diapositiva dicendo "nelle mani di esperti, sono fantastici" - buon riassunto del problema nella domanda
Tom
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.