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.
la source
Réponses:
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.
la source
for
boucle quand unmap
serait plus approprié (et lisible). Il y a un juste milieu.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:
Utilisez
Triplet<A, B, C>
le type de retour (qui devrait vraiment êtrejava.util
, et je ne peux pas vraiment expliquer pourquoi il n'est pas. D' autant plus que JDK8 introduireFunction
,BiFunction
,Consumer
,BiConsumer
, etc ... Il semble juste quePair
etTriplet
au moins aurait du sens. Mais je digresse )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.
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.Duration
objet, ce qui serait beaucoup plus explicite que les deux cas précédents.la source
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:
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:
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:
la source
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
ApproximateDuration
type 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_1ms
et 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) :
Voici une implémentation Java de ce qui précède:
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
struct
type 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).la source
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.
la source
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:
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.
la source
À 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:
la source
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:
"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:
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:
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:
quand vous vouliez dire:
Avec ce type d’interface Java, vous ne pouvez pas compiler:
du tout. Tu dois faire:
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:
Ç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 direresult[1]
. Vous devez toujours créer des instances assez succinctement, et en extraire des données n'est pas si difficile non plus: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.
la source
originalTimeUnit=DurationTimeUnit.UNDER1MS
l'appelant, il tentait de lire la valeur en millisecondes.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é.
la source
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:
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:
}
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.
la source
À ma connaissance, les raisons principales sont
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.
la source
def f(...): (Int, Int)
une fonctionf
qui 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.Je suis d'accord avec JacquesB pour répondre que
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 source
(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:
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":
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:
la source
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.
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.
la source
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:
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.
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.
la source
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.
la source