Le besoin d’un Time-To-Market de plus en plus court nécessite d’intégrer de plus en plus de bibliothèques tierces. Il n’y a plus de temps pour le syndrome des NIH s’il l’a jamais été. Alors que la plupart du temps, l’API de la bibliothèque est prête à l’emploi, il arrive qu’il faille parfois « l’adapter » à la base de code. La facilité d’adaptation dépend beaucoup de la langue.
Par exemple, dans la JVM, il existe quelques bibliothèques de programmation réactive : RxJava, Project Reactor, Mutiny et coroutines. Vous pourriez avoir besoin d’une bibliothèque qui utilise les types d’une bibliothèque, mais vous avez basé votre projet sur une autre.
Dans cet article, j’aimerais décrire comment ajouter un nouveau comportement à un objet/type existant. Je n’utiliserai aucun type réactif pour le rendre plus général, mais ajouter toTitleCase()
à String
. Lorsqu’il existe, l’héritage est ne pas une solution car elle crée un nouveau type.
Je m’excuse à l’avance que les implémentations ci-dessous soient assez simples : elles sont destinées à mettre en évidence mon point, pas à gérer les cas de coin, par exemple, chaînes vides, non UTF 8, etc.
JavaScript
JavaScript est un langage interprété dynamiquement et faiblement typé, qui gère le World Wide Web – jusqu’à ce que WASM prenne le relais ? Pour autant que je sache, sa conception est unique, car elle est basée sur un prototype. Un prototype est un moule pour de nouvelles « instances » de ce type.
Vous pouvez facilement ajouter des propriétés, que ce soit un état ou un comportement, à un prototype.
Object.defineProperty(String.prototype, "toTitleCase", {
value: function toTitleCase() {
return this.replace(/wS*/g, function(word) {
return word.charAt(0).toUpperCase() + word.substr(1).toLowerCase();
});
}
});
console.debug("OncE upOn a tImE in thE WEst".toTitleCase());
Notez que les objets créés à partir de ce prototype après l’appel à defineProperty
offrira la nouvelle propriété; objets créés avant habitude.
Rubis
Ruby est un langage interprété dynamiquement et fortement typé. Bien qu’il ne soit pas aussi populaire qu’auparavant avec le framework Ruby On Rails, je l’utilise toujours avec le système Jekyll qui alimente ce blog.
L’ajout de méthodes ou d’attributs à une classe existante est assez standard dans l’écosystème Ruby. J’ai trouvé deux mécanismes pour ajouter une méthode à un type existant dans Ruby :
-
Utilisez class_eval :
« Évalue la chaîne ou le bloc dans le contexte de mod, sauf que lorsqu’un bloc est donné, la recherche de variable constante/classe n’est pas affectée. Cela peut être utilisé pour ajouter des méthodes à une classe »
-
Implémentez simplement la méthode sur la classe existante.
Voici le code de la deuxième approche :
class String
def to_camel_case()
return self.gsub(/wS*/) {|word| word.capitalize()}
end
end
puts "OncE upOn a tImE in thE WEst".to_camel_case()
Python
Python est un langage interprété dynamiquement et fortement typé. Je suppose que tous les développeurs ont entendu parler de Python de nos jours.
Python vous permet d’ajouter des fonctions aux types existants – avec des limitations. Essayons avec le str
type intégré :
import re
def to_title_case(string):
return re.sub(
r'wS*',
lambda word: word.group(0).capitalize(),
string)
setattr(str, 'to_title_case', to_title_case)
print("OncE upOn a tImE in thE WEst".to_title_case())
Malheureusement, le code ci-dessus échoue lors de l’exécution :
Traceback (most recent call last):
File "<string>", line 9, in <module>
TypeError: can't set attributes of built-in/extension type 'str'
Parce que str
est un intégré type, nous ne pouvons pas ajouter de comportement dynamiquement. Nous pouvons mettre à jour le code pour faire face à cette limitation :
import re
def to_title_case(string):
return re.sub(
r'wS*',
lambda word: word.group(0).capitalize(),
string)
class String(str):
pass
setattr(String, 'to_title_case', to_title_case)
print(String("OncE upOn a tImE in thE WEst").to_title_case())
Il devient désormais possible d’étendre String
, car c’est une classe que nous avons créée. Bien sûr, cela va à l’encontre de l’objectif initial : nous devions prolonger str
en premier lieu. Par conséquent, il fonctionne avec des bibliothèques tierces.
Avec les langages interprétés, il est relativement facile d’ajouter un comportement aux types. Pourtant, Python touche déjà les limites car les types intégrés sont implémentés en C.
Java
Java est un langage compilé statiquement et fortement typé qui s’exécute sur la JVM. Sa nature statique rend impossible l’ajout de comportement à un type.
La solution de contournement consiste à utiliser static
méthodes. Si vous êtes un développeur Java depuis longtemps, je pense que vous avez probablement vu StringUtils
et DateUtils
cours au début de votre carrière. Ces classes ressemblent à quelque chose comme ça :
public class StringUtils {
public static String toCamelCase(String string) {
// The implementation is not relevant
}
// Other string transformations here
}
J’espère qu’à l’heure actuelle, l’utilisation d’Apache Commons et de Guava a remplacé toutes ces classes :
System.out.println(WordUtils.capitalize("OncE upOn a tImE in thE WEst"));
Dans les deux cas, l’utilisation de méthodes statiques empêche une utilisation fluide de l’API et altère ainsi l’expérience du développeur. Mais d’autres langages JVM offrent des alternatives intéressantes.
Scala
Comme Java, Scala est un langage compilé, statiquement et fortement typé qui s’exécute sur la JVM. Il a été initialement conçu pour faire le pont entre la programmation orientée objet et la programmation fonctionnelle. Scala fournit de nombreuses fonctionnalités puissantes. Parmi eux, implicite les classes permettent d’ajouter un comportement et un état à une classe existante. Voici comment ajouter le toCamelCase()
fonction de String
:
import Utils.StringExtensions
object Utils {
implicit class StringExtensions(thiz: String) {
def toCamelCase() = "\w\S*".r.replaceAllIn(
thiz,
{ it => it.group(0).toLowerCase().capitalize }
)
}
}
println("OncE upOn a tImE in thE WEst".toCamelCase())
Bien que j’aie un peu touché à Scala, je n’ai jamais été fan. En tant que développeur, j’ai toujours affirmé qu’une grande partie de mon travail consistait à implicite conditions explicite. Ainsi, j’ai désapprouvé l’utilisation intentionnelle du implicit
mot-clé. Chose intéressante, il semble que je n’étais pas seul. Scala 3 conserve la même capacité en utilisant une syntaxe plus appropriée :
extension(thiz: String)
def toCamelCase() = "\w\S*".r.replaceAllIn(
thiz,
{ it => it.group(0).toLowerCase().capitalize }
)
Notez que le bytecode est un peu similaire à Java statique approche méthodique dans les deux cas. Pourtant, l’utilisation de l’API est fluide, car vous pouvez enchaîner les appels de méthode les uns après les autres.
Kotlin
Comme Java et Scala, Kotlin est un langage compilé, statiquement et fortement typé qui s’exécute sur la JVM. Plusieurs autres langages, dont Scala, ont inspiré sa conception.
Mon opinion est que Scala est plus puissant que Kotlin, mais le compromis est une charge cognitive supplémentaire. A l’inverse, Kotlin a une approche légère, plus pragmatique. Voici la version Kotlin :
fun String.toCamelCase() = "\w\S*"
.toRegex()
.replace(this) {
it.groups[0]
?.value
?.lowercase()
?.replaceFirstChar { char -> char.titlecase(Locale.getDefault()) }
?: this
}
println("OncE upOn a tImE in thE WEst".toCamelCase())
Si vous vous demandez pourquoi le code Kotlin est plus détaillé que celui de Scala malgré ma réclamation précédente, voici deux raisons :
- Je ne connais pas assez Scala, donc je n’ai pas géré les corner cases (capture vide, etc.), mais Kotlin ne vous laisse pas le choix
- L’équipe de Kotlin a supprimé le
capitalize()
fonction de lastdlib
dans Kotlin 1.5
Rouiller
Last but not least dans notre liste, Rust est un langage compilé, statiquement et fortement typé. Il a été initialement conçu pour produire des binaires natifs. Pourtant, avec la configuration appropriée, il permet également de générer Était M. Au cas où vous seriez intéressé, j’ai pris le lien :/focus/start-rust/[a couple of notes] tout en apprenant la langue.
Il est intéressant de noter que, bien que typé statiquement, Rust permet également d’étendre des API tierces, comme le montre le code suivant :
trait StringExt { // 1
fn to_camel_case(&self) -> String;
}
impl StringExt for str { // 2
fn to_camel_case(&self) -> String {
let re = Regex::new("\w\S*").unwrap();
re.captures_iter(self)
.map(|capture| {
let word = capture.get(0).unwrap().as_str();
let first = &word[0..1].to_uppercase();
let rest = &word[1..].to_lowercase();
first.to_owned() + rest
})
.collect::<Vec<String>>()
.join(" ")
}
}
println!("{}", "OncE upOn a tImE in thE WEst".to_camel_case());
- Créez l’abstraction pour contenir la référence de fonction. C’est connu comme un trait en paix.
- Implémenter le trait pour une structure existante.
L’implémentation des traits a une limitation : notre code doit déclarer au moins l’un des traits ou de la structure. Vous ne pouvez pas implémenter un trait existant pour une structure existante.
Conclusion
Avant d’écrire cet article, je pensais que les langages interprétés permettraient d’étendre les API externes, contrairement aux langages compilés – à l’exception de Kotlin. Après avoir rassemblé le matériel, ma compréhension a radicalement changé.
J’ai réalisé que tous les langages courants offrent une telle fonctionnalité. Bien que je n’aie pas inclus de section C#, c’est également le cas. Ma conclusion est triste, car Java est le seul langage qui n’offre rien à cet égard.
J’ai régulièrement déclaré que l’avantage le plus important de Kotlin par rapport à Java réside dans les propriétés/méthodes d’extension. Bien que l’équipe Java continue d’ajouter des fonctionnalités au langage, il n’offre toujours pas une expérience de développeur proche de l’un des langages ci-dessus. Comme j’utilise Java depuis deux décennies, je trouve cette conclusion un peu triste, mais c’est comme ça, malheureusement.
Pour aller plus loin:
Publié à l’origine dans A Java Geek le 7 novembree, 2021