Culture Java Grokking - Pourquoi les choses sont-elles si lourdes? Pour quoi optimise-t-il? [fermé]

288

J'ai beaucoup codé en Python. Maintenant, pour des raisons professionnelles, je code en Java. Les projets que je fais sont plutôt petits, et Python fonctionnerait probablement mieux, mais il existe des raisons valables pour des raisons autres que l’ingénierie d’utiliser Java (je ne peux pas entrer dans les détails).

La syntaxe Java n'est pas un problème. c'est juste une autre langue. Mais en dehors de la syntaxe, Java possède une culture, un ensemble de méthodes de développement et de pratiques considérées comme "correctes". Et pour l'instant, je ne parviens pas à "maîtriser" cette culture. J'apprécierais donc vraiment des explications ou des indications dans la bonne direction.

Un exemple minimal minimal est disponible dans une question de débordement de pile que j'ai commencée: https://stackoverflow.com/questions/43619566/returning-a-result-with-several-values-the-java-way/43620339

J'ai une tâche - analyser (à partir d'une seule chaîne) et gérer un ensemble de trois valeurs. En Python, il s'agit d'une ligne (tuple), en Pascal ou C d'un enregistrement / struct à 5 lignes.

Selon les réponses, l'équivalent d'une structure est disponible dans la syntaxe Java et un triple est disponible dans une bibliothèque Apache largement utilisée. Pourtant, la solution "correcte" consiste à créer une classe distincte pour la valeur, avec Getters et setters. Quelqu'un a été très gentil pour donner un exemple complet. Il s'agissait de 47 lignes de code (certaines de ces lignes étaient vierges).

Je comprends qu’une énorme communauté de développement n’est probablement pas "fausse". C'est donc un problème avec ma compréhension.

Les pratiques Python optimisent la lisibilité (ce qui, dans cette philosophie, conduit à la maintenabilité) et ensuite la vitesse de développement. Les pratiques C optimisent l'utilisation des ressources. Pourquoi les pratiques Java optimisent-elles? Ma meilleure hypothèse est l'évolutivité (tout devrait être dans un état prêt pour un projet de plusieurs millions de lettres), mais c'est une hypothèse très faible.

Mikhail Ramendik
la source
1
ne répondra pas aux aspects techniques de la question mais restera dans son contexte: softwareengineering.stackexchange.com/a/63145/13899
Newtopian
74
Vous pouvez apprécier l'essai de Steve Yegge intitulé Exécution au Royaume des noms
Colonel Panic.
3
Voici ce que je pense être la bonne réponse. Vous ne vous moquez pas de Java parce que vous envisagez de programmer comme un administrateur système, pas un programmeur. Pour vous, le logiciel est quelque chose que vous utilisez pour accomplir une tâche particulière de la manière la plus rapide possible. J'écris du code Java depuis 20 ans et certains des projets sur lesquels j'ai travaillé ont nécessité une équipe de 20, 2 ans. Java ne remplace pas Python ni l'inverse. Ils font tous les deux leur travail, mais leur travail est totalement différent.
Richard
1
La raison pour laquelle Java est la norme de facto est qu’elle est tout à fait juste. Il a été créé correctement, pour la première fois, par des gens sérieux qui portaient la barbe avant que la barbe ne soit à la mode. Si vous avez l'habitude d'assembler des scripts de shell pour manipuler des fichiers, Java semble excessivement gonflé. Mais si vous souhaitez créer un cluster de serveurs redondant capable de desservir 50 millions de personnes par jour, reposant sur une mise en cache redis en cluster, 3 systèmes de facturation et un cluster de base de données Oracle de 20 millions de livres. Vous n'utiliserez pas python, même si la base de données en Python contient 25 lignes moins le code.
Richard

Réponses:

237

Le langage java

Je crois que toutes ces réponses manquent à l'essentiel en essayant d'attribuer une intention à la façon dont Java fonctionne. La verbosité de Java ne découle pas de son orientation objet, car Python et de nombreux autres langages ont également une syntaxe de calculateur. La verbosité de Java ne vient pas non plus de son support des modificateurs d'accès. C'est simplement la façon dont Java a été conçu et a évolué.

Java a été créé à l’origine sous forme de C légèrement amélioré avec OO. En tant que tel, Java a une syntaxe des années 70. De plus, Java est très prudent sur l'ajout de fonctionnalités afin de conserver la compatibilité avec les versions antérieures et de lui permettre de résister à l'épreuve du temps. Si Java avait ajouté des fonctionnalités à la mode comme les littéraux XML en 2005, alors que XML était à la mode, le langage aurait été rempli de fonctionnalités fantômes dont personne ne se soucie et qui limitent son évolution 10 ans plus tard. Par conséquent, Java manque simplement de beaucoup de syntaxe moderne pour exprimer des concepts de manière concise.

Cependant, rien n’empêche Java d’adopter cette syntaxe. Par exemple, Java 8 a ajouté des références lambdas et méthodes à ses méthodes, ce qui réduit considérablement la verbosité dans de nombreuses situations. De même, Java pourrait prendre en charge les déclarations de type de données compactes telles que les classes de cas de Scala. Mais Java ne l’a tout simplement pas fait. Notez que les types de valeurs personnalisées sont à l'horizon et que cette fonctionnalité peut introduire une nouvelle syntaxe pour les déclarer. Je suppose que nous verrons.


La culture java

L'histoire du développement Java d'entreprise nous a largement conduits à la culture actuelle. À la fin des années 90 et au début des années 2000, Java est devenu un langage extrêmement populaire pour les applications métier côté serveur. À l'époque, ces applications étaient en grande partie écrites ad-hoc et intégraient de nombreuses préoccupations complexes, telles que les API HTTP, les bases de données et le traitement des flux XML.

Au cours des années 00, il est devenu évident que beaucoup de ces applications avaient beaucoup en commun et que les frameworks permettant de gérer ces problèmes, tels que l'ORM Hibernate, l'analyseur XML Xerces, les JSP et l'API servlet, ainsi que les EJB, devenaient populaires. Toutefois, bien que ces infrastructures réduisent les efforts nécessaires pour travailler dans le domaine qu’elles sont censées automatiser, elles nécessitent une configuration et une coordination. À l'époque, pour une raison quelconque, il était courant d'écrire des frameworks pour les cas d'utilisation les plus complexes. Par conséquent, ces bibliothèques étaient difficiles à configurer et à intégrer. Et au fil du temps, ils sont devenus de plus en plus complexes au fur et à mesure qu'ils ont accumulé des fonctionnalités. Le développement d'entreprise Java s'est progressivement tourné vers la connexion de bibliothèques tierces et moins vers l'écriture d'algorithmes.

Finalement, la configuration et la gestion fastidieuses des outils d'entreprise sont devenues suffisamment pénibles pour que des frameworks, notamment le framework Spring, se présentent pour gérer le management. Vous pouvez mettre toute votre configuration au même endroit, selon la théorie, et l’outil de configuration configurerait ensuite les pièces et les relierait. Malheureusement, ces "frameworks" ont ajouté plus d'abstraction et de complexité à la boule de cire.

Au cours des dernières années, la popularité des bibliothèques légères a augmenté. Néanmoins, toute une génération de programmeurs Java est arrivée à maturité avec la croissance des infrastructures d'entreprise lourdes. Leurs modèles, ceux qui développent les cadres, ont écrit des usines et des chargeurs de beans de configuration proxy. Ils devaient configurer et intégrer ces monstruosités au jour le jour. En conséquence, la culture de la communauté dans son ensemble a suivi l’exemple de ces cadres et a eu tendance à mal se sur-ingénier.

Le secret de Salomonoff
la source
15
Ce qui est amusant, c’est que d’autres langues semblent prendre des "java-isms". Les fonctionnalités de PHP OO sont fortement inspirées de Java, et de nos jours, JavaScript est souvent critiqué pour son énorme quantité de frameworks et sa difficulté à rassembler toutes les dépendances et à démarrer une nouvelle application.
Marc
14
@marcus peut-être parce que certaines personnes ont finalement appris le truc du "ne pas réinventer la roue"? Les dépendances sont le coût de ne pas réinventer la roue après tout
Walfrat le
9
"Terse" se traduit souvent par des arcanes, "intelligent" , des one-liners code-golf-ish.
Tulains Córdova
7
Réponse réfléchie et bien écrite. Contexte historique. Relativement sans opinion. Bien fait.
Cheezmeister
10
@ TulainsCórdova Je suis d'accord pour dire que nous devrions "éviter ... des manipulations astucieuses comme la peste" (Donald Knuth), mais cela ne signifie pas écrire une forboucle quand un mapserait plus approprié (et lisible). Il y a un juste milieu.
Brian McCutchon
73

Je pense avoir une réponse à l’un des points que vous avez soulevés et qui n’a pas été soulevée par d’autres.

Java optimise la détection des erreurs de programmation lors de la compilation en ne faisant aucune hypothèse

En général, Java a tendance à inférer des faits sur le code source après que le programmeur a déjà explicitement exprimé son intention. Le compilateur Java ne fait jamais d'hypothèses sur le code et n'utilise l'inférence que pour réduire le code redondant.

La raison derrière cette philosophie est que le programmeur est uniquement humain. Ce que nous écrivons n’est pas toujours ce que nous voulons réellement que le programme fasse. Le langage Java tente d’atténuer certains de ces problèmes en obligeant les développeurs à toujours déclarer explicitement leurs types. C'est juste une façon de vérifier que le code qui a été écrit fait ce qui était prévu.

D'autres langages poussent encore plus loin cette logique en vérifiant les conditions préalables, les conditions préalables et les invariants (bien que je ne sois pas sûr qu'ils le fassent au moment de la compilation). Ce sont des moyens encore plus extrêmes pour le programmeur de demander au compilateur de vérifier son propre travail.

Dans votre cas, cela signifie que pour que le compilateur vous garantisse le retour des types que vous pensez retourner, vous devez lui fournir ces informations.

En Java, il y a deux façons de le faire:

  1. Utilisez Triplet<A, B, C>le type de retour (qui devrait vraiment être java.util, et je ne peux pas vraiment expliquer pourquoi il n'est pas. D' autant plus que JDK8 introduire Function, BiFunction, Consumer, BiConsumer, etc ... Il semble juste que Pairet Tripletau moins aurait du sens. Mais je digresse )

  2. Créez votre propre type de valeur à cette fin, où chaque champ est nommé et correctement saisi.

Dans ces deux cas, le compilateur peut garantir que votre fonction renvoie les types déclarés, que l'appelant réalise le type de chaque champ renvoyé et les utilise en conséquence.

Certaines langues permettent la vérification de type statique ET l’inférence de type en même temps, mais cela laisse la porte ouverte à une classe subtile de problèmes d’incompatibilité de types. Lorsque le développeur avait l'intention de renvoyer une valeur d'un certain type, mais en renvoie une autre et que le compilateur accepte STILL le code car il arrive que, par coïncidence, la fonction et l'appelant n'utilisent que des méthodes pouvant être appliquées à la fois et les types réels.

Envisagez quelque chose comme ceci dans Typescript (ou type de flux) où l'inférence de type est utilisée à la place du typage explicite.

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

C'est un cas trivial et ridicule, bien sûr, mais il peut être beaucoup plus subtil et rendre difficile le débogage des problèmes, car le code est correct. Ce type de problèmes est totalement évité en Java, et c’est ce sur quoi tout le langage est conçu.


Notez que, conformément aux principes orientés objet et à la modélisation pilotée par domaine appropriés, le cas d'analyse de la durée dans la question liée peut également être renvoyé sous forme d' java.time.Durationobjet, ce qui serait beaucoup plus explicite que les deux cas précédents.

LordOfThePigs
la source
38
Déclarer que Java optimise la correction du code peut être un argument valable, mais il me semble (beaucoup de langages, récemment un peu de java) que le langage échoue ou est extrêmement inefficace. Certes, Java est vieux et il n’existait pas beaucoup de choses à l’époque, mais il existe des alternatives modernes qui offrent une meilleure vérification de l’exactitude, comme Haskell (et osez-vous? Rust ou Go). Toute la lourdeur de Java n’est pas nécessaire pour atteindre cet objectif. - Et d'ailleurs, cela n'explique pas du tout la culture, mais Solomonoff a révélé que la culture était BS.
Tomsmeding
8
Cet exemple ne démontre pas que l'inférence de type est le problème mais que la décision de JavaScript d'autoriser les conversions implicites dans un langage dynamique est stupide. Tout langage dynamique sain ou statué avec une inférence de type ne le permettrait pas. A titre d'exemple pour les deux types: python lancerait un runtime sauf Haskell ne compilerait pas.
Voo le
39
@tomsmeding Il est intéressant de noter que cet argument est particulièrement ridicule, car Haskell existe depuis 5 ans de plus que Java. Java a peut-être un système de types, mais il n’avait même pas de vérification de la correction de l'état de la technique au moment de sa publication.
Alexis King
21
«Java optimise la détection des erreurs au moment de la compilation» - Non. Il le fournit mais, comme d'autres l'ont noté, il n'optimise certainement pas dans tous les sens du terme. De plus, cet argument est sans aucun rapport à la question de l' OP parce que d' autres langues qui ne permettent d' optimiser réellement pour la détection d'erreur de compilation ont une syntaxe beaucoup plus légère pour des scénarios similaires. La verbosité exagérée de Java n’a rien à voir avec sa vérification au moment de la compilation.
Konrad Rudolph
19
@ KonradRudolph Oui, cette réponse est complètement absurde, même si je suppose que c'est émotionnellement attrayant. Je ne sais pas pourquoi il a tant de voix. Haskell (ou peut-être même plus significativement, Idris) en optimise beaucoup plus pour la correction que Java, et il a des n-uplets avec une syntaxe légère. De plus, la définition d'un type de données est une ligne, et vous obtenez tout ce que la version Java obtiendrait. Cette réponse est une excuse pour la conception médiocre des langages et les commentaires inutiles, mais cela semble bien si vous aimez Java et ne maîtrisez pas les autres langages.
Alexis King
50

Java et Python sont les deux langages que j'utilise le plus mais je viens de l'autre sens. C'est-à-dire que j'étais profondément ancré dans le monde Java avant de commencer à utiliser Python afin de pouvoir vous aider. Je pense que la réponse à la question plus vaste de "pourquoi les choses sont si lourdes" se résume à 2 choses:

  • Les coûts de développement entre les deux sont comme l’air d’un ballon long ballon. Vous pouvez presser une partie du ballon et une autre partie gonfle. Python a tendance à serrer la première partie. Java serre la dernière partie.
  • Il manque encore à Java certaines fonctionnalités permettant de supprimer une partie de ce poids. Java 8 a fait une énorme entorse à cela, mais la culture n’a pas complètement digéré les changements. Java pourrait utiliser un peu plus de choses telles que yield.

Java "optimise" les logiciels de grande valeur qui seront maintenus pendant de nombreuses années par de grandes équipes. J'ai eu l'expérience d'écrire des choses en Python et de les regarder un an plus tard et d'être dérouté par mon propre code. En Java, je peux consulter de minuscules extraits du code d'autres personnes et savoir instantanément ce qu'il fait. En Python, vous ne pouvez pas vraiment faire ça. Ce n'est pas que l'on se rend compte mieux que vous semblez comprendre, ils ont juste des coûts différents.

Dans le cas spécifique que vous mentionnez, il n'y a pas de tuples. La solution simple consiste à créer une classe avec des valeurs publiques. Quand Java est apparu pour la première fois, les gens le faisaient assez régulièrement. Le premier problème avec cela est que c'est un casse-tête d'entretien. Si vous devez ajouter de la logique ou de la sécurité des threads ou si vous souhaitez utiliser le polymorphisme, vous devrez au minimum toucher à toutes les classes qui interagissent avec cet objet "tuple-esque". En Python, il existe des solutions pour cela, __getattr__etc., donc ce n'est pas si grave.

Il y a cependant quelques mauvaises habitudes (IMO) à ce sujet. Dans ce cas, si vous voulez un tuple, je me demande pourquoi vous en feriez un objet mutable. Vous n'avez besoin que de getters (sur une note de côté, je déteste la convention get / set mais c'est ce qu'elle est.) Je pense vraiment qu'une classe nue (modifiable ou non) peut être utile dans un contexte privé ou de package privé en Java. . C'est-à-dire qu'en limitant les références du projet à la classe, vous pouvez refactoriser ultérieurement si nécessaire sans modifier l'interface publique de la classe. Voici un exemple de la façon dont vous pouvez créer un objet immuable simple:

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

C'est une sorte de motif que j'utilise. Si vous n'utilisez pas d'IDE, vous devriez commencer. Cela génèrera les getters (et les setters si vous en avez besoin) pour que ce ne soit pas si douloureux.

Je me sentirais négligent si je ne signalais pas qu'il existe déjà un type qui semblerait répondre à la plupart de vos besoins ici . En mettant cela de côté, l’approche que vous utilisez n’est pas bien adaptée à Java car elle exploite ses faiblesses et non ses forces. Voici une amélioration simple:

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;
  }
}
JimmyJames
la source
Les commentaires ne sont pas pour une discussion prolongée; cette conversation a été déplacée pour discuter .
maple_shaft
2
Permettez-moi de vous suggérer également d'examiner Scala en ce qui concerne les fonctionnalités Java "plus modernes" ...
MikeW
1
@ MikeW En fait, j'ai commencé avec Scala dans les premières versions et j'ai été actif sur les forums Scala pendant un certain temps. Je pense que c’est une grande réussite en matière de conception linguistique, mais j’en suis venu à la conclusion que ce n’était pas vraiment ce que je recherchais et que j’étais un pion carré dans cette communauté. Je devrais y revenir car il a probablement beaucoup changé depuis cette époque.
JimmyJames
Cette réponse n'aborde pas l'aspect valeur approximative soulevée par l'OP.
ErikE
@ErikE Les mots "valeur approximative" n'apparaissent pas dans le texte de la question. Si vous pouvez m'indiquer la partie spécifique de la question que je n'ai pas abordée, je peux essayer de le faire.
JimmyJames
41

Avant de vous mettre en colère contre Java, veuillez lire ma réponse dans votre autre message .

Une de vos plaintes est la nécessité de créer une classe simplement pour renvoyer un ensemble de valeurs en guise de réponse. Ceci est une préoccupation valable qui, je pense, montre que vos intuitions de programmation sont sur la bonne voie! Cependant, je pense que d’autres réponses manquent à la lettre en restant fidèles à l’anti-tendance obsessionnelle primitive à laquelle vous vous êtes engagé. De plus, Java n’a pas la même facilité de travailler avec plusieurs primitives que Python, dans lequel vous pouvez renvoyer plusieurs valeurs de manière native et les affecter facilement à plusieurs variables.

Mais une fois que vous commencez à penser à ce qu'un ApproximateDurationtype fait pour vous, vous réalisez qu'il n'est pas si étroitement lié à "juste une classe apparemment inutile pour renvoyer trois valeurs". Le concept représenté par cette classe est en fait l'un des concepts métier de votre domaine clé : il est nécessaire de pouvoir représenter les temps de manière approximative et de les comparer. Cela doit faire partie du langage omniprésent du noyau de votre application, avec une bonne prise en charge des objets et des domaines, de sorte qu'il puisse être testé, modulaire, réutilisable et utile.

Votre code qui additionne-t-il les durées approximatives (ou les durées avec une marge d'erreur, quelle que soit leur représentation) est-il entièrement procédural ou existe-t-il une quelconque objectivité? Je proposerais qu'une bonne conception permettant de résumer des durées approximatives dicterait de le faire en dehors de tout code consommateur, au sein d'une classe qui peut elle-même être testée. Je pense que l'utilisation de ce type d'objet de domaine aura un effet d'entraînement positif dans votre code, ce qui vous aidera à vous éloigner des étapes procédurales ligne par ligne pour accomplir une tâche de haut niveau unique (avec de nombreuses responsabilités) vers des classes à responsabilité unique. qui sont libres des conflits de préoccupations différentes.

Par exemple, supposons que vous en appreniez plus sur la précision ou l’échelle qui est réellement nécessaire pour que votre sommation et votre comparaison de durée fonctionnent correctement, et que vous avez besoin d’un indicateur intermédiaire pour indiquer "une erreur d’environ 32 millisecondes" (près du carré). racine de 1000, donc à mi-chemin logarithmique entre 1 et 1000). Si vous vous êtes lié à un code qui utilise des primitives pour représenter cela, vous devez trouver chaque endroit du code où vous en avez is_in_seconds,is_under_1mset le changer enis_in_seconds,is_about_32_ms,is_under_1ms. Tout devrait changer partout! Créer une classe dont la responsabilité est d'enregistrer la marge d'erreur afin qu'elle puisse être utilisée ailleurs libère vos consommateurs de toutes les informations relatives aux marges d'erreur qui importent ou de la manière dont ils se combinent et leur permet de spécifier simplement la marge d'erreur pertinente. en ce moment. (En d’autres termes, aucun code consommateur dont la marge d’erreur est correcte n’est forcé de changer lorsque vous ajoutez une nouvelle marge de niveau d’erreur dans la classe, car toutes les anciennes marges d’erreur sont toujours valides).

Déclaration de clôture

La plainte concernant la lourdeur de Java semble alors disparaître au fur et à mesure que vous vous rapprochez des principes de SOLID et GRASP, ainsi que d’une ingénierie logicielle plus avancée.

Addenda

J'ajouterai de manière totalement gratuite et injuste que les propriétés automatiques de C # et sa capacité à affecter des propriétés get-only aux constructeurs aident à nettoyer encore plus le code quelque peu compliqué que "la méthode Java" nécessitera (avec des champs de sauvegarde privés explicites et des fonctions 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; }
}

Voici une implémentation Java de ce qui précède:

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

Maintenant, c'est sacrément propre. Notez l'utilisation très importante et intentionnelle de l'immutabilité - cela semble crucial pour ce type particulier de classe de valeur.

D'ailleurs, cette classe est également un candidat décent pour être un structtype de valeur. Certains tests montreront si le passage à une structure présente un avantage en termes de performances d'exécution (cela pourrait être le cas).

ErikE
la source
9
Je pense que c'est ma réponse préférée. Je trouve que, lorsque je promeut une classe de détenteurs de ce genre à un autre, elle devient le foyer de toutes les responsabilités qui s’y rattachent et qu’elle s’amuse! tout devient plus propre! Et j'apprends généralement quelque chose d'intéressant à propos de l'espace problématique en même temps. Évidemment, il y a une habileté à éviter l'ingénierie excessive ... mais Gosling n'était pas stupide lorsqu'il a omis la syntaxe des tuples, comme avec les GOTO. Votre déclaration finale est excellente. Merci!
SusanW
2
Si vous aimez cette réponse, lisez l'article sur la conception par domaine. Il a beaucoup à dire sur le sujet de la représentation des concepts de domaine dans le code.
Neontapir
@neontapir Oui, merci! Je savais qu'il y avait un nom pour ça! :-) Bien que je dois dire que je le préfère quand un concept de domaine apparaît organiquement d'un refactoring inspiré (comme celui-ci!) ... c'est un peu comme quand vous arrivez à résoudre votre problème de gravité du 19ème siècle en découvrant Neptune. .
SusanW
@SusanW Je suis d'accord. Refactoriser pour supprimer l'obsession primitive peut être un excellent moyen de découvrir les concepts de domaine!
Neontapir
J'ai ajouté une version Java de l'exemple, comme indiqué. Je ne suis pas au courant de tout le sucre syntaxique magique en C #, donc s'il manque quelque chose, faites-le-moi savoir.
JimmyJames
24

Python et Java sont optimisés pour la maintenabilité conformément à la philosophie de leurs concepteurs, mais ils ont des idées très différentes sur la façon d’y parvenir.

Python est un langage multi-paradigmes qui optimise la clarté et la simplicité du code (facile à lire et à écrire).

Java est (traditionnellement) un langage OO basé sur une classe à paradigme unique, optimisé pour être explicite et cohérent , même au prix de codes plus verbeux.

Un tuple Python est une structure de données avec un nombre fixe de champs. La même fonctionnalité peut être obtenue par une classe régulière avec des champs explicitement déclarés. En Python, il est naturel de fournir des n-uplets comme alternative aux classes car cela vous permet de simplifier grandement le code, notamment en raison de la prise en charge de la syntaxe intégrée pour les n-uplets.

Mais cela ne correspond pas vraiment à la culture Java pour fournir de tels raccourcis, car vous pouvez déjà utiliser des classes déclarées explicites. Il n'est pas nécessaire d'introduire un type de structure de données différent pour enregistrer certaines lignes de code et éviter certaines déclarations.

Java préfère un concept unique (classes) systématiquement appliqué avec un minimum de sucre syntaxique dans des cas particuliers, tandis que Python fournit plusieurs outils et de nombreux sucres syntaxiques pour vous permettre de choisir le plus pratique pour un usage particulier.

JacquesB
la source
+1, mais comment fonctionne ce maillage avec des langages statiquement typés avec des classes, telles que Scala, qui a néanmoins trouvé la nécessité d’introduire des tuples? Je pense que les tuples ne sont que la solution la plus élégante dans certains cas, même pour les langues avec des classes.
Andres F.
@AndresF .: Qu'entendez-vous par maillage? Scala est un langage distinct de Java et possède des principes de conception et des idiomes différents.
JacquesB
Je le voulais en réponse à votre 5ème paragraphe, qui commence par "mais cela ne correspond pas vraiment à la culture Java [...]". En effet, je conviens que la verbosité fait partie de la culture de Java, mais le manque de n-uplets ne le peut pas car ils "utilisent déjà des classes explicitement déclarées" - après tout, Scala a aussi des classes explicites (et introduit les classes de cas les plus concises ), permet également des tuples! En fin de compte, j'estime qu'il est probable que Java n'a pas introduit de n-uplets non seulement parce que les classes peuvent obtenir le même résultat (avec plus d'encombrement), mais aussi parce que sa syntaxe doit être familière aux programmeurs C et que C n'avait pas de n-uplets.
Andres F.
@AndresF.: Scala n'a pas d'aversion pour de multiples façons de faire les choses, elle combine des caractéristiques du paradigme fonctionnel et du classique OO. C'est plus proche de la philosophie Python de cette façon.
JacquesB
@AndresF .: Oui, Java a commencé avec une syntaxe proche de C / C ++, mais beaucoup simplifiée. C # a commencé comme une copie presque exacte de Java, mais a ajouté de nombreuses fonctionnalités au fil des ans, y compris des n-uplets. C'est donc vraiment une différence de philosophie de conception linguistique. (Les versions ultérieures de Java semblent toutefois être moins dogmatiques. Les fonctions d'ordre supérieur ont été initialement rejetées car vous pouviez faire la même chose avec les classes, mais elles ont maintenant été introduites. Nous assistons donc peut-être à un changement de philosophie.)
JacquesB
16

Ne cherchez pas de pratiques; c'est généralement une mauvaise idée, comme dit dans Meilleures pratiques BAD, des modèles BON? . Je sais que vous ne demandez pas les meilleures pratiques, mais je pense toujours que vous y trouverez des éléments pertinents.

Rechercher une solution à votre problème est préférable à une pratique, et votre problème n'est pas un tuple pour renvoyer rapidement trois valeurs en Java:

  • Il y a des tableaux
  • Vous pouvez retourner un tableau sous forme de liste dans une ligne: Arrays.asList (...)
  • Si vous voulez garder le côté objet avec le moins de passe-passe possible (et pas de 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,....){
        ...
    }
 }

Ici, vous avez un objet immuable contenant uniquement vos données, et public, vous n'avez donc pas besoin de getters. Notez cependant que si vous utilisez des outils de sérialisation ou une couche de persistance comme un ORM, ils utilisent généralement getter / setter (et peuvent accepter un paramètre pour utiliser des champs au lieu de getter / setter). Et c’est pourquoi ces pratiques sont très utilisées. Donc, si vous voulez en savoir plus sur les pratiques, il vaut mieux comprendre pourquoi elles sont ici pour mieux les utiliser.

Enfin: j'utilise des accesseurs parce que j'utilise beaucoup d'outils de sérialisation, mais je ne les écris pas non plus; J'utilise lombok: J'utilise les raccourcis fournis par mon IDE.

Walfrat
la source
9
Il est toujours utile de comprendre les idiomes communs que vous rencontrez dans différentes langues, car ils deviennent un standard de facto plus accessible. Ces idiomes ont tendance à tomber dans la catégorie des "meilleures pratiques" pour une raison quelconque.
Berin Loritsch
3
Hé, une solution sensée au problème. Il semble tout à fait raisonnable de simplement écrire une classe (/ struct) avec quelques membres du public au lieu d'une solution complète de POO surdéveloppée avec [gs] outils.
tomsmeding
3
Cela conduit à gonfler parce que, contrairement à Python, la recherche ou une solution plus courte et plus élégante n'est pas encouragée. Mais l’avantage, c’est que le problème sera plus ou moins le même pour tous les développeurs Java. Par conséquent, la maintenabilité est plus grande parce que les développeurs sont plus interchangeables, et si deux projets sont joints ou si deux équipes divergent involontairement, la différence de style à combattre est moindre.
Mikhail Ramendik
2
@ MikhailRamendik C'est un compromis entre YAGNI et OOP. OOP orthodoxe dit que chaque objet doit être remplaçable; la seule chose qui compte est l'interface publique de l'objet. Cela vous permet d'écrire du code moins centré sur des objets concrets et davantage sur des interfaces. et comme les champs ne peuvent pas faire partie d’une interface, vous ne les exposez jamais. Cela peut rendre le code beaucoup plus facile à gérer à long terme. De plus, il vous permet d'éviter les états non valides. Dans votre exemple d'origine, il est possible d'avoir un objet qui est à la fois "<ms" et "s", qui s'excluent mutuellement. C'est mauvais.
Luaan
2
@PeterMortensen C'est une bibliothèque Java qui fait beaucoup de choses relativement indépendantes. C'est très bien quand même. Voici les caractéristiques
Michael
11

À propos des idiomes Java en général:

Il existe diverses raisons pour lesquelles Java a des classes pour tout. Pour autant que je sache, la raison principale en est:

Java devrait être facile à apprendre pour les débutants. Plus les choses sont explicites, plus il est difficile de rater des détails importants. Il y a moins de magie difficile à comprendre pour les débutants.


En ce qui concerne votre exemple spécifique: la ligne d’argumentation pour une classe séparée est la suivante: si ces trois éléments sont suffisamment liés les uns aux autres pour qu’ils soient renvoyés sous la forme d’une valeur unique, il vaut la peine de nommer ce "élément". Et introduire un nom pour un groupe de choses structurées de manière commune signifie définir une classe.

Vous pouvez réduire le passe-partout avec des outils comme Lombok:

@Value
class MyTuple {
    long value_in_milliseconds;
    boolean is_in_seconds;
    boolean is_under_1ms;
}
Marstato
la source
5
Le est également le projet AutoValue de Google: github.com/google/auto/blob/master/value/userguide/index.md
Ivan
C'est aussi pourquoi les programmes Java peuvent être relativement facilement maintenus!
Thorbjørn Ravn Andersen
7

On pourrait dire beaucoup de choses sur la culture Java, mais je pense que dans le cas auquel vous êtes confronté actuellement, il y a quelques aspects importants:

  1. Le code de bibliothèque est écrit une fois mais est utilisé beaucoup plus souvent. Bien qu'il soit agréable de minimiser le temps système nécessaire à l'écriture de la bibliothèque, il est probablement plus intéressant à long terme d'écrire d'une manière qui minimise le temps système d'utilisation de la bibliothèque.
  2. Cela signifie que les types auto-documentés sont excellents: les noms de méthodes permettent de clarifier ce qui se passe et ce que vous obtenez d'un objet.
  3. Le typage statique est un outil très utile pour éliminer certaines classes d’erreurs. Cela ne résout certainement pas tout (les gens aiment blaguer à propos de Haskell, une fois que le système de typage accepte votre code, c'est probablement correct), mais il est très facile de rendre certains types de mauvaises choses impossibles.
  4. L'écriture de code de bibliothèque consiste à spécifier des contrats. La définition d'interfaces pour vos types d'argument et de résultat permet de définir plus clairement les limites de vos contrats. Si quelque chose accepte ou produit un tuple, il est impossible de dire si c'est le genre de tuple que vous devriez réellement recevoir ou produire, et il y a très peu de contraintes sur un type aussi générique (a-t-il même le bon nombre d'éléments? Sont ils du type que vous attendiez?).

"Struct" classes avec des champs

Comme d'autres réponses l'ont mentionné, vous pouvez simplement utiliser une classe avec des champs publics. Si vous faites ces derniers, alors vous obtenez une classe immuable, et vous les initialiserez avec le constructeur:

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

Bien entendu, cela signifie que vous êtes lié à une classe particulière et que tout ce qui doit générer ou utiliser un résultat d'analyse doit utiliser cette classe. Pour certaines applications, ça va. Pour d'autres, cela peut causer de la douleur. Une grande partie du code Java consiste à définir des contrats, ce qui vous mènera généralement vers des interfaces.

Un autre piège est qu'avec une approche basée sur les classes, vous exposez des champs et tous ces champs doivent avoir des valeurs. Par exemple, isSeconds et millis doivent toujours avoir une valeur, même si isLessThanOneMilli est true. Quelle doit être l'interprétation de la valeur du champ millis lorsque isLessThanOneMilli est true?

"Structures" comme interfaces

Avec les méthodes statiques autorisées dans les interfaces, il est en fait relativement facile de créer des types immuables sans trop de surcharge syntaxique. Par exemple, je pourrais implémenter le type de structure de résultats dont vous parlez comme ceci:

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

Je suis tout à fait d’accord, c’est toujours beaucoup, mais il ya aussi quelques avantages, et je pense que ceux-ci commencent à répondre à certaines de vos principales questions.

Avec une structure comme celle-ci, le contrat de votre analyseur est très clairement défini. En Python, un tuple n'est pas vraiment distinct d'un autre tuple. En Java, le typage statique est disponible, nous avons donc déjà exclu certaines classes d’erreurs. Par exemple, si vous renvoyez un tuple en Python et que vous souhaitez renvoyer le tuple (millis, isSeconds, isLessThanOneMilli), vous pouvez accidentellement effectuer les opérations suivantes:

return (true, 500, false)

quand vous vouliez dire:

return (500, true, false)

Avec ce type d’interface Java, vous ne pouvez pas compiler:

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

du tout. Tu dois faire:

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

C'est un avantage des langages statiquement typés en général.

Cette approche commence également à vous donner la possibilité de restreindre les valeurs que vous pouvez obtenir. Par exemple, lorsque vous appelez getMillis (), vous pouvez vérifier si isLessThanOneMilli () est true et, le cas échéant, déclenchez IllegalStateException (par exemple), car il n'y a pas de valeur significative de millis dans ce cas.

Faire en sorte qu'il soit difficile de faire le mauvais choix

Dans l'exemple d'interface ci-dessus, vous avez toujours le problème de pouvoir échanger accidentellement les arguments isSeconds et isLessThanOneMilli, car ils ont le même type.

En pratique, vous voudrez peut-être utiliser TimeUnit et la durée pour obtenir le résultat suivant:

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

Ça commence à être beaucoup plus de code, mais vous n'avez besoin de l'écrire qu'une seule fois et (en supposant que vous ayez bien documenté les choses), les personnes qui finissent par utiliser votre code n'ont pas à deviner ce que le résultat signifie, et ne peut pas faire accidentellement des choses comme result[0]quand ils veulent dire result[1]. Vous devez toujours créer des instances assez succinctement, et en extraire des données n'est pas si difficile non plus:

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

Notez que vous pouvez également faire quelque chose comme ceci avec l'approche basée sur les classes. Il suffit de spécifier les constructeurs pour les différents cas. Vous avez toujours la question de savoir à quoi initialiser les autres champs et vous ne pouvez pas empêcher leur accès.

Une autre réponse a indiqué que la nature de type entreprise de Java signifiait que la plupart du temps, vous composiez d'autres bibliothèques déjà existantes ou rédigiez des bibliothèques que d'autres personnes pourraient utiliser. Votre API publique ne devrait pas nécessiter beaucoup de temps pour consulter la documentation afin de déchiffrer les types de résultats si elle peut être évitée.

Vous n'écrivez ces structures qu'une seule fois, mais vous les créez plusieurs fois, de sorte que vous souhaitez toujours cette création concise (que vous obtenez). Le typage statique garantit que les données que vous en retirez sont conformes à vos attentes.

Cela dit, il reste encore des endroits où de simples nuplets ou des listes peuvent avoir beaucoup de sens. Il peut y avoir moins de temps système à renvoyer un tableau de quelque chose, et si c'est le cas (et que le temps système est significatif, ce que vous détermineriez avec le profilage), alors utilisez un simple tableau de valeurs en interne. peut sembler très judicieux. Votre API publique devrait toujours avoir des types clairement définis.

Joshua Taylor
la source
J'ai utilisé mon propre enum au lieu de TimeUnit à la fin, parce que je viens d'ajouter UNDER1MS avec les unités de temps réelles. Alors maintenant, je n'ai que deux champs, pas trois. (En théorie, je pourrais abuser de TimeUnit.NANOSECONDS, mais ce serait très déroutant.)
Mikhail Ramendik
Je n'ai pas créé de getter, mais je vois maintenant comment un getter me permettrait de lever une exception si, avec originalTimeUnit=DurationTimeUnit.UNDER1MSl'appelant, il tentait de lire la valeur en millisecondes.
Mikhail Ramendik
Je sais que vous en avez parlé, mais souvenez-vous que votre exemple avec les tuples en python concerne en fait des tuples dynamiques et statiques; cela ne dit pas grand chose sur les tuples eux-mêmes. Vous pouvez avoir des n-uplets typés statiquement qui ne compileront pas, comme vous le savez sûrement :)
Andres F.
1
@ Veedrac Je ne suis pas sûr de ce que vous voulez dire. Mon point de vue était qu'il est correct d'écrire le code de bibliothèque de manière plus verbeuse (même si cela prend plus de temps), car il faudra plus de temps pour utiliser le code que pour l' écrire .
Joshua Taylor
1
@ Veedrac Oui, c'est vrai. Mais je maintiens qu'il est une distinction entre le code qui sera réutilisé de façon significative, et le code qui ne sera pas. Par exemple, dans un package Java, certaines classes sont publiques et sont utilisées depuis d'autres emplacements. Ces API doivent être bien pensées. En interne, il est possible que certaines classes n'aient besoin que de se parler. Ces couplages internes sont les endroits où des structures rapides et sales (par exemple, un objet [] censé comporter trois éléments) pourraient convenir. Pour l’API publique, il faut généralement privilégier quelque chose de plus significatif et explicite.
Joshua Taylor
7

Le problème est que vous comparez des pommes à des oranges . Vous avez demandé comment simuler le renvoi de plus d’une valeur unique en donnant un exemple python rapide et sale avec un tuple non typé et vous avez effectivement reçu le message réponse une ligne .

La réponse acceptée fournit une solution commerciale correcte. Aucune solution temporaire rapide que vous auriez à jeter et à mettre en œuvre correctement pour la première fois ne nécessiterait aucune action pratique avec la valeur renvoyée, mais une classe POJO compatible avec un grand nombre de bibliothèques, y compris persistance, sérialisation / désérialisation, instrumentation et tout ce qui est possible.

Ce n'est pas long du tout. La seule chose que vous devez écrire sont les définitions de champs. Des setters, des getters, des hashCodes et des équivalents peuvent être générés. Votre question devrait donc être de savoir pourquoi les accesseurs et les traceurs ne sont pas générés automatiquement mais qu’il s’agit d’un problème de syntaxe (problème du sucre syntaxique, dirait certains) et non pas d’un problème culturel.

Et finalement, vous réfléchissez à essayer d’accélérer quelque chose qui n’a aucune importance. Le temps passé à écrire des classes DTO est insignifiant comparé au temps passé à maintenir et à déboguer le système. Par conséquent, personne n'optimise pour moins de verbosité.

Communauté
la source
2
"Personne" est incorrect du point de vue des faits: de nombreux outils s’optimisent de manière moins verbeuse, les expressions régulières en étant un exemple extrême. Mais vous auriez peut-être voulu dire "personne dans le monde Java traditionnel", et dans cette lecture, la réponse a beaucoup de sens, merci. Ce qui me préoccupe avec la verbosité n’est pas le temps passé à écrire du code, mais le temps passé à le lire; l'IDE ne fera pas gagner du temps de lecture; mais il semble que l’idée est que 99% des fois, on ne lit que la définition de l’interface, alors CELA devrait être concis?
Mikhail Ramendik le
5

Ce sont trois facteurs différents qui contribuent à ce que vous observez.

Tuples versus champs nommés

Peut-être le plus trivial - dans les autres langues, vous utilisiez un tuple. Discuter de la question de savoir si les tuples est une bonne idée n’est pas vraiment le problème, mais en Java, vous utilisiez une structure plus lourde, ce qui rendait la comparaison un peu injuste: vous auriez pu utiliser un tableau d’objets et des transtypages.

Syntaxe de langage

Pourrait-il être plus facile de déclarer la classe? Je ne parle pas de rendre les champs publics ou d'utiliser une carte, mais quelque chose comme les classes de cas de Scala, qui offrent tous les avantages de la configuration que vous avez décrite, mais beaucoup plus concise:

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

Nous pourrions avoir cela - mais il y a un coût: la syntaxe devient plus compliquée. Bien sûr, cela peut valoir la peine dans certains cas, ou même dans la plupart des cas, ou même dans la plupart des cas pour les 5 prochaines années - mais cela doit être jugé. Soit dit en passant, c’est l’un des aspects intéressants des langues que vous pouvez modifier vous-même (par exemple, lisp) - et notez comment cela devient possible grâce à la simplicité de la syntaxe. Même si vous ne modifiez pas réellement le langage, une syntaxe simple permet d'utiliser des outils plus puissants. par exemple, il me manque souvent des options de refactoring disponibles pour Java mais pas pour Scala.

Philosophie du langage

Mais le facteur le plus important est qu’une langue doit permettre une certaine manière de penser. Parfois, cela peut sembler oppressant (j’ai souvent souhaité une prise en charge de certaines fonctionnalités), mais il est tout aussi important de les supprimer que de les avoir. Pourriez-vous tout supporter? Bien sûr, mais vous pourriez aussi bien écrire un compilateur qui compile chaque langue. En d'autres termes, vous n'aurez pas de langue - vous aurez un sur-ensemble de langues et chaque projet adoptera un sous-ensemble.

Il est bien sûr possible d'écrire du code qui va à l'encontre de la philosophie du langage et, comme vous l'avez fait remarquer, les résultats sont souvent laids. Avoir une classe avec seulement quelques champs en Java revient à utiliser un var dans Scala, à dégénérer des prédicats prolog en fonctions, à faire un unsafePerformIO dans haskell, etc. Les classes Java ne sont pas légères - elles ne sont pas là pour transmettre des données . Lorsque quelque chose semble difficile, il est souvent utile de prendre du recul et de voir s'il existe un autre moyen. Dans votre exemple:

Pourquoi la durée est-elle séparée des unités? Il existe de nombreuses bibliothèques de temps qui vous permettent de déclarer une durée - quelque chose comme Durée (5 secondes) (la syntaxe peut varier), qui vous permettront ensuite de faire ce que vous voulez d'une manière beaucoup plus robuste. Peut-être voulez-vous le convertir - pourquoi vérifier si le résultat [1] (ou [2]?) Est une "heure" et multiplié par 3600? Et pour le troisième argument, quel est son but? J'imagine qu'à un moment donné vous devrez imprimer "moins de 1 ms" ou le temps réel - c'est une logique qui appartient naturellement aux données temporelles. C'est à dire que vous devriez avoir un cours comme celui-ci:

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

}

ou ce que vous voulez réellement faire avec les données, encapsulant ainsi la logique.

Bien sûr, il pourrait y avoir un cas où cette méthode ne fonctionnera pas - je ne dis pas qu'il s'agit de l'algorithme magique permettant de convertir des résultats de tuple en code Java idiomatique! Et il peut y avoir des cas où c'est très laid et mauvais et peut-être que vous auriez dû utiliser un langage différent - c'est pourquoi il y en a tellement après tout!

Mais, selon moi, les classes sont des "structures lourdes" en Java, c'est que vous n'êtes pas censé les utiliser en tant que conteneurs de données, mais en tant que cellules logiques autonomes.

Thanos Tintinidis
la source
Ce que je veux faire avec les durées, c'est en réalité en faire une somme et ensuite en les comparant à une autre. Aucune sortie impliquée. La comparaison doit tenir compte de l’arrondi. Par conséquent, j’ai besoin de connaître les unités de temps originales au moment de la comparaison.
Mikhail Ramendik
Il serait probablement possible de créer un moyen d'ajouter et de comparer des instances de ma classe, mais cela pourrait devenir extrêmement lourd. Surtout que je dois aussi diviser et multiplier par des flottants (il y a des pourcentages à vérifier dans cette interface). Donc pour l’instant, j’ai fait une classe immuable avec des champs finaux, ce qui semble être un bon compromis.
Mikhail Ramendik
La partie la plus importante de cette question particulière était l’aspect plus général des choses et, de toutes les réponses, une image semble se dégager.
Mikhail Ramendik
@ MikhailRamendik peut-être une sorte d'accumulateur serait une option - mais vous connaissez mieux le problème. En effet, il ne s'agit pas de résoudre ce problème particulier, désolé si je me suis un peu laissé distraire - mon principal argument est que le langage décourage l'utilisation de données "simples". parfois, cela vous aide à repenser votre approche - d'autres fois, un int n'est qu'un int
Thanos Tintinidis
@ MikhailRamendik La bonne chose à propos de la façon de faire les choses, c'est qu'une fois que vous avez une classe, vous pouvez y ajouter un comportement. Et / ou rendez-le immuable comme vous l'avez fait (c'est une bonne idée la plupart du temps; vous pouvez toujours renvoyer un nouvel objet sous forme de somme). En fin de compte, votre classe "superflue" peut encapsuler tout le comportement dont vous avez besoin et le coût pour les accesseurs devient alors négligeable. De plus, vous pouvez ou non en avoir besoin. Pensez à utiliser lombok ou autovalue .
Maaartinus
5

À ma connaissance, les raisons principales sont

  1. Les interfaces constituent le moyen de base en Java d’abstraire des classes.
  2. Java ne peut renvoyer qu'une seule valeur d'une méthode - un objet, un tableau ou une valeur native (int / long / double / float / boolean).
  3. Les interfaces ne peuvent pas contenir de champs, seulement des méthodes. Si vous souhaitez accéder à un champ, vous devez utiliser une méthode - par conséquent, les accesseurs et les setters.
  4. Si une méthode retourne une interface, vous devez avoir une classe d'implémentation à renvoyer.

Cela vous donne le "Vous devez écrire une classe à renvoyer pour tout résultat non trivial", ce qui est plutôt lourd. Si vous utilisez une classe à la place de l'interface, vous pouvez simplement avoir des champs et les utiliser directement, mais cela vous lie à une implémentation spécifique.

Thorbjørn Ravn Andersen
la source
Pour ajouter à cela: si vous ne avez besoin de ce dans un petit contexte ( par exemple, une méthode privée dans une classe), il est bon d'utiliser un disque nu - idéalement immuable. Mais cela ne devrait vraiment pas vraiment fuir dans le contrat public de la classe (ou pire, dans la bibliothèque!). Vous pouvez décider de ne pas utiliser d’enregistrements simplement pour éviter que cela ne se produise lors de modifications ultérieures du code; c'est souvent la solution la plus simple.
Luaan
Et je suis d’accord avec la vue "Les tuples ne fonctionnent pas bien en Java". Si vous utilisez des génériques, cela finira très vite par être désagréable.
Thorbjørn Ravn Andersen
@ ThorbjørnRavnAndersen Ils fonctionnent assez bien en Scala, un langage typé de manière statique avec des génériques et des tuples. Alors peut-être que c'est la faute de Java?
Andres F.
@AndresF. Veuillez noter la partie "Si vous utilisez des génériques". La manière dont Java le fait ne se prête pas à des constructions complexes, contrairement à Haskell, par exemple. Je ne connais pas assez Scala pour effectuer une comparaison, mais est-il vrai que Scala autorise plusieurs valeurs de retour? Cela pourrait aider beaucoup.
Thorbjørn Ravn Andersen
@ ThorbjørnRavnAndersen Les fonctions Scala ont une seule valeur de retour qui peut être de type tuple (par exemple, def f(...): (Int, Int)une fonction fqui renvoie une valeur qui se trouve être un tuple d'entiers). Je ne suis pas sûr que les génériques en Java posent problème; Notez que Haskell fait aussi un effacement, par exemple. Je ne pense pas qu'il y ait une raison technique pour laquelle Java n'a pas de nuplets.
Andres F.
3

Je suis d'accord avec JacquesB pour répondre que

Java est (traditionnellement) un langage OO basé sur une classe à paradigme unique qui optimise le caractère explicite et la cohérence.

Mais explicite et cohérence ne sont pas les objectifs finaux à optimiser. Lorsque vous dites que «python est optimisé pour la lisibilité», vous mentionnez immédiatement que l'objectif final est «maintenabilité» et «vitesse de développement».

Que faites-vous lorsque vous avez une cohérence et une cohérence, en mode Java? Mon point de vue est qu'il a évolué en tant que langage prétendant fournir un moyen prévisible, cohérent et uniforme de résoudre tout problème logiciel.

En d’autres termes, la culture Java est optimisée pour faire croire aux gestionnaires qu’ils comprennent le développement de logiciels.

Ou, comme un sage l'a dit il y a très longtemps ,

La meilleure façon de juger une langue est de consulter le code rédigé par ses promoteurs. "Radix enim omnium malorum est cupiditas" - et Java est clairement un exemple de programmation axée sur l'argent (MOP). Comme le défenseur principal de Java chez SGI m'a dit: "Alex, tu dois aller où l'argent est." Mais je ne veux pas particulièrement aller là où se trouve l'argent - cela ne sent généralement pas bon là-bas.

artem
la source
3

(Cette réponse n'est pas une explication de Java en particulier, mais aborde plutôt la question générale de «Qu'est-ce que les pratiques [lourdes] peuvent optimiser?»)

Considérez ces deux principes:

  1. C'est bien quand votre programme fait la bonne chose. Nous devrions faciliter l'écriture de programmes qui font le bon choix.
  2. C'est mauvais quand votre programme fait la mauvaise chose. Nous devrions rendre plus difficile l’écriture de programmes mal conçus.

Vouloir optimiser l’un de ces objectifs peut parfois gêner l’autre (c’est-à-dire qu’il est plus difficile de faire ce qui est mal peut aussi l’empêcher de faire ce qui est bien , ou inversement).

Les compromis à effectuer dans chaque cas dépendent de l'application, des décisions des programmeurs ou de l'équipe en question et de la culture (de l'organisation ou de la communauté linguistique).

Par exemple, si un bogue ou une interruption de quelques heures dans votre programme pouvait entraîner des pertes en vies humaines (systèmes médicaux, aéronautique) ou même simplement de l'argent (comme des millions de dollars dans les systèmes de publicité de Google), vous feriez des compromis différents dans votre langue, mais aussi dans d’autres aspects de la culture de l’ingénierie) que vous ne le feriez pour un scénario unique: il s’agit probablement du côté "lourd".

D'autres exemples ont tendance à rendre votre système plus "lourd":

  • Lorsque de nombreuses équipes ont travaillé sur une base de code volumineuse pendant de nombreuses années, l'un des problèmes majeurs est que quelqu'un puisse utiliser l'API d'un autre utilisateur de manière incorrecte. Une fonction appelée avec des arguments dans le mauvais ordre, ou appelée sans garantir certaines conditions préalables / contraintes attendues, peut être catastrophique.
  • En tant que cas particulier, supposons que votre équipe gère une API ou une bibliothèque particulière et souhaite la modifier ou la refactoriser. Plus vos utilisateurs sont «limités» dans l'utilisation de votre code, plus il est facile de le modifier. (Notez qu'il serait préférable ici d'avoir des garanties réelles que personne ne pourrait l'utiliser de manière inhabituelle.)
  • Si le développement est divisé entre plusieurs personnes ou équipes, il peut sembler judicieux de faire en sorte qu'une personne ou une équipe «spécifie» l'interface et que d'autres la mettent en œuvre. Pour que cela fonctionne, vous devez être en mesure de gagner un certain degré de confiance lorsque la mise en œuvre est terminée, que celle-ci correspond réellement à la spécification.

Ce ne sont là que quelques exemples, pour vous donner une idée des cas dans lesquels rendre les choses «lourdes» (et rendre plus difficile l’écriture de code rapidement) peut réellement être intentionnel. (On pourrait même dire que si écrire du code demande beaucoup d'efforts, cela peut vous amener à réfléchir plus attentivement avant d'écrire du code! Bien sûr, cette argumentation devient vite ridicule.)

Un exemple: le système Python interne de Google a tendance à alourdir les choses de telle sorte que vous ne pouvez pas simplement importer le code de quelqu'un d'autre, vous devez déclarer la dépendance dans un fichier BUILD . L'équipe dont vous souhaitez importer le code doit alors déclarer sa bibliothèque visible par votre code, etc.


Remarque : Tout ce qui précède concerne à peu près le moment où les choses ont tendance à devenir "lourdes". Je ne prétends absolument pas que Java ou Python (ni les langages eux-mêmes, ni leurs cultures) soient des compromis optimaux pour un cas particulier; c'est à vous de penser. Deux liens connexes sur ces compromis:

ShreevatsaR
la source
1
Qu'en est-il de la lecture du code? Il y a un autre compromis là-bas. Le code alourdi par des incantantions bizarres et des rituels abondants est plus difficile à lire; avec un texte standard et un texte inutile (et des hiérarchies de classes!) dans votre IDE, il devient plus difficile de voir ce que le code fait réellement. Je peux voir que le code ne devrait pas nécessairement être facile à écrire (je ne suis pas sûr de l’accepter, mais il a un certain mérite), mais il devrait certainement être facile à lire . Il est évident que le code complexe peut et a été construit avec Java - ce serait stupide de prétendre le contraire - mais était - ce grâce à sa verbosité, ou en dépit de celui - ci?
Andres F.
@AndresF. J'ai édité la réponse pour préciser qu'il ne s'agissait pas de Java (dont je ne suis pas un grand fan). Mais oui, compromis lors de la lecture du code: d'un côté, vous voulez pouvoir facilement lire «ce que le code fait réellement», comme vous l'avez dit. De l’autre côté, vous voulez pouvoir voir facilement les réponses à des questions telles que: comment ce code est-il lié à un autre code? Quelle est la pire chose que ce code puisse faire: quel peut être son impact sur un autre État, quels sont ses effets secondaires? Sur quels facteurs externes ce code pourrait-il éventuellement dépendre? (Bien entendu, nous souhaitons des réponses rapides aux deux séries de questions.)
ShreevatsaR Le
2

La culture Java a évolué au fil du temps et a été fortement influencée par les arrière-plans des logiciels open source et des logiciels d’entreprise, ce qui est un mélange étrange si vous y réfléchissez vraiment. Les solutions d'entreprise exigent des outils lourds et l'open source exige de la simplicité. Le résultat final est que Java est quelque part au milieu.

Une partie de ce qui influence la recommandation est ce qui est considéré comme lisible et maintenable en Python et Java est très différent.

  • En Python, le tuple est une fonctionnalité du langage .
  • En Java et en C #, le tuple est (ou serait) une fonctionnalité de bibliothèque .

Je ne mentionne que C # car la bibliothèque standard a un ensemble de classes Tuple <A, B, C, .. n>, et c'est un exemple parfait de la lourdeur des n-uplets si le langage ne les prend pas directement en charge. Dans presque tous les cas, votre code devient plus lisible et maintenable si vous avez des classes bien choisies pour gérer le problème. Dans l'exemple spécifique de votre question liée au débordement de pile, les autres valeurs seraient facilement exprimées sous forme de getters calculés sur l'objet de retour.

Une solution intéressante proposée par la plate-forme C # et offrant un juste milieu heureux est l’idée d’ objets anonymes (publiée en C # 3.0 ) qui gratte assez bien cette démangeaison. Malheureusement, Java n'a pas encore d'équivalent.

Jusqu'à ce que les fonctionnalités du langage Java soient modifiées, la solution la plus lisible et la plus maintenable consiste à avoir un objet dédié. Cela est dû à des contraintes dans le langage qui remontent à ses débuts en 1995. Les auteurs originaux avaient prévu de nombreuses autres fonctionnalités qui ne l'avaient jamais été, et la compatibilité en amont est l'une des principales contraintes qui pèsent sur l'évolution de Java.

Berin Loritsch
la source
4
Dans la dernière version de c #, les nouveaux n-uplets sont des citoyens de première classe plutôt que des classes de bibliothèque. Il a fallu trop de versions pour y arriver.
Igor Soloydenko
Les trois derniers paragraphes peuvent être résumés comme suit: "Le langage X a la fonctionnalité que Java ne recherche pas", et je ne vois pas ce qu’ils ajoutent à la réponse. Pourquoi mentionner C # dans une rubrique Python to Java? Non, je ne vois tout simplement pas le but. Un peu bizarre d'un gars de 20k + représentant.
Olivier Grégoire
1
Comme je l'ai dit, "je ne mentionne que C # parce que Tuples est une fonctionnalité de bibliothèque" similaire à ce qui fonctionnerait en Java s'il avait Tuples dans la bibliothèque principale. C'est très difficile à utiliser, et la meilleure solution est presque toujours d'avoir une classe dédiée. Les objets anonymes égratignent une démangeaison similaire à celle pour laquelle les n-uplets sont conçus. Sans support linguistique pour quelque chose de mieux, vous devez travailler avec ce qui est le plus facile à maintenir.
Berin Loritsch
@IgorSoloydenko, peut-être qu'il y a de l'espoir pour Java 9? Ceci est juste une de ces fonctionnalités qui sont la prochaine étape logique de lambdas.
Berin Loritsch
1
@IgorSoloydenko Je ne suis pas sûr que ce soit tout à fait vrai (citoyens de première classe). Ils semblent être des extensions de syntaxe permettant de créer et de décompresser des instances de classes à partir de la bibliothèque. Ils bénéficient plutôt d'un support de première classe dans le CLR sous la forme class, struct, enum.
Pete Kirkham
0

Je pense que l’un des éléments fondamentaux de l’utilisation d’une classe dans ce cas est que ce qui est ensemble doit rester ensemble.

J'ai eu cette discussion en sens inverse, à propos des arguments de méthode: considérons une méthode simple qui calcule l'IMC:

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

Dans ce cas, je dirais contre ce style car poids et taille sont liés. La méthode "communique", ce sont deux valeurs distinctes quand elles ne le sont pas. Quand calculeriez-vous un IMC avec le poids d'une personne et la taille d'une autre? Cela n'aurait pas de sens.

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

Cela a plus de sens parce que vous communiquez clairement maintenant que la taille et le poids proviennent de la même source.

La même chose vaut pour renvoyer plusieurs valeurs. Si elles sont clairement connectées, renvoyez un petit paquet ordonné et utilisez un objet, si elles ne renvoient pas plusieurs valeurs.

Pieter B
la source
4
Ok, mais supposons que vous ayez ensuite une tâche (hypothétique) pour afficher un graphique: comment l’IMC dépend-il du poids pour une hauteur fixe donnée? Ensuite, vous ne pouvez pas faire cela sans créer une personne pour chaque point de données. Et, à l'extrême, vous ne pouvez pas créer d'instance de classe Personne sans date de naissance et numéro de passeport valides. Maintenant quoi? Je n'ai pas voté par ailleurs, il existe des raisons valables de regrouper des propriétés dans une classe.
artem
1
@artem est la prochaine étape où les interfaces entrent en jeu. Donc, une interface personweightheight pourrait être quelque chose comme ça. Vous êtes pris dans l'exemple que j'ai donné pour faire valoir que ce qui va ensemble devrait être ensemble.
Pieter B
0

Pour être franc, la culture veut que les programmeurs Java aient tendance à provenir d'universités où des principes orientés objet et des principes de conception de logiciels durables ont été enseignés.

Comme ErikE le dit plus en détail dans sa réponse, vous ne semblez pas écrire de code durable. Ce que je vois de votre exemple, c’est un enchevêtrement très inquiétant de préoccupations.

Dans la culture Java, vous aurez tendance à savoir quelles bibliothèques sont disponibles, ce qui vous permettra de réaliser bien plus que votre programmation improvisée. Ainsi, vous échangeriez vos particularités contre les modèles et les styles de conception qui avaient été essayés et testés dans des environnements industriels à noyau dur.

Mais comme vous le dites, cela n’est pas sans inconvénient: aujourd’hui, j’utilise Java depuis plus de 10 ans, j’ai tendance à utiliser soit Node / Javascript, soit Go for de nouveaux projets, car ils permettent un développement plus rapide. suffit souvent. À en juger par le fait que Google utilisait déjà beaucoup Java, mais a été à l'origine de Go, je suppose qu'ils pourraient faire de même. Mais même si j'utilise maintenant Go et Javascript, j'utilise toujours de nombreuses compétences en conception que j'ai acquises au cours de nombreuses années d'utilisation et de compréhension de Java.

À M
la source
1
Notamment, l'outil de recrutement foobar utilisé par Google permet d'utiliser deux langues pour les solutions: java et python. Je trouve intéressant que Go ne soit pas une option. Je suis tombé sur cette présentation sur Go qui contient quelques points intéressants sur Java et Python (ainsi que sur d’autres langages). Par exemple, il ya un point qui ressort: "Peut-être, par conséquent, les programmeurs non experts ont-ils confondu utilisez "avec interprétation et frappe dynamique". Ça vaut le coup d'être lu.
JimmyJames
@JimmyJames vient d'arriver sur la diapositive en disant "entre les mains des experts, ils sont excellents" - bon résumé du problème posé par la question
Tom