Edit comme j'avais des doutes sur le benchmark lancé depuis Intellij, j'ai recodé un comparatif complet qui aura tourné 4 heures sur ma machine
Avec Kotlin, sous Java 21, en utilisant JMH, j'obtiens des résultats en total contradiction avec ceux obtenus via IntelliJ. L'écart passe de 12% à 2,2% dans le pire des cas.
Pour mon benchmark, j'ai calculé la longueur d'une String
avec la fonction suivante : 2 * str.length + 1
. Le but étant d'illustrer quel type d'appel est le plus rapide pour effectuer ce calcul.
Voici mes tableaux de comparatifs :
Benchmark (Sorted by Worst) | Score | Error | Worst | Best | Units | Note Relative | Gain |
---|---|---|---|---|---|---|---|
ValueClass(str).size() | 2 252 | 34 | 2 218 | 2 287 | ops/us | 1,000 | 0,0 % |
build_lambda(str)() | 2 269 | 25 | 2 244 | 2 294 | ops/us | 1,012 | 1,2 % |
Class::size(str) | 2 282 | 37 | 2 245 | 2 320 | ops/us | 1,012 | 1,2 % |
OpenIntance(str).size() | 2 275 | 27 | 2 249 | 2 302 | ops/us | 1,014 | 1,4 % |
Instance(str).size() | 2 285 | 37 | 2 249 | 2 322 | ops/us | 1,014 | 1,4 % |
DataClass(str).size() | 2 273 | 22 | 2 251 | 2 295 | ops/us | 1,015 | 1,5 % |
object.method(str) | 2 272 | 19 | 2 253 | 2 291 | ops/us | 1,016 | 1,6 % |
this.size(str) | 2 282 | 28 | 2 254 | 2 310 | ops/us | 1,016 | 1,6 % |
build_anonymous_function(str)() | 2 266 | 5 | 2 261 | 2 271 | ops/us | 1,019 | 1,9 % |
Benchmark (Sorted by Best) | Score | Error | Worst | Best | Units | Note Relative | Gain |
---|---|---|---|---|---|---|---|
build_anonymous_function(str)() | 2 266 | 5 | 2 261 | 2 271 | ops/us | 1,000 | 0,0 % |
ValueClass(str).size() | 2 252 | 34 | 2 218 | 2 287 | ops/us | 1,007 | 0,7 % |
object.method(str) | 2 272 | 19 | 2 253 | 2 291 | ops/us | 1,009 | 0,9 % |
build_lambda(str)() | 2 269 | 25 | 2 244 | 2 294 | ops/us | 1,010 | 1,0 % |
DataClass(str).size() | 2 273 | 22 | 2 251 | 2 295 | ops/us | 1,011 | 1,1 % |
OpenIntance(str).size() | 2 275 | 27 | 2 249 | 2 302 | ops/us | 1,014 | 1,4 % |
this.size(str) | 2 282 | 28 | 2 254 | 2 310 | ops/us | 1,017 | 1,7 % |
Class::size(str) | 2 282 | 37 | 2 245 | 2 320 | ops/us | 1,021 | 2,1 % |
Instance(str).size() | 2 285 | 37 | 2 249 | 2 322 | ops/us | 1,022 | 2,2 % |
Benchmark (Sorted by Score) | Score | Error | Worst | Best | Units | Note Relative | Gain |
---|---|---|---|---|---|---|---|
ValueClass(str).size() | 2 252 | 34 | 2 218 | 2 287 | ops/us | 1,000 | 0,0 % |
build_anonymous_function(str)() | 2 266 | 5 | 2 261 | 2 271 | ops/us | 1,006 | 0,6 % |
build_lambda(str)() | 2 269 | 25 | 2 244 | 2 294 | ops/us | 1,007 | 0,7 % |
object.method(str) | 2 272 | 19 | 2 253 | 2 291 | ops/us | 1,009 | 0,9 % |
DataClass(str).size() | 2 273 | 22 | 2 251 | 2 295 | ops/us | 1,009 | 0,9 % |
OpenIntance(str).size() | 2 275 | 27 | 2 249 | 2 302 | ops/us | 1,010 | 1,0 % |
this.size(str) | 2 282 | 28 | 2 254 | 2 310 | ops/us | 1,013 | 1,3 % |
Class::size(str) | 2 282 | 37 | 2 245 | 2 320 | ops/us | 1,013 | 1,3 % |
Instance(str).size() | 2 285 | 37 | 2 249 | 2 322 | ops/us | 1,015 | 1,5 % |
Conclusion après benchmark
Créer des instances à la Yegor Bugayenko semble être globalemment l'une si ce n'est la meilleure des solutions lorsque l'on observe son positionnement qui est au pire moyen sinon deux fois premier.
J'ai passé le nouvel an avec @Kysofer & @Chlouloutte. Quand tous les enfants ont le même âge c'est vraiment sympa ; surtout quand ils se couchent tous en même temps :P
Mais passons, ce matin après ce qui sera déjà l'une des plus courtes nuit de cette nouvelle année, je discute avec @Kysofer de ces projets pour 2024, il me sort un truc improbable :
Je pense que je vais abandonner la programmation orientée objet pour la programmation fonctionnelle...
Là, je le regarde limite choquée... Il faut dire que lui et moi partageons - je devrais dire partagions - le même avis sur la programmation fonctionnelle, notamment depuis nos déboires en OCaml (merci le programme de MPSI).
Donc, avec le plus grand respect je lui réponds :
Nani the fuck !?
À ma décharge je n'ai pas bu un seul verre d'alcool depuis la grossesse de ma petite dernière. Et oui. :P
Là il m'explique tout un tas de trucs et je dois dire que je me sens déstabilisée. Je m'explique, j'ai vécu trop de fois le fait de devoir maintenir du Scala pour savoir à quel point, sur le temps long, la programmation fonctionnelle pure devient encore moins maintenable que du Java + Spring Boot.
D'autant que je m'attendais à ce qu'il m'annonce qu'il allait (enfin) se monter un Shaarli.
Bref son idée est dirigée par les contraintes suivantes :
- Avoir un code dirigé par des contrats (au sens interfaces Java) dont le respect du typage est prouvé lors de la compilation et garanti lors de l'exécution.
- Ne pas avoir besoin de créer des instances techniques qui implémentent ces contrats, afin d'encapsuler des données dedans.
- Parvenir quand même à encapsuler les données pour les protéger mais sans jamais créer de nouvelles instances (cf. le point précédent).
- Avoir les mêmes performance d'exécution que du chaînage d'appels de fonctions
static
; et donc l'eldorado du zero GC-time. - Permettre une exécution différée/lazy/paresseuse.
Quand on lit le truc, on se dit que la vie n'est plus à un paradoxe prêt, mais pourtant sa façon de faire fonctionne.
Je vais essayer de vous décrire ce qu'il m'a expliqué afin de mieux l'ingérer moi-même.
Exemple : Déterminer si deux trucs sont égaux
Dans tous les cas de figure, nous allons supposer que les données sont décorées par une implémentation de l'interface suivante qui permet de les représenter sous la forme d'un tableau d'octets.
interface RawData {
fun asBytes():ByteArray
}
Version OOP Pure
1) On exprime le concept de vérification via une interface :
interface Check {
fun status():Boolean
}
Ensuite on fournit une implémentation de l'interface Check
qui soit spécialisée dans la comparaison de deux RawData
:
class IsEqual(
private val left:RawData,
private val right:RawData
):Check {
override fun status():Boolean {
return this.isSameInstances() || this.haveSameBytes()
}
private fun haveSameBytes():Boolean {
return left.asBytes().contentEquals(right.asBytes())
}
private fun isSameInstances():Boolean {
return left === right
}
}
Et ça s'utilise comme ceci :
val left:RawData = StrAsData("same data")
val right:RawData = StrAsData("same data")
val comparison:Boolean = IsEqual(left, right).status()
Problème
Pour comparer deux objets on doit forcément créer une instance de IsEqual
puis invoquer une méthode dessus. Sur JRE, c'est ~12% de temps de calcul supplémentaire par rapport à un simple appel de méthode statiques !
Version PF pure
Avant toute chose, il faut comprendre ce qu'est un contrat pour @Kysofer. C'est un truc qui va prouver à la compilation que les types d'entrée et de sortie soient bien respectés, ce qui fait que l'implémentation A pourra se substituer à l'implémentation B sans problème lors du runtime.
Mais comment peut-on produire l'équivalent d'une interface en PF ?
=> On passe par des fonctions de premier ordre.
En effet, une méthode qui reçoit une fonction de premier ordre ne sait pas ce qu'elle va exécuter. Elle sait juste que la fonction prendra un certain type en entrée et un certain type en retour. Le reste, c'est le compilateur qui agit.
Si l'on reprend l'interface Check
cela nous donne le prototype suivant :
val prototype:() -> Boolean
C'est-à-dire un Predicat
sans paramètre.
Maintenant passons à l'implémentation naïve, ceci nous donne :
fun isEqual(left:RawData, right:RawData):() -> Boolean {
return {
isSameInstances(left, right) || haveSameBytes(left, right)
}
}
private inline fun haveSameBytes(left:RawData, right:RawData):Boolean {
return left.asBytes().contentEquals(right.asBytes())
}
private inline fun isSameInstances(left:RawData, right:RawData):Boolean {
return left === right
}
On voit bien que la fonction isEqual(..)
retourne une fonction de premier ordre qui elle-même retournera un Boolean
.
Rappel
En Kotlin, les lambda sont déclarées entre deux accolades { .. }
.
Sauf que cet exemple a un problème puisqu'une lambda, sur JRE tout du moins, c'est une instance de l'interface Function
ou l'un de ses équivalents. Bref, il n'y a pas de gain en terme de performance par rapport la version OOP.
Pour l'obtenir, il suffit de remplacer la création d'une lambda par un pointeur sur une fonction anonyme, ce qui nous donne :
fun isEqual(left:RawData, right:RawData) = fun():Boolean {
return isSameInstances(left, right) || haveSameBytes(left, right)
}
Et la ça devient magique car la fonction anonyme étant écrite directement dans le byte-code et épinglée une et une seule fois en mémoire, cela revient à avoir un pointeur sur fonction qui exécutera la-dite fonction.
=> Soit un gain de 12% en termes de performance, ce qui fait qu'on rattrape la vitesse d'exécution d'un chaînage de méthodes statiques.
Mieux que cela, @Kysofer s'appuie sur une variante de la Curryfication . En effet, la méthode isEqual(..)
sert à encapsuler les données dans la pile d'appel pour les rendre utilisables que par la fonction anonyme. Donc il devient impossible de savoir ce qui sera utilisé par la lambda qui sera retournée.
=> Les paramètres left
et right
sont bien encapsulés dans la lambda !
Si on résume
- Le fait que la fonction
isEqual(..)
retourne une lambda et non un résultat fait que l'on va déclarer un contrat et donc rendre les lambda interchangeables. - Le fait que l'on retourne une fonction et non un résultat permet un calcul différé/paresseux potentiellement dans un autre thread.
- Le fait que l'on passe par une fonction anonyme qui utilisera les paramètres de la fonction déclarée fait qu'on encapsulera bien les données avant d'exécuter quoi que ce soit dessus.
- Le fait que tout soit
fonction
garanti l'absence de création d'instances techniques.
Nous avons donc bien là une version fonctionnelle de l'encapsulation et des contrats de l'OOP.
Ça aura pris 15 ans mais je pense être réconciliée totalement avec la PF (O_O) ; sauf le récursif, ça jamais ! En tout cas pas avant une démonstration comme celle-ci :P
Je travaille en ce moment sur deux projets, un en Java/Kotlin pour des clients, un second en Rust pour des besoins internes, et j'ai réalisé la chose suivante :
- En Java/Kotlin, le langage est fait pour vous aider mais vous allez vous battre contre les frameworks.
- En Rust, les frameworks sont faits pour vous aider mais vous allez vous battre contre le langage.
C'est incroyable à quel point le borrow checker vous pousse à écrire du code procédurale et mécaniquement intestable. Dès l'instant où l'on souhaite passer par des traits tout devient ultra compliqué. Dernièrement je me suis faite avoir en utilisant des Rc
et des RcCell
sur un code qui allait devenir multi-thread #Horreur
Pour moi, casser le couplage entre deux composants via une interface/trait est IN-DIS-PEN-SABLE mais Rust pousse à définir des structures comme contrat d'interfaçage principal entre deux pans de code.
Forcément, le langage se transforme en enfer dès l'instant où l'on souhaite dépendre d'un trait et non d'une structure (c'est pourtant le 'I' de SOLID, dépendre des Interfaces mais pas des Implémentations).
En même temps je l'avais déjà dit par le passé, le paradigme fonctionnel pur est un cancer métastasé car l'expressivité de la syntaxe donne le sentiment que des tests ne sont pas nécessaires or c'est toujours faux.
Et j'ai suffisamment souffert de Java dans ma vie pour haïr le fait de devoir mocker/instancier/déclarer quarante-douze-mille trucs avant de pouvoir tester une simple fonction.
Bref, deux ans sur Rust à temps partiel et je me vois retourner dans le RustBook tous les 4 mois pour y chercher un truc #Pénible alors qu'en Kotlin ça ne m'arrive jamais.
En un code d'exemple :
// use macro_rules! <name of macro>{<Body>}
macro_rules! add {
// macth like arm for macro
($a:expr,$b:expr) => {
// macro expand to this code
{
// $a and $b will be templated using the value/variable provided to macro
$a+$b
}
}
}
// Usage in code
fn main(){
// call to macro, $a=1 and $b=2
add!(1,2);
}
Attention à ne pas abuser de la méta-programmation car cela peut augmenter significativement les temps de compilation.
Différences entre les enums Option et Result en Rust.
Il n'y a pas à dire, à chaque fois que je code et où le langage m'oblige à utiliser des Optional, je me dis que c'est un langage pourri. Et ceci inclus Rust malgré tout le bien que je pense de lui 😤 !
La meilleure façon de gérer les cas de nullité se trouve dans Kotlin 🥰, TypeScript et Groovy. Le type Optional est une technique archaïque et verbeuse quand on vient du null-check de Kotlin.
Mais bon j'ai déjà expliqué par le passé en quoi le paradigme fonctionnel pure était un cancer métastasé 🤮. Et entre C++ et Rust pour de la programmation système il n'y a plus vraiment de discussion à cette heure, donc faisons-nous violence avec les Optional.
Mon rêve serait un langage natif reprenant la syntaxe de Kotlin (sauf tout ce qui touche aux getter/setter) avec le borrow checker de Rust et qui produise des binaires natifs. Je pense que je peux rêver encore longtemps 🥲
Article vraiment très bien proposé ici par @Sebsauvage.
On se rapproche très fortement de ce que propose David West dans Object Thinking (livre qui a 20 ans déjà) et Yegor Bugayenko dans Elegant Objects (livre qui doit fêter ses 8/9 ans). Je me permets de compléter la solution de la pratique n° 3.
L'auteur propose d'encapsuler la hauteur dans une entité, ce qui nous donne :
// Primitive contenue dans un objet (aussi appelé Value Object)
class ArticleHeight {
private value: number;
constructor(value: number) {
if (value < 10) {
throw new HeightCanNotBeLessThanTen();
}
if (value > 100) {
throw new HeightCanNotBeGreaterThan100();
}
this.value = value;
}
}
// passage de notre ArticleHeight dans le constructor
class Article {
private height: ArticleHeight;
constructor(height: ArticleHeight) {
this.height = height;
}
}
// eh voilou !
class AddArticleUsecase {
execute({ height }) {
//...
const article = new Article(new ArticleHeight(height));
//...
}
}
Pas moi. Je propose que la classe Article
s'attende à recevoir en paramètre une interface Height
dont l'une des implémentations possible soit une ArticleHeight
mais qui pourrait très bien être une valeur venant d'une BDD au moyen d'une HeightFromBdd
(pas le meilleur nom, mais c'est pour représenter l'idée).
Ceci casse le couplage entre deux classes concrètes et subséquemment facilite les mocks/stubs durant les tests dont l'auteur ne parle pas.
Ce qui nous donne
// Primitive contenue dans un objet (aussi appelé Value Object)
interface Height {
value(): number
}
class ArticleHeight implements Height {
private value: number;
constructor(value: number) {
if (value < 10) {
throw new TooShortLength("Un article doit faire au minium 10 lignes");
}
if (value > 100) {
throw new TooLongLength("Un article ne peut faire plus de 100 lignes");
}
this.value = value;
}
value(): number {
return this.value;
}
}
class Article {
private height: Height;
constructor(height: Height) {
this.height = height;
}
}
class AddArticleUsecase {
execute({ height }) {
//...
const article = new Article(new ArticleHeight(height));
//...
}
}
Et sur le même modèle, si votre classe expose une méthode publique, c'est qu'elle est imposée par une interface, dans tous les autres cas de figure les méthodes qui ne viennent pas d'interfaces doivent être privées.
Ce serait la 10ème règle que j'ajouterais à l'article.
Les observateurs auront aussi remarqué que j'ai changé les exceptions. Il ne vaut pas confondre nom de l'exception et contexte dans lequel une erreur été levée. D'où l'importance d'un message qui exprime la raison d'une erreur et le nom de l'exception qui exprime le type d'erreur remontée.
Je cite :
« À mesure qu'Android passe de C/C++ à Java/Kotlin/Rust, nous nous attendons à ce que le nombre de vulnérabilités liées à la sécurité de la mémoire continue de diminuer. Vivement un avenir où les bogues de corruption de la mémoire sur Android seront rares », a conclu Google.
Rust et Kotlin sont deux superbes langages (avec une petite préférence pour Kotlin). J'ai pourtant quelques reproches à faire à l'un et à l'autre mais quand je vois que le marché avance vers eux à grands pas, autant vous dire que j'en suis toute chose <3
Que du beau sous le soleil.
Kotiln, Rust et Python progressent et de plus en plus de développeurs les adoptent (et c'est très bien).
J'ai été une grande utilisatrice de Python il y a un peu plus d'une quinzaine d'années lorsque je travaillais en labo sur du Data-Mining (l'ancêtre du Machine Learning). J'avais laissé de côté Python pour trois raisons à l'époque :
- Les problèmes de performance.
- Les problèmes d'outillage autour du build.
- Le fait que les programmes écrits en Python, pour être rapides, doivent utiliser des libs écrites en C, et donc avec un code orienté procédure.
Aujourd'hui, si je devais produire un système temps-réel et très peu énergivore, je partirais sur Rust.
Dans tous les autres cas de figure, je prendrais Kotlin sur OpenJDK ou Kotlin native (via le compilateur Kotlin-native ou GraalVM).
Par contre Python n'est plus du tout dans ma liste car pour moi à présent, si l'exécution d'un langage n'est pas prouvée à la compilation, c'est un stop immédiat. La majorité des développeurs n'écrivant pas de tests et maîtrisant mal le code (en tout cas en industrie) c'est indispensable.
Article très intéressant sur le vocabulaire que je vais essayé de résumer.
La Programmation Dynamique (DP)
La programmation dynamique consiste à réorganiser un arbre d'appels récursifs en graphe orienté convergent, où des embranchements impliquant la même exécution vont voir leurs résultats mémorisés afin de ne pas être recalculés.
Typiquement pour le calcul d'une suite de Fibonacci nous avons cet arbre avant :
Transformé en ce graphe après (ou le résultat de **fib(1) a été mémorisé) :
La memoization
La mémoïsation en français est une technique qui consiste à mettre en cache les valeurs déjà calculée.
L'exemple de l'article est le suivant :
fun square(x) {
return x * x
}
fun square_memoized(x) {
if (mem[x] is not set) {
mem[x] = x * x
}
return mem[x]
}
La tabulation
Cette technique ressemble un peu à la memoization mais consiste à remplir le cache systématiquement jusqu'au moment où l'on trouve la bonne valeur, alors que la memoization va se concentrer sur la valeur à retourner directement.
L'exemple donné est le suivant :
fun fib_tab(n) {
mem[0] = 0
mem[1] = 1
for i = 2...n
mem[i] = mem[i-2] + mem[i-1]
return mem[n]
}
Donc l'exécution réelle donne ceci :
mem[0] = 0
mem[1] = 1
mem[2] = mem[0] + mem[1]
mem[3] = mem[1] + mem[2]
mem[4] = mem[2] + mem[3]
On voit bien que le cache est surutilisé dans le cadre de la tabulation puisque chaque valeur suivante s'appuie sur la précédente forcément cachée. De facto, la tabulation consomme plus de mémoire mais garantie un accès instantané à la valeur dès la seconde utilisation, à l'image des caches gloutons finalement ("goutons" au sens algorithmes gloutons du terme).
Je me souviens que je disais en 2010 à @Roger quelque chose du type :
La programmation fonctionnelle est un cancer.
Heureusement, dix ans plus tard j'ai quand même un peu changé d'avis sur la question, je dirais aujourd'hui :
La programmation fonctionnelle "pure" est un cancer.
La différence étant que je suis en mesure d'argumenter le ressenti inconscient que j'avais à l'époque. Depuis le bouquin sur les design-patterns du Gang of Four, nous avons eu une pléthore d'autres auteurs qui nous ont expliqué pourquoi il faut toujours dépendre des interfaces et jamais des implémentations. Le problème avec ça, c'est qu'une grande partie des développeurs ne comprennent pas bien ce que sont les implémentations et surtout pourquoi il ne faut pas dépendre d'elles.
En réalité, une implémentation embarque avec elle des attributs (et si un objet n'a pas d'attribut c'est que le développeur a codé en procédurale "pure", il n'a rien encapsulé, ça n'est pas objet du tout mais je m'égare). Le problème avec les implémentations ce sont justement les attributs qui deviennent visibles. Dit autrement nous commençons à devenir dépendant de la structure de données qui nous arrive et non plus d'un ensemble de fonctions (ie. méthodes) que nous pouvons exécuter. D'ailleurs nous sommes tellement dépendant des attributs que même s'ils sont privés, nous allons alors ajouter des getters/setters pour y accéder quand même.
Remonter à l'interface c'est casser ce lien explicite avec la structure de données embarquée dans une classe et alors un changement de structure ou de structure de la structure n'impactera pas le code utilisateur.
Mais alors quel est le problème avec la programmation fonctionnelle "pure" ?
C'est justement qu'elle pousse tous les morceaux de code à dépendre des mêmes structures. Changez la structure à un endroit et vous êtes partis pour changer toutes les fonctions qui s'appuient sur cette structure. Cela engendre fondamentalement un maillage global de tous les pans de code d'une application avec un couplage fort autour de cette ou de ces structures.
La seule condition pour y remédier c'est d'avoir "la bonne structure du premier coup"... lol quoi... Comment prévoir quelles données (et de quels types) nous arriveront demain ?
À l'inverse, retourner des implémentations d'interface autoboxé dans le type de cette interface (c-à-d. les fameux "messages" de la POO) garanti non seulement que les changements de structures n'auront pas d'impact, mais que les changements d'algorithmes non plus (ici "pas d'impact" est à prendre au sens où le contrat d'interfaçage n'est pas rompu et donc que le code continu de compiler).
Pourquoi est-ce que je vous parle de tout ça ?
À cause Rust et de mes quatre semaines d'immersion intense.
Soyons clair, je trouve que les concepts derrière le langage sont incroyables et ses performances merveilleuses (en tant que dev Java j'ai toujours détesté la JVM rien que pour ce sujet). Par contre le fait que Rust se soit tourné exclusivement vers le fonctionnel et la programmation en "structure-first" à la place de celle en "contract-first" car la majorité des devs ne parviennent pas à penser en objets avec l'encapsulation, cela fait de Rust un langage aussi immaintenable que C mais un peu plus fiable grâce à son meilleur compilateur.
L'API de Rust est digne de celle du C. Par exemple, prenons la méthodes HashMap::keys()
de la stdlib de Rust. Celle-ci aurait pu retourner l'implémentation d'un trait Iterator
mais non, elle retourne une structure Key
qui contient une structureIter
qui contient une autre structure base::Iter
.
Changez un morceau de la chaîne et préparez-vous à gérer les impacts partout.
En résumé, et après être passée dans l'ordre par Java, OCaml, PHP, C, ASM, Bash, CSH, Python, JavaScript (ES5 à ES7), Anubis, Haskell, Ruby, Groovy, TypeScript, Scala, Go, Kotlin et enfin Rust (ndr. je bidouillais en Rust depuis quelques années), je peux vous assurer que :
- Rust est techniquement un super langage avec l'un des meilleurs compilateur du marché.
- Rust a une API aussi pourrie que celle de C, encourageant le couplage et augmentant l'immaintenabilité. Je crois qu'il doit exister un moyen d'outre-passer cela, mais je ne sais pas encore comment faire et ça me fruste pas mal.
Enfin, je sais que certains dev vont être fâchés de lire ce que j'écris alors permettez-moi de vous proposer un test car j'ai le sentiment que si c'est le cas, c'est que vous n'avez jamais pensé en OOP - et donc que vous ne pouvez pas encore comprendre ce que je dis. Il s'agit d'un exercice que @Kysofer a imaginé pour ses entretiens d'embauche afin de savoir si un candidat "expert Java" savait penser et programmer en orienté objet.
Prenez ces deux classes :
class Person {
private final String name;
private final String firstName;
private final int age;
public Person(String name, String firstName, int age) {
this.name = name;
this.firstName = firstName;
this.age = age;
}
}
class Car {
private final String brand;
private final String name;
public Car(String brand, String name) {
this.brand = brand;
this.name = name;
}
}
Objectifs :
- Sans violer l'encapsulation, c'est-à-dire sans jamais accéder aux attributs des deux classes depuis l'extérieur de ces deux classes.
- Sans ajouter des getter ou des setter.
- Sans mettre les attributs en public, package ou protected.
- Sans implémenter les algorithmes de conversion à l'intérieur des deux classes elles-mêmes.
=> Écrivez une architecture qui soit capable de convertir en JSON ou en XML ces deux objets.
Indice : Quand on pense en objet, c'est évident, très facile même. Quand on ne pense qu'en procédurale ou son évolution en fonctionnel, cela paraît impossible.
Et dans tout ça je ne compte pas arrêter Rust pour autant mais je fais appel à mes amis pour qu'ils m'aident à trouver une façon "clean" de coder dans ce langage.
@Sweet le 9 c'est le plus compliqué quand on vient du procédurale et qu'on essaie de faire de l'OOP (note : l'API Java est procédurale à 99%).
Je l'ai compris grâce à des formations que des copains d'ITAMETIS m'ont offertes (merci @Kysofer pour tout le temps que tu as passé à m'appendre). En substance, dès qu'une classe affiche un getter ou un setter, alors son développeur a violé le principe d'encapsulation, ce qui est "anti-objet" par nature.
Dit autrement, nous mettons des get/set pour avoir bonne conscience mais nous pourrions les remplacer par des attributs public cela reviendrait exactement au même. En ajoutant des getter/setter nous exposons deux choses en les sortant de l'encapsulation :
- La valeur de la donnée
- Le type réel de la donnée (c'est de loin le pire)
Et ce type d'objets s'appelle : une structure de données. Les méthodes des autres objets appelant les get/set n'étant que des procédures décidant quoi mettre et quoi retirer de ces objets structures de données.
Alors cela m'a pris un bon moment avant de comprendre comment coder sans setter (pour l'immutabilité de la programmation fonctionnelle) et sans getter (pour respecter l'encapsulation de la programmation orientée objets) ; et bien sûr sans jamais remplacer l'un ou l'autre par des attributs publics. C'est ce que j'entends lors que j'écris "programmer en interface-first".
Le meilleur bouquin que je puisse te recommander à ce sujet est Elegant Objects de Yegor Bugayenko. C'est un solid 5/7 au niveau de la "disruptivité". Mais si tu fais du Java par exemple, alors tu devrais comprendre pourquoi ceux qui font "du vrai" OOP (pardon pour l'expression mais c'est à prendre au sens "ne violant jamais l'encapsulation") disent que Spring n'est pas orienté objet pour un sou et que le framework pousse à de la programmation procédurale comme en C ou VB.
Après c'est mon côté coach crafts qui incite toujours à mieux comprendre un paradigme de programmation et d'expérience maintenant, la grande majorité des développeurs ne creusent pas plus loin que les 75% d'un concept (ça descend en dessous des 20-25% pour l'objet et le fonctionnel car les "frameworks s'en occupent pour nous") et puis il faut aussi admettre que le procédurale est bien plus accessible quand on veut produire vite, même s'il est nettement moins maintenable.
J'avais entendu le nom il y a quelques années mais n'ayant pas plus creusé le concept je l'avais oublié. Le hasard du destin me fait retomber dessus aujourd'hui. Bref il s'agit de 9 règles de programmation à appliquer.
En résumé :
- N'utiliser qu'un seul niveau d'indentation
- Ne pas utiliser le mot-clef ELSE
- Encapsuler toutes les primitives et les String dans des objets
- Encapsuler les collections dans des objets
- N'enchaîner qu'un seul point (ou flèche) par ligne
- Ne pas utiliser d’abréviations
- Garder des classes petites (moins de 50 lignes)
- Aucune classe ne doit avoir plus de deux instances en guise de variables
- Ne jamais utiliser de Getter/Setter
Globalement, je le fais sans m'en rendre compte, mes classes sont minuscules (20 lignes en moyennes). Éventuellement le ELSE m'est encore utile mais c'est vrai que programmant en fail-first je lui préfère un throw direct et donc qu'il n'apparaît vraiment pas souvent.
Une compilation des bases de Rust.
Via Sebsauvage.
J'avoue qu'en terme d’obfuscation du code ça se pose là !
Pour @Going
Whaouuu. @Lenny qui poste des liens de ouf dans des tickets mais qui ne les reposte pas sur le cozo ! Bref un très bon article arguant sur les getter et setter en Java.
L'article est d'Allen Holub à ranger à côté de ceux de Yegor Bougayenko.
Bon ça fait deux années maintenant et je sais où j'en suis niveau langage de programmation : de toutes les syntaxes, ma préférée est sans aucun doute et de trèèès loin celle de Kotlin (sauf pour les get / set).
Par contre, le meilleur compilateur du marché est celui de Rust, il n'y a pas photo. J'ai vraiment hâte que Kotlin Native décolle 😉 !
Via Riduidel.
Spoiler de l'article : Kotlin gagne quasiment partout.
Par contre Kotlin n'est pas que pour Android mais aussi pour tout ce qui cible la JVM ou la compilation du byte-code de JVM en natif. Chez nous il est côté serveur depuis plus de deux ans maintenant et a TOTALEMENT REMPLACÉ JAVA !
Rappel, une façon de créer des modules en C :
/* interface.h */
struct library {
const int some_value;
void (*method1)(void);
void (*method2)(int);
/* ... */
};
extern const struct library Library;
/* interface.h */
/* interface.c */
#include "interface.h"
void method1(void)
{
...
}
void method2(int arg)
{
...
}
const struct library Library = {
.method1 = method1,
.method2 = method2,
.some_value = 36
};
/* end interface.c */
/* client code */
#include "interface.h"
int main(void)
{
Library.method1();
Library.method2(5);
printf("%d\n", Library.some_value);
return 0;
}
/* end */
Je me sens tellement proche de ce qu'écrit le monsieur. C'est vrai que chaque langage à ses défauts et en général je ne remets pas en question le dernier langage à la mode autant que je le fais pour les frameworks !!! Car à cause d'eux, nous nous retrouvons avec ces IT qui ont oublié comment coder ne serait-ce qu'un Hello World!.
Par contre on sent que le monsieur a été CEO lorsqu'il écrit ce genre de saletés :
Est-ce que la solution est politique ? Il faut légiférer pour mieux gouverner le monde du logiciel. On le voit avec les dernières réponses législatives aux problèmes concrets : RGPD, notification des cookies… la source du problème n’est pas résolue. Peut-être parce que les politiques ne comprennent que très mal le monde du logiciel.
La dernière chose dont nous avons besoin c'est de personnalités politiques dans le logiciel.
Dans la liste, je cite :
Perl, Haskell, Ruby, Objective-C et finalement R
J'ajouterai Java d'ici à 20 ans et Scala d'ici à 15, les deux remplacés par Kotlin en grande partie et Clojure/Elixir dans une autre mesure ; et enfin PHP d'ici à 15 ans remplacé par Python et TypeScript. Voici un graphique tiré de l'article :
Question... À quand JavaScript ?
Le vocabulaire indispensable pour comprendre les StackOverflow récentes. Aujourd'hui j'ai appris ce qu'est le Bicrement => +2 (à la place du +1 issu du i++).