Préface▲
Dart étant encore jeune, il est difficile de dire s'il participera à la nouvelle révolution du Web. Cependant, nous pouvons applaudir la démarche de Google nous offrant une solution aux différentes problématiques actuelles du développement Web, notamment pour ceux à qui JavaScript donne des boutons, mais pas uniquement.
Dart s'inscrit dans une mouvance de nouveaux paradigmes Web tels que TypeScript, CoffeeScript ou encore Node.js. qui ont tous pour objectif de simplifier les développements.
Il est important de noter qu'avec Dart, Google n'a pas l'intention de casser le Web puisque l'un des prérequis de Dart est d'être compilable en JavaScript (grâce à l'outil dart2js), pour être utilisé dans tout navigateur moderne (IE 6, 7, 8 et 9 étant donc exclus) ayant une Machine Virtuelle JavaScript compatible HTML5, mais pas de Machine Virtuelle Dart.
Dart faisant partie du projet Chrome, nous pouvons espérer voir apparaître une Machine Virtuelle Dart dans Chrome une fois la version 1.0 sortie.
Ce tutoriel a été réalisé avec la participation de Soat.
À propos de l'auteur▲
Développeur depuis l'âge de 14 ans (Assembleur, C/C++, PHP, Python, Java), Yohan Beschi ne fait quasiment que du Java depuis 2002.
Il s'est récemment penché sur le développement Dart, qu'il évangélise depuis. Il participe à la team d'expertise Soat dans le cadre de ses expériences dans le domaine du Web et Java. À ce titre il contribue à l'émulation technologique sur developpez.com, le blog soat et sur twitter (@yohanbeschi).
1. Introduction▲
1-1. Le langage▲
Dart en quelques mots :
- Dart est un langage orienté objet avec un héritage simple, des classes concrètes et abstraites, des interfaces et des mixins (sorte d'héritage multiple limité) ;
- Dart est optionnellement typé que ce soit dans l'assignation d'une variable ou la définition d'une fonction ;
- Dart est gouverné par des fonctions de haut niveau, c'est-à -dire des fonctions non encapsulées dans une classe ou un objet. Des fonctions encapsulées dans une classe ou un objet sont aussi appelées méthodes ;
- en Dart, les fonctions sont des objets, ce qui nous permet de les passer en paramètre de constructeurs ou de fonctions, mais aussi d'avoir des fonctions retournées par une autre fonction ;
- toute application a au moins une fonction de haut niveau, la fonction main()Â ;
- Dart permet d'utiliser des Generics.
Dart est par défaut monoprocessus à l'instar de JavaScript. Néanmoins, il a un modèle de concurrence appelé isolates, permettant une exécution parallèle avec un système de passage de messages. Lorsque compilés en JavaScript, ces isolates deviennent des Web Workers.
Différences majeures avec JavaScript :
- Dart n'utilise pas de prototypes ;
- Dart ne permet pas de créer et d'exécuter du code à la volée. Il n'y a ni de fonction eval(), ni de possibilité de monkey patching (modification d'un programme en cours d'exécution). Seuls les isolates permettent d'exécuter du code dynamiquement dans ce que j'appellerai une architecture orientée plugins.
1-2. L'écoystème▲
Dart n'est pas seulement un langage, c'est une plateforme de développement qui comprend :
- un SDK avec une VM serveur ;
- un IDE (DartEditor), mais aussi des plugins pour Eclipse et IntelliJÂ ;
- une VM cliente (Dartium)Â ;
- un outil de gestion des dépendances (Pub Package Manager) ;
- un compilateur Dart vers JavaScript (dart2js)Â ;
- un générateur de documentation à partir du code (dartdoc) :
1-2-1. Dart Editor▲
Dart Editor est basé sur Eclipse Rich Client Platform (RCP), un framework permettant de créer facilement des IDE. Par conséquent, son aspect est très similaire à Eclipse. En revanche, ne vous attendez pas aux mêmes fonctionnalités qui se limitent à une autocomplétion (sommaire), une analyse de code statique montrant erreurs et warnings, et une navigation facilitée entre les fichiers, classes, méthodes, etc.
Il est bien sûr possible d'exécuter le code avec ou sans débogage, ou bien de le convertir en JavaScript.
Pour ceux qui souhaitent utiliser leur éditeur favori, il existe des plugins pour Eclipse, les IDE de JetBrains, Sublime Text 2 ou Vim. Et bien sûr pour les personnes Old School, un simple éditeur de texte répond à la majorité des besoins et situations.
1-2-2. Les Machines Virtuelles▲
Les Machines Virtuelles (VM) Dart sont le cœur du langage. Il en possède deux pour des usages différents. La première (VM Serveur) permet d'exécuter une application côté serveur en ligne de commande dans une console. La seconde (VM Client) est embarquée dans un navigateur tel que Dartium, comme nous le verrons dans le point suivant, au même titre que la Machine Virtuelle JavaScript. Ceci permet d'avoir la partie cliente et la partie serveur d'une application codée dans le même langage.
L'unique différence entre ces deux versions est leurs bibliothèques. La VM Serveur contient la bibliothèque dart:io, qui permet d'effectuer les opérations d'entrée/sortie habituelles (manipulation de fichiers, sockets, etc.). La VM Cliente contient la bibliothèque dart:html, permettant de manipuler le DOM (tags, events, etc.).
Cette distinction est nécessaire pour des raisons de sécurité évidentes concernant la bibliothèque dart:io. En revanche, bien que compréhensible, l'absence de la bibliothèque dart:html de la VM Serveur rend compliqué, l'exécution en ligne de commande de tests unitaires validant du code utilisant cette bibliothèque.
Pour exécuter des tests unitaires de classes utilisant la bibliothèque dart:html, il faut utiliser Dartium, ce qui est déconcertant, voire un problème pour l'industrialisation.
1-2-3. Dartium▲
Dartium est une version modifiée de Chomium (la version Open Source de Chrome), avec une Machine Virtuelle Dart embarquée.
Il reconnaît la balise <script type="application/dart" …> et exécute le code Dart dans le navigateur sans avoir à le convertir en JavaScript.
Le fait d'avoir une Machine Virtuelle cliente est le vrai point fort de Dart. Aujourd'hui, hormis le JavaScript tous les autres langages ont besoin d'être compilés avant d'être exécutés dans un navigateur. Et même si seulement un nombre infime de navigateurs auront une Machine Virtuelle Dart, c'est une avancée pour la phase développement, permettant une productivité accrue, en considérant qu'une fois implémentée dans tous les navigateurs, le HTML5 permettra un comportement standard.
1-2-4. dart2js▲
dart2js permet de compiler du Dart en JavaScript (compatible HTML5 uniquement), depuis Dart Editor ou en ligne de commande.
dart2js compile toutes les sources de votre application, ainsi que les biblothèques utilisées, en un seul fichier JavaScript.
Par défaut, le code n'est pas minifié. Pour ce faire, il suffit d'ajouter l'option --minify.
À noter qu'avant compilation, après construction de l'arbre syntaxique (AST) du code, toutes les branches non utilisées (code mort ou fonctionnalités d'une bibliothèque non utilisées dans notre application) sont élaguées ce qui permet de réduire considérablement la taille du code généré. Ce mécanisme est connu sous le nom Tree Shaking.
1-2-5. Pub▲
La gestion de dépendances (bibliothèques) est une fonctionnalité indispensable pour tout langage, Java a Maven, .NET a NuGet, Node.js a npm, etc. Dart a donc appelé le sien pub. Dans un fichier pubspec.yaml, sont indiquées différentes informations sur l'application, mais aussi les bibliothèques utilisées, ainsi que leur version (toutes les versions, seulement une ou un intervalle).
name
:
pacifista_rocks
description
:
The best application in the whole world
version
:
0
.0
.1
dependencies
:
great_lib
:
any
À l'aide de pub, il est aussi possible de publier son code sur GitHub, BitBucket ou pub.dartlang.org.
Nous regretterons que Pub soit basé sur une architecture monolithique ne permettant pas aux utilisateurs d'ajouter des plugins comme pour Maven, mais aussi que la refonte - interminable - de l'API de Dart soit un frein à son évolution.
1-2-6. dartdoc▲
dartdoc permet de générer une documentation au format HTML à partir des commentaires présents dans le code.
Un commentaire de type documentation est identifié par un triple slash (///) ou slash étoile-étoile + étoile slash (/** */).
/// This is a Dart doc
/**
* And here another Dart doc
*/
void
main() {
print("Dart"
);
}
La documentation générée aura la même forme que celle de la documentation de l'API Dart.
Premières applications▲
Dans un premier temps, je vous conseille d'utiliser le package Dart Editor, contenant tout ce dont nous avons besoin pour les applications présentées dans ce manuel. Avec un peu plus de pratique, vous pourrez n'utiliser que la SDK et Dartium, voire utiliser les plugins Dart pour Eclipse, les IDE de JetBrains, Sublime Text 2 ou Vim.
Notes
- Dart Editor est disponible pour Windows (Vista, 7 et 8), Mac et Linux.
- Pour utiliser Dart Editor il est nécessaire d'avoir une JRE installée.
- Le répertoire de Dart Editor est créé dans le répertoire suivant : <user_home>/DartEditor. Si vous ne souhaitez pas qu'il soit créé à cet endroit. Il est nécessaire de modifier la ligne suivante @user.home\DartEditor du fichier DartEditor.ini.
Au premier lancement de Dart Editor vous devriez voir quelque chose ressemblant à l'écran suivant :
- Dart est un langage Open Source, Orienté Objet et optionnellement typé.
- Il est par défaut monoprocessus à l'instar de JavaScript.
- Il a un modèle de concurrence appelé isolates permettant une exécution parallèle.
Comme déjà évoqué, il est possible de créer deux types d'applications Dart : des applications en ligne de commande (serveur) et des applications Web (cliente).
2-1. Création d'une application serveur▲
Pour créer une application : File -> New Application ou dans la fenêtre Files, clic droit -> New Application.
Application Name : nom de l'application (doit commencer par une lettre ASCII, les caractères underscore « _ » ou dollar « $ », puis des lettres ASCII, des chiffres ou les caractères underscore « _ » ou dollar « $ ». Par exemple, helloworld, $hello_world1, _hello$world2, etc.).
Parent Directory : répertoire contenant la nouvelle application
Generate sample content : génère une application de démonstration.
Pour notre première application en ligne de commande, sélectionnez Command-line application, puis cliquez sur Finish. La nouvelle application apparaît dans la fenêtre Files avec un fichier portant le même nom et se terminant par .dart.
Un fichier pubspec.yaml est aussi créé. Il contient uniquement le nom de l'application et une description.
Vous pouvez aussi constater que des répertoires packages sont générés. Vous ne devez surtout pas les modifier puisqu'ils sont gérés par pub pour contenir des liens symboliques vers les bibliothèques utilisées par votre application.
Le code généré est extrêmement simple :
void
main() {
print("Hello, World!"
);
}
Au minimum, une application Dart a un fichier .dart avec une fonction main(), qui est généralement présente dans le fichier portant le nom de l'application, mais ce n'est pas une obligation.
Comme de nombreux langages, la fonction main() est le point d'entrée d'une application Dart. Elle ne retourne rien (void) et ne prend aucun paramètre.
La fonction print() affiche du texte dans la console. Elle appartient à la bibliothèque dart:core importée implicitement par la Dart VM.
Pour lancer l'application, sélectionnez le fichier .dart et faites Ctrl-R, ou clic droit sur le fichier et Run.
La Machine Virtuelle Dart exécute le code Dart sans compilation intermédiaire contrairement à d'autres langages (Why Not a Bytecode VM?).
Dans la console, nous voyons le message issu de la fonction print().
Hello, World!
Bien que la fonction main() ne prenne pas de paramètre, il est possible d'en fournir au démarrage de l'application.
Avec Dart Editor, nous pouvons passer des paramètres à notre application de la manière suivante :
- choisissez Manage Launches…
- puis dans Script arguments, ajoutez les paramètres que vous souhaitez fournir à votre application :
L'option Run in checked mode permet de contrôler le type des variables et d'activer les assertions. Vous pouvez la désactiver dans cet écran ; - pour finir, cliquez sur Run. Néanmoins l'application ne faisant rien de nos nouveaux paramètres, l'affichage reste le même que précédemment.
Il nous faut à présent récupérer les paramètres. Pour ce faire, nous avons à notre disposition la classe dart:core#Options :
2.
3.
4.
5.
6.
7.
8.
import
"dart:io"
;
void
main() {
Options options = new
Options();
List
<String
> args = options.arguments;
String
firstArg = args[0
];
print("Hello,
$firstArg
${args[
0
]}
${options.arguments[
1
]}
!"
);
}
La ligne 7 montre comment insérer des variables dans une chaîne de caractères en utilisant le symbole dollar ($), et des accolades pour les collections et membres d'objets (de type collection ou non).
2-2. Création d'une application cliente▲
La raison première d'exister de Dart est de pouvoir créer facilement des applications Web à page unique (Single-Page Web Apps). Cela signifie qu'une seule page HTML sera chargée dans le navigateur, avec plusieurs scripts Dart ou un seul script JavaScript formant notre application. La partie serveur devenant principalement un fournisseur de données.
Pour créer une application cliente (Web), il suffit de procéder comme précédemment et de sélectionner Web application (sans web_ui). Quatre fichiers portant le nom de l'application sont créés : un fichier .css, un fichier .html, un fichier .dart et un fichier pubspec.yaml. Pour l'exemple, le nom de l'application est helloweb.
Le fichier .css étant une feuille de style standard, je ne m'attarderai pas dessus. Tout comme le fichier pubspec.yaml qui contrairement à l'exemple précédent définit une dépendance sur la bibliothèque browser. Concernant le fichier .html, seules les deux lignes de fin du body méritent que l'on s'y intéresse.
2-2-1. Fichier *.html▲
Le fichier *.html est le point d'entrée de notre application Web.
<!
DOCTYPE html
>
<html>
<head>
<meta charset
=
"utf-8"
>
<title>Helloweb</title>
<link rel
=
"stylesheet"
href
=
"helloweb.css"
>
</head>
<body>
<h1>Helloweb</h1>
<p>Hello world from Dart!</p>
<div id
=
"sample_container_id"
>
<p id
=
"sample_text_id"
></p>
</div>
<script type
=
"application/dart"
src
=
"helloweb.dart"
></script>
<script src
=
"packages/browser/dart.js"
></script>
</body>
</html>
La ligne suivante indique à la VM Dart dans quel fichier se trouve la fonction main() (j'ai appelé mon application « helloweb ») :
<script type
=
"application/dart"
src
=
"helloweb.dart"
></script>
Nous avons ensuite un script JavaScript (inclus dans la bibliothèque browser et téléchargé automatiquement par pub) :
<script src
=
"packages/browser/dart.js"
></script>
Dans le cas où le navigateur utilisé n'a pas de VM Dart, le script dart.js remplace la première ligne par :
<script src
=
"helloweb.dart.js"
></script>
2-2-2. Fichier *.dart▲
Le fichier .dart est, quant à lui, beaucoup plus intéressant puisqu'il contient le code Dart.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
import
'dart:html'
;
void
main() {
query("#sample_text_id"
)
..text = "Click me!"
..onClick.listen(reverseText);
}
void
reverseText(MouseEvent event) {
var
text = query("#sample_text_id"
).text;
var
buffer = new
StringBuffer
();
for
(int
i = text.length - 1
; i >= 0
; i--) {
buffer.write(text[i]);
}
query("#sample_text_id"
).text = buffer.toString();
}
#1 : import de la bibliothèque dart:html permettant de manipuler le DOM.
#4 : la fonction query() appartient à la bibliothèque dart:html. Elle s'appuie sur les sélecteurs CSS tout comme le fait la fonction $() de jQuery, ce qui permet dans le cas présent de récupérer l'élément portant l'id « text ».
#5/6 : la notation « .. » est un sucre syntaxique permettant de simplifier l'écriture.
Les lignes 4 à 6 auraient pu être écrites de la manière suivante :
var
element = query("#text"
); // ou Element element = query("#text");
element.text = "Click me!"
;
element.onClick.listen(reverseText);
#6 : ajoute un listener à l'événement DOM « onClick », sous la forme d'un callback (fonction exécutée au fire de l'événement).
Quant à la fonction reverseText(), elle est sans surprise. Elle récupère le texte de l'élément ayant l'id « text », inverse toutes les lettres et remplace le mot de l'élément « text ».
Pour résumer, un clic sur le texte inverse les lettres et affiche le nouveau texte.
3. Le langage▲
Dans cette partie nous allons étudier le langage Dart dans ses moindres détails. Néanmoins, il est conseillé que vous soyez déjà familiarisé avec au moins un langage orienté objet puisque nous ne rentrerons pas toujours dans les détails, bien que parfois l'utilisation de bonnes pratiques sera exposée.
3-1. Utiliser le code d'exemple▲
Pour utiliser le code présenté dans cette partie, il suffit de le récupérer depuis Github, puis dans la fenêtre Files de Dart Editor, cliquer sur le bouton droit de la souris (attention aucun projet ne doit être sélectionné sous peine de ne pas avoir le bon menu contextuel) et choisir Open Existing Folder…
Le projet est organisé en répertoires correspondant à chacune des sous-parties. Chaque répertoire contient plusieurs fichiers illustrant une particularité du langage où chaque fichier est une application autonome contenant une fonction main().
La mise en place de tests unitaires n'étant pas triviale en Dart, le parti a été pris d'utiliser des assertions. Nous utiliserons donc le mot-clé is permettant d'identifier si une variable est d'un certain type, et effectuerons des assertions avec assert, qui prend un booléen en paramètre et lève une exception si la valeur de ce paramètre n'est pas vraie (true). Pour rappel, les applications doivent être lancées en checked mode pour que les assertions ne soient pas ignorées (cf. 2.1 Création d'une application serveur).
À la racine du projet, le fichier execute_all.dart exécute tous les fichiers dans des isolates. Ceci permet de lancer tous nos pseudotests unitaires en une seule fois, et de vérifier simplement si le langage et l'API n'ont pas changé lors de la sortie de nouvelles versions.
Pour finir, vous noterez que dans la fenêtre Problems de Dart Editor il y a six warnings. Cela est tout à fait normal, vous trouverez les explications dans les sous-parties qui suivent.
Pour limiter la taille des blocs de code, il peut arriver que seulement le code lié au concept expliqué dans la partie en rapport soit présent. Par conséquent, pour avoir le code complet, il est parfois nécessaire de consulter les sources (le chemin est indiqué en en-tête des blocs de code et à chaque fin de partie un lien - nommé par « Source » - pointe vers le fichier en question sur Github).
3-2. Noms de variables, fonctions, classes et bibliothèques▲
Le nom d'une variable, d'une fonction ou d'une classe doit commencer par une lettre ASCII, le caractère underscore « _ » ou dollar « $ », puis des lettres ASCII, des chiffres ou les caractères underscore « _ » ou dollar « $ ».
Par exemple : helloworld, $hello_world1, _hello$world2, etc.
3-3. Variables▲
Utiliser une variable sans type est très simple :
var
name = 'Bob'
;
var
age = 30
;
Par défaut toutes les variables non initialisées ont pour valeur null.
var
uninitialized;
assert
(uninitialized == null
);
3-4. Types intégrés▲
Bien qu'en Dart l'utilisation de types soit optionnelle, ils sont dans certains cas bien utiles comme nous le verrons dans la suite de cet article. Si une variable n'est pas typée, la DVM (Dart Virtual Machine) lui en attribue un.
De plus, utiliser des types facilite le processus développement au sein d'une équipe, et permet l'utilisation d'outils d'analyse permettant de valider le code. Néanmoins, il est conseillé d'être conscient que pour des phases de prototypage ou si le code est bien documenté, l'utilisation de types peut être source de plus de problèmes que de solutions. Il conviendra par conséquent à chaque équipe de développement de faire les choix opportuns en établissant des règles dès le début de vie du projet pour garantir une certaine homogénéité.
3-4-1. Chaînes de caractères▲
En Dart, une chaîne de caractères est une séquence de codes de caractères UTF-16.
3-4-1-1. Définition▲
Les chaînes de caractères peuvent être définies en utilisant des guillemets ou des apostrophes.
// Print strings
print('Hello world'
);
print("Hello world"
);
var
singleQuotes = 'Single quotes.'
;
var
doubleQuotes = "Double quotes."
;
String
singleQuotesWithType = 'Single quotes.'
;
String
doubleQuotesWithType = "Double quotes."
;
3-4-1-2. Échapper un caractère▲
Échapper un caractère s'effectue de la même manière que pour la plupart des langages :
var
escaping = 'It
\'
s easy to escape the string delimiter.'
;
3-4-1-3. Le type String est aussi de type Object▲
Il est important de noter qu'en Dart tout ce qui peut être assigné à une variable est un objet de type Object.
print("Hello world is String:
${
'Hello world'
is
String
}
"
);
print("escaping is String:
${escaping
is
String
}
"
);
print("escaping is Object:
${escaping
is
Object
}
"
);
3-4-1-4. Concaténation▲
Pour concaténer des chaînes de caractères, Dart n'utilise aucun caractère particulier :
print('This '
'is'
'a'
'string'
);
print('This '
'is'
'a'
'multi-line'
'string'
);
Tout comme de nombreux autres langages, il est aussi possible d'utiliser l'opérateur plus (+).
print('This '
+ 'is'
+ 'a'
+ 'string'
+ 'too'
);
3-4-1-5. Interpolation▲
Pour utiliser une expression dans une chaîne de caractères, il suffit d'utiliser ${expression}. Si l'expression est une variable, les accolades sont inutiles.
print('Hello
$singleQuotes
${doubleQuotesWithType.toUpperCase()}
'
);
3-4-1-6. Formater une chaîne de caractères▲
En encadrant une chaîne de caractères de trois apostrophes ou trois guillemets, il est possible de formater une chaîne de caractères sans avoir à utiliser de caractères spéciaux.
print('''With
triple simple quotes
I can define
a string
over multiple
lines
'''
);
print("""And with
triple double
quotes
"""
);
Affiche
With
triple simple quotes
I can define
a string
over multiple
lines
And with
triple double
quotes
3-4-1-7. Texte brut▲
Il peut arriver que nous souhaitions afficher des caractères habituellement interprétés par la Machine Virtuelle, et au lieu d'avoir à les échapper Dart propose une autre syntaxe, en préfixant la chaîne de caractères d'un « r »"
print(r'Hello \n $singleQuotes ${doubleQuotesWithType.toUpperCase()}'
);
affiche :
Hello \n $singleQuotes ${doubleQuotesWithType.toUpperCase()}
3-4-2. Nombres▲
En Dart, il existe trois types permettant de définir des nombres, num, int et double. num étant le super type de int et double.
Les int sont d'une précision arbitraire en termes de taille.
Les double sont de double précision (64 bits, standard IEEE 754)
// num et int
var
num1 = 1
;
var
num2 = 1
.0
;
var
num3 = 0xFF
; // (255hex)
num
num4 = 1
;
num
num5 = 1
.0
;
num
num6 = 0xFF
; // (255hex)
int
num7 = 1
;
int
num8 = 1
.0
;
int
num9 = 0xFF
; // (255hex)
// num et double
var
num10 = 1
.1
;
var
num11 = 5e
-10
;
num
num12 = 1
.1
;
num
num13 = 5e
-10
;
double
num14 = 1
.1
;
double
num15 = 5e
-10
;
3-4-3. Booléens▲
Pour représenter des booléens, Dart a le type bool.
var
boolean1 = true
;
var
boolean2 = false
;
bool
boolean3 = true
;
bool
boolean4 = false
;
En Dart, seulement la valeur true correspond à vrai. Toutes les autres valeurs correspondent à faux.
Il est important de noter, qu'en checked mode, seulement les valeurs true ou false sont considérées comme des booléens, tous les autres provoqueront une exception.
if
(1
) {
print('JavaScript prints this line because it thinks 1 is true.'
);
} else
{
print('Dart in production mode prints this line.'
);
}
Pour activer le production mode : Ctrl+Shift+M, sélectionnez la commande correspondant à votre programme et décochez Run in checked mode.
3-4-4. Listes▲
Dart n'a pas de type tableau, mais un type List pouvant être initialisé avec une taille fixe ou non.
3-4-4-1. Initialisation d'une liste avec des valeurs▲
Une liste peut être initialisée telle une liste JSON :
var
list = [1
, true
, 'String'
, 5
.6e5
];
List
realList = [1
, true
, 'String'
, 5
.6e5
];
3-4-4-2. Instanciation d'une liste vide de taille fixe▲
Pour créer une liste d'une taille fixe dont chacun des éléments est null, il suffit d'indiquer la taille désirée dans le constructeur.
List
fixedList = new
List
(4
);
3-4-4-3. Ajouter des valeurs à une liste de taille fixe▲
Définir des valeurs pour les quatre éléments de cette liste se fera de la manière suivante :
fixedList[0
] = 1
;
fixedList[1
] = true
;
fixedList[2
] = 'String'
;
fixedList[3
] = 5
.6e5
;
Rajouter un élément à cette liste provoquera une exception :
fixedList[4
] = 0
; // Exception
fixedList.add(0
); // lève une exception
Mais attention, une liste préinitialisée n'est pas une liste de taille fixe.
List
list = [null
, null
, null
, null
];
fixedList[4
] = 0
; // lève une exception
fixedList.add(0
); // Valide, ajoute un élément à l'index 4
3-4-4-4. Instanciation d'une liste vide de taille indéterminée▲
Si nous souhaitons, une liste de taille indéterminée à l'avance, il nous suffit de ne pas indiquer de valeur dans le constructeur de List.
List
dynamicList = new
List
();
Pour ajouter des éléments, nous devons utiliser la méthode add().
dynamicList.add(1
);
dynamicList.add(true
);
dynamicList.add('String'
);
dynamicList.add(5
.6e5
);
Si un élément n'a jamais été défini à un certain index, nous ne pouvons pas utiliser la notation de type tableau avec des crochets.
List
dynamicList = new
List
();
//dynamicList[0] = 1; // lève une exception
dynamicList.add(1
); // Valide
dynamicList[0
] = 'String'
; // Remplace '1', par 'String'
Le contenu de la liste n'ayant pas de type, il est possible d'y ajouter des valeurs de n'importe quel type.
3-4-4-5. Generics▲
Si nous souhaitons n'avoir qu'un seul type possible dans notre liste, nous devons utiliser des Generics.
List
<int
> genericsList = new
List
();
genericsList.add(1
);
Néanmoins, rien ne nous empêche d'écrire :
genericsList.add(true
);
genericsList.add('String'
);
genericsList.add(5
.6e5
);
L'analyseur émettra des warnings, mais la VM ignorant les types lors de l'exécution, cela ne posera aucun problème.
Par conséquent, en Dart, les types ne doivent pas être utilisés avec les mêmes intentions qu'un langage fortement typé.
Selon le Dart Style Guide, les types doivent être considérés telle une documentation utile pour les utilisateurs de votre code comme les paramètres d'une fonction. Dans la plupart des cas, les variables locales à une fonction peuvent être déclarées en utilisant le mot-clé var.
3-4-4-6. Liste de Liste▲
Une liste de liste permet de simuler des tableaux à plusieurs dimensions :
List
multiDimensionList = [[10
, 20
, 30
], [40
, 50
, 60
]];
print('
${multiDimensionList[
0
][
1
]}
${multiDimensionList[
1
][
2
]}
'
);
3-4-5. Maps▲
Une Map est une structure de données associant une clé à une valeur.
3-4-5-1. Initialisation d'une map avec des valeurs▲
Une Map peut être initialisée tel un objet JSON, pour lequel les clés doivent être des chaînes de caractères :
var
map = {'key'
: 'value'
, '1'
: 1
};
Map
realMap = {'key'
: 'value'
, '1'
: 1
};
3-4-5-2. Initialisation d'une map vide▲
Heureusement, nous ne sommes pas limités à des clés de type String :
Map
newMap = new
Map
();
newMap[1
] = true
;
newMap['1'
] = false
;
print(newMap);
print('
${newMap[
1
]}
${newMap[
'1'
]}
'
);
3-4-5-3. Generics▲
Tout comme pour les listes, nous pouvons utiliser des Generics, n'ayant aucun impact sur l'exécution :
Map
<String
, int
> genericMap = new
Map
();
genericMap['one'
] = 1
;
genericMap[2
] = '2'
;
print(genericMap);
Nous avons vu les types les plus utilisés, néanmoins Dart a d'autres types. Je vous invite à consulter la documentation officielle pour en savoir plus sur le sujet.
3-5. Opérateurs▲
Dart a de nombreux opérateurs comme vous pouvez le constater dans la liste ci-dessous. De nombreux opérateurs peuvent être surchargés comme nous le verrons dans la partie 3.9.21 Surcharge des opérateurs.
3-5-1. Liste des opérateurs et leur précédence▲
Description | Opérateurs |
Suffixe unaire et test | expr++ expr-- () [] . |
Préfixe unaire | -expr !expr ~expr ++expr --expr |
Multiplicatif | * / % ~/ |
Additif | + - |
Shift | << >> |
Relationnel et test de type | >= > <= < as is is! |
Égalité | == != |
Bitwise AND | & |
Bitwise XOR | ^ |
Bitwise OR | | |
AND Logique | && |
OR Logique | || |
Expression ternaire | expr1Â ? expr2Â : expr3 |
Cascade | .. |
Assignation | = *= /= ~/= %= += -= <<= >>= &= ^= |= |
3-5-2. Opérateurs arithmétiques▲
Les opérateurs suivants permettent d'effectuer des opérations arithmétiques sur des nombres (num, int et bool). Néanmoins, la surcharge des opérateurs permet d'étendre leur fonctionnalité en s'appliquant à d'autres types.
Opérateur | Sens |
expr++ | x = x + 1 (x == x) |
expr-- | x = x - 1 (x == x) |
++expr | x = x + 1 (x == x + 1) |
--expr | x = x - 1 (x == x - 1) |
+ | Addition |
- | Soustraction |
-expr | Négation |
* | Multiplication |
/ | Division |
~/ | Division retournant un entier |
Exemples :
var
x = 1
;
var
y = 1
;
assert
(x++ == 1
); // x = 1
assert
(x == 2
);
assert
(x-- == 2
); // x = 2
assert
(x == 1
);
assert
(++x == 2
); // x = 2
assert
(--x == 1
); // x = 1
var
a = 100
;
var
b = 3
;
assert
(a + b == 103
);
assert
(b - a == -97
);
assert
(a * b == 300
);
assert
( a / b > 33
.3
&& a / b < 33
.4
);
assert
(a ~/ b == 33
);
assert
(a % b == 1
);
3-5-3. Opérateurs d'égalité et relationnels▲
Ces opérateurs permettent de comparer deux valeurs. Ils retournent true ou false en fonction du résultat de la comparaison.
Opérateur | Sens |
== | Égalité |
!= | Inégalité |
> | Plus grand que |
< | Plus petit que |
>= | Plus grand que ou égal |
<= | Plus petit que ou égal |
Exemples :
assert
(1
== 1
);
assert
(1
!= 2
);
assert
(1
> 0
);
assert
(0
< 1
);
assert
(1
>= 0
);
assert
(1
>= 1
);
assert
(0
<= 1
);
assert
(0
<= 0
);
3-5-4. Opérateurs de test de type▲
Les opérateurs suivants permettent de caster le type d'une expression et de tester son type.
Opérateur | Sens |
as | Casting |
is | est du type indiqué |
is! | n'est pas du type indiqué |
Exemples :
assert
('foo'
is
String
);
assert
('foo'
is
! num
);
// List implémente Iterable
Iterable
iterable = new
List
();
// iterable n'a pas de méthode add()
// nous le castons donc en List
(iterable as
List
).add(10
);
3-5-5. Opérateurs logiques▲
Ces opérateurs permettent d'inverser ou de combiner des expressions booléennes.
Opérateur | Sens |
!expr | Inverse l'expression booléenne qui suit (change true en false et inversement) |
|| | OR logique |
&& | AND logique |
Exemples :
assert
(!false
);
var
a = true
;
var
b = false
;
assert
(a && !b);
assert
(a || b);
assert
(b || a && !b); // && a une priorité supérieure à ||
3-5-6. Opérateurs de déplacement de bits et d'opérations de bit à bit▲
Les opérateurs permettent de manipuler les bits de nombres. Ces opérateurs sont généralement utilisés avec des entiers en base 16.
Opérateur | Sens |
& | AND |
| | OR |
^ | XOR |
~expr | Complément à un (les 0 deviennent des 1 et inversement) |
<< | Décalage à gauche |
>> | Décalage à droite |
Exemples :
// Le détail des opérations est dans le code source
int
value = 0xa9
; //1010 1001
int
bitmask = 0xf0
; //1111 0000
assert
((value & bitmask) == 0xa0
); // AND
assert
((value & ~bitmask) == 0x09
); // AND NOT
assert
((value | bitmask) == 0xf9
); // OR
assert
((value ^ bitmask) == 0x59
); // XOR
assert
((5
<< 1
) == 10
); // Shift left
assert
((10
>> 1
) == 5
); // Shift right
3-5-7. Opérateurs d'assignation▲
À l'exception du premier opérateur qui effectue une assignation simple, les autres effectuent l'opération désignée par l'opérateur se trouvant avant l'opérateur d'égalité.
D'une manière générale, a opérateur = b est équivalent à  : a = a opérateur b.
= | *= | %= | &= | += | /= | <<= | ^= | -= | ~/= |
Les exemples se trouvent dans le code source.
3-5-8. Autres opérateurs▲
Les opérateurs suivants ne peuvent être regroupés dans aucune autre catégorie.
Opérateur | Sens |
() | Change la priorité de résolution d'une expression |
[] | Accès à une liste ou une map (cf. Listes et Maps) |
expr1Â ? expr2Â : expr3 | Expression ternaire (Si expr1 == true alors expr2 sinon expr3) |
. | Accès à un membre d'un objet (cf. Classes) |
.. | Actions multiples sur un objet (cf. Classes) |
3-6. Commentaires▲
3-6-1. Commentaires de code▲
En Dart, les commentaires sont semblables à de nombreux langages (// ou /* … */).
// Ceci est un commentaire d'une ligne.
/*
* Ceci est
* un commentaire
* s'étendant sur plusieurs lignes.
*/
3-6-2. Commentaires de documentation▲
Les commentaires utilisés lors de la génération d'une documentation avec Dart ont la forme suivante (/// ou /** … */). :
/// Ceci est un commentaire de documentation d'une ligne.
/**
* Ceci est un commentaire de documentation
* s'étendant sur plusieurs lignes.
*/
La documentation d'une application ou bibliothèque est générée à l'aide de dartdoc (présent dans le répertoire bin/ du SDK) :
$
dartdoc <
répertoire>
Le répertoire de documentation (nommé docs) est généré à la racine du répertoire courant.
3-7. Contrôle du flow d'exécution▲
Comme tout langage de programmation, Dart permet de contrôler le flow d'exécution d'un programme.
3-7-1. Boucles▲
Les boucles permettent d'itérer sur des éléments.
3-7-1-1. for▲
La boucle que l'on retrouve dans presque tous les langages de programmation est aussi présente en Dart.
List
<int
> list = [1
, 2
, 3
, 4
, 5
, 6
, 7
, 8
, 9
];
int
sum = 0
;
for
(int
i = 0
; i < list.length; i++) {
sum += list[i];
}
assert
(sum == 45
);
3-7-1-2. for in▲
Il est aussi possible d'itérer sur tous les éléments d'une collection sans utiliser de compteur :
List
<int
> list = [1
, 2
, 3
, 4
, 5
, 6
, 7
, 8
, 9
];
int
sum = 0
;
// for (Type variable in collection) {…}
for
(int
element in
list) {
sum += element;
}
assert
(sum == 45
);
3-7-1-3. foreach▲
Les collections et les maps ont une méthode forEach() permettant d'itérer sur chacun des éléments. Cette méthode prend en paramètre une closure - fonction capturant une variable (sum dans le cas présent) hors de son scope. Le seul paramètre de cette closure est e, où e est un élément de la liste, qui au cours de l'itération sur cette liste prendra les différentes valeurs des différents éléments de la liste. Le corps de la fonction est suivi de l'opérateur => et est composé d'une seule ligne : sum += e.
Nous verrons plus en détail les closures dans la partie 3.8.5 Closures.
List
<int
> list = [1
, 2
, 3
, 4
, 5
, 6
, 7
, 8
, 9
];
//---- foreach
int
sum = 0
;
list.forEach((int
e) => sum += e);
assert
(sum == 45
);
3-7-1-4. while▲
Une boucle do-while évalue une condition après l'exécution de la boucle. Le code est par conséquent exécuté au moins une fois.
List
<int
> list = [1
, 2
, 3
, 4
, 5
, 6
, 7
, 8
, 9
];
int
sum = 0
;
int
i = 0
;
do
{
sum += list[i];
i++;
} while
(i < list.length);
assert
(sum == 45
);
3-7-2. Instructions conditionnelles▲
Les instructions conditionnelles permettent d'exécuter des bouts de code sous certaines conditions.
3-7-2-1. if … else▲
Si la condition est vraie, le code qui suit est exécuté, sinon le code après else est exécuté.
List
<int
> list = [1
, 2
, 3
, 4
, 5
, 6
, 7
, 8
, 9
];
List
<int
> leTwo = new
List
();
List
<int
> gtSeven = new
List
();
List
<int
> others = new
List
();
for
(int
e in
list) {
if
(e <= 2
) {
leTwo.add(e);
} else
if
(e > 7
) {
gtSeven.add(e);
} else
{
others.add(e);
}
}
assert
(leTwo.toString() == '[1, 2]'
);
assert
(gtSeven.toString() == '[8, 9]'
);
assert
(others.toString() == '[3, 4, 5, 6, 7]'
);
3-7-2-2. Switch case▲
Une instruction switch compare la valeur d'une variable à une constante. Si les deux coïncident, le case associé à la constante est exécuté, jusqu'à la rencontre d'un break. Si la valeur ne correspond à aucune constante, il est possible de définir un comportement par défaut (comparable au else) à l'aide du mot-clé default.
List
<int
> list = [1
, 2
, 3
, 4
, 5
, 6
, 7
, 8
, 9
];
List
<int
> leTwo = new
List
();
List
<int
> gtSeven = new
List
();
List
<int
> others = new
List
();
for
(int
e in
list) {
switch
(e) {
case
1
:
case
2
:
leTwo.add(e);
break
;
case
8
:
case
9
:
gtSeven.add(e);
break
;
default
:
others.add(e);
break
;
}
}
Dans un switch case tous les types autorisés.
3-7-3. Break et continue▲
Les mots-clés break and continue permettent eux aussi de modifier le flow d'exécution d'un programme.
3-7-3-1. Boucles▲
Dans une boucle, le mot-clé break provoque la fin de son exécution et le saut à la première instruction suivante.
void
main() {
int
sum = 0
;
while
(true
) {
if
(sum == 100
) {
break
;
}
sum++;
}
assert
(sum == 100
);
}
Une fois la variable sum égale à 100, la boucle s'arrête.
Dans une boucle, le mot-clé continue permet d'ignorer les instructions suivantes du corps de la boucle et de revenir au début.
void
main() {
int
sum = 0
;
for
(int
i = 0
; i < 10
; i++) {
if
(i % 2
!= 0
) {
continue
;
}
sum += i;
}
assert
(sum == 20
);
}
Seuls les nombres pairs sont additionnés.
3-7-3-2. Switches▲
Comme dans une boucle, le mot-clé break permet de sortir du switch. En revanche, le mot-clé continue est légèrement différent. Il permet d'exécuter le case suivant un label, et ce bien que la constante (« 2 ») ne corresponde pas à la valeur de la variable (« i »).
void
main() {
for
(int
i = 1
; i <= 3
; i++) {
switch
(i) {
case
1
:
print('1 -
$i
'
);
continue
goto; // Nom du label " goto "
case
3
:
print('3 -
$i
'
);
break
;
goto : // Label suivit de " : "
case
2
:
print('2 -
$i
'
);
break
;
}
}
}
3-8. Fonctions▲
Une fonction est un morceau de code effectuant une tâche, pouvant être réutilisé à différents endroits d'un programme.
En Dart, une fonction est définie par un type de retour, un nom et des paramètres. Le type de retour et les paramètres étant optionnels.
Notes
- les paramètres d'une fonction sont toujours passés par référence ;
- Dart ne supporte pas la surcharge des fonctions ;
- une fonction retournera toujours une référence à un objet. Par abus de langage dans la suite du texte, il ne sera fait aucune distinction entre « une valeur » et « une référence » retournée par une fonction.
3-8-1. Fonctions de haut niveau▲
Une fonction de haut niveau est une fonction définie dans un fichier .dart et non liée à une classe, telle que la fonction main(), le point d'entrée de tout programme Dart.
void
main() {
sayHello();
sayHelloWithName('Foo'
);
int
squaredNum = square(2
);
assert
(squaredNum == 4
);
}
// Fonction sans paramètre, ni valeur retournée
void
sayHello() {
print('Hello'
);
}
// Fonction avec un paramètre, mais sans valeur retournée
void
sayHelloWithName(String
name) {
print('Hello
$name
'
);
}
// Fonction avec un paramètre et une valeur retournée
int
square(int
i) {
return
i * i;
}
3-8-2. Fonctions de premier ordre▲
Le terme de fonction de premier ordre signifie qu'une fonction peut être assignée à une variable, passée en paramètre d'une fonction et retournée par une fonction.
Il n'y a pas de syntaxe particulière pour identifier une fonction de premier ordre. En Dart, toute fonction est de premier ordre.
Pour accéder à un objet de type « fonction » (ce qui est différent de l'appel d'une fonction), il suffit d'utiliser le nom d'une fonction sans parenthèses ni paramètres.
Si nous prenons la fonction square() de l'exemple précédent, il est possible de l'assigner à une variable de type Function, de la manière suivante :
Function
objFunc = square;
Nous pouvons ensuite, soit appeler la fonction square() en passant par la variable objFunc :
int
squaredNum = objFunc(2
);
soit passer la variable objFunc en paramètre d'une autre fonction :
doSomething(objFunc);
Voyons avec un exemple plus complet d'une calculatrice simplifiée au maximum et sans aucun contrôle. Pour faire simple nous considérons que l'expression (formée de deux entiers et d'un opérateur) a été parsée sous la forme d'une liste où le premier élément est l'opérateur et les deux suivants les membres de l'opération.
Commençons tout d'abord par définir les quatre opérations (+, -, *, /) en utilisant des fonctions de haut niveau :
num
add(num
left, num
right) {
return
left + right;
}
num
subtract(num
left, num
right) {
return
left - right;
}
num
multiply(num
left, num
right) {
return
left * right;
}
num
divide(num
left, num
right) {
return
left / right;
}
Nous souhaitons ensuite avoir la fonction correspondant à l'opérateur :
Function
findOperator(String
operator
) {
Function
op = null
;
switch
(operator
) {
case
'+'
:
op = add;
break
;
case
'-'
:
op = subtract;
break
;
case
'*'
:
op = multiply;
break
;
case
'/'
:
op = divide;
break
;
}
return
op;
}
Et nous finissons par la méthode qui prend en paramètre la liste composée des éléments de l'opération et qui effectue le calcul :
num
compute(List
operation) {
Function
operator
= findOperator(operation[0
]);
double
left = double
.parse(operation[1
]);
double
right = double
.parse(operation[2
]);
return
operator
(left, right);
}
void
main() {
List
operation1 = ['+'
, '5'
, '2'
];
num
result1 = compute(operation1);
assert
(result1 == 7
);
List
operation2 = ['/'
, '5.5'
, '2'
];
num
result2 = compute(operation2);
assert
(result2 == 2
.75
);
}
Il est à noter que nous aurions pu aussi avoir une fonction de ce type :
num
compute(Function
operator
, num
left, num
right) {
return
operator
(left, right);
}
Bien que dans le cas présent l'intérêt d'une telle fonction soit limité, elle montre qu'il est possible de passer une fonction en paramètre d'une autre fonction et d'ensuite appeler cette fonction, en passant par une référence et non directement son nom.
3-8-3. Fonctions locales▲
Une fonction locale est une fonction définie à l'intérieur d'une autre et utilisable uniquement à l'intérieur de la fonction parente ou en tant que paramètre.
3-8-3-1. Fonctions anonymes▲
Une fonction anonyme, n'a pas de nom et ne définit pas de type de retour.
Ce type de construction est souvent utilisé pour définir un callback, comme nous le verrons par la suite.
Dans l'exemple suivant, les trois fonctions de la partie « Fonctions de haut niveau » sont transformées en fonctions anonymes et assignées à des variables. Elles sont ensuite appelées tel que dans l'exemple précédent.
Vous noterez que la déclaration de ces fonctions se termine par un point virgule (;).
void
main() {
// Déclaration
Function
sayHello = () {
print('Hello'
);
};
Function
sayHelloWithName = (String
name) {
print('Hello
$name
'
);
};
Function
square = (int
i) {
return
i * i;
};
// Appel
sayHello();
sayHelloWithName('Foo'
);
int
squaredNumber = square(3
);
assert
(squaredNumber == 9
);
Pour l'exemple les fonctions anonymes ont été assignées à des variables. Néanmoins, en situation réelle, comme nous le verrons dans l'exemple concluant la partie sur les fonctions, les fonctions anonymes sont, généralement, directement fournies en paramètre d'une autre fonction.
Une fonction anonyme n'ayant pas de nom, elle peut être récursive uniquement si elle est assignée à une variable définie au préalable.
Function
sayHello = null
;
sayHello = () {
print('Hello'
);
sayHello(); // Provoque une boucle infinie
};
3-8-3-2. Fonctions locales nommées▲
Dans l'exemple précédent, nous avons vu que nous pouvions définir des fonctions anonymes dans une fonction de niveau supérieur. Mais ce n'est pas le seul cas. Il est aussi possible de définir des fonctions nommées.
void
main() {
square(int
i) {
return
i * i;
}
int
squaredNum = square(2
);
assert
(squaredNum == 4
);
}
Suite aux différents exemples d'utilisation des fonctions locales (anonymes et nommées), nous pouvons constater que le code est complexifié. Mais surtout, nous avons à présent des morceaux de code que l'on ne peut pas tester unitairement. Puisqu'il ne faut pas oublier, outre le fait d'offrir la possibilité d'avoir des morceaux de code pouvant être appelés à plusieurs endroits, le fait d'avoir des fonctions atomiques de quelques lignes permet d'avoir des tests unitaires pertinents, facilitant le débogage et donc améliorant la qualité globale d'une application.
De plus, l'utilisation de fonctions locales associées au passage de fonctions en paramètre peut mener à des imbrications typiques du JavaScript, mais allant à l'encontre de toutes les bonnes pratiques de programmation.
3-8-4. Formes simplifiées▲
Toute fonction d'une seule ligne peut être simplifiée en supprimant les accolades et le mot-clé return. Dans ce cas de figure, les fonctions locales sont plus acceptables, mais toujours non testables.
void
main() {
Function
squareAnonymous = (int
i) => i * i;
int
squareLocale(int
i) => i * i;
int
squaredNum = square(2
);
assert
(squaredNum == 4
);
int
squaredNumAnonymous = squareAnonymous(3
);
assert
(squaredNumAnonymous == 9
);
int
squaredNumLocale = squareLocale(3
);
assert
(squaredNumLocale == 9
);
}
int
square(int
i) => i * i;
Cette forme simplifiée retourne toujours quelque chose, sans que l'on ait besoin du mot-clé return. Si l'expression se trouvant à droite de l'opérateur => ne retourne rien, la fonction retourne null.
Function
returnNull = () => print('This function return null'
);
assert
(null
== returnNull());
C'est aussi le cas d'une fonction « retournant » void :
void
main() {
assert
(null
== returnNullToo());
}
void
returnNullToo() {
print("This function doesn't return anything"
);
}
Il est aussi possible d'écrire la fonction main() en forme simplifiée.
void
main() => print('hello'
);
3-8-5. Closures▲
Les fonctions de type closures ne sont pas nouvelles, néanmoins elles n'ont jamais été plus à la mode que ces derniers temps.
Les closures sont très courantes en JavaScript pour émuler un système de Classes avec des getters et des setters. Or en Dart, nous avons de vraies classes, avec des setters et getters. Mais les closures ont d'autres fonctions. Très souvent les développeurs les utilisent sans s'en rendre compte.
Une closure est une fonction locale utilisant (capturant) une variable de niveau supérieur et ceci sans avoir besoin de la passer en paramètre.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
void
main() {
int
number;
Function
incToOne = () => number++;
int
incToFive() => number += 5
;
number = 1
;
incToOne();
assert
(number == 2
);
number = 5
;
incToFive();
assert
(number == 10
);
incToOne();
assert
(number == 11
);
}
#2 - La variable capturée doit être définie avant de pouvoir être utilisée dans la déclaration des closures. Néanmoins, elle a besoin d'être initialisée, avant que la closure soit appelée.
#4 et 5 - Déclaration de closures, sous forme simplifiée (la forme « standard » fonctionne de la même manière).
#7 - Initialisation de la variable number.
#9 Ã 18 - Modification de la valeur de la variable number depuis la fonction main() ou des closures.
La variable number locale à la fonction main() est en réalité une variable globale aux closures.
3-8-5-1. Imbrication de closures▲
Il est aussi possible d'imbriquer les closures. L'exemple suivant utilise la forme simplifiée.
Function
multiAdder(num
one)
=> (num
two)
=> (num
three)
=> one + two + three;
L'appel de la fonction multiAdder() et des fonctions imbriquées peut sembler déconcertant, mais reste simple :
void
main() {
num
sum = multiAdder(1
)(2
)(3
);
assert
(6
== sum);
}
Bien évidemment, une telle construction doit être limitée à des cas simples. Nous en verrons un par la suite.
3-8-5-2. Closure hell▲
Une closure peut aussi être une fonction retournant une autre fonction définie dans son scope :
void
main() {
Function
operator
= (String
opStr) {
num
add(num
left, num
right) {
return
left + right;
}
num
subtract(num
left, num
right) {
return
left - right;
}
num
multiply(num
left, num
right) {
return
left * right;
}
num
divide(num
left, num
right) {
return
left / right;
}
Function
op = null
;
switch
(opStr) {
case
'+'
:
op = add;
break
;
case
'-'
:
op = subtract;
break
;
case
'*'
:
op = multiply;
break
;
case
'/'
:
op = divide;
break
;
}
return
op;
};
assert
(operator
('*'
)(2
, 5
) == 10
);
}
Cet exemple extrême (qui est une reprise de l'exemple de la partie « Fonctions de premier ordre » sous forme de closures) d'une quarantaine de lignes est l'exemple typique de code intestable, - ou tout du moins difficilement -, difficilement lisible et par conséquent à ne pas reproduire. Restons dans la simplicité des méthodes atomiques ne faisant que quelques lignes autant que possible.
3-8-5-3. Rendre testable une closure▲
Néanmoins, dans un langage ayant des fonctions de premier ordre les closures sont indispensables.
Prenons un exemple simple. Si l'on utilise une bibliothèque attendant une fonction en paramètre :
void
doSomething(Function
func) {
}
Pour que la fonction doSomething() puisse utiliser la fonction func() ses paramètres doivent être définis à l'avance. Par conséquent, l'utilisation de closures est la seule et unique solution pour que notre fonction func() puisse en utiliser d'autres. De ce fait pour créer un code lisible et facilement testable, il est conseillé de wrapper la closure dans une fonction de haut niveau, ayant pour seul objectif de prendre les paramètres supplémentaires.
Si l'on prend un exemple simple :
num
execute(Function
operation) {
return
operation();
}
La fonction execute() prend une fonction en paramètre. La fonction quant à elle n'attend aucun paramètre.
Rien ne nous empêche de créer une fonction locale (add() dans notre exemple), mais nous ne pouvons pas la tester indépendamment du reste de la fonction main().
void
main() {
int
left = 2
;
int
right = 3
;
//---- Wrong use
Function
add = () => left + right;
num
wrong = execute(add);
assert
(5
== wrong);
}
Une meilleure solution est d'utiliser l'imbrication de fonctions :
Function
adder(num
left, num
right)
=> ()
=> left + right;
Bien que la fonction execute() attende une fonction ne prenant aucun paramètre, nous avons reconstitué notre fonction add() prenant deux paramètres, utilisés par la closure.
Hormis retourner une autre fonction, la fonction adder() ne fait rien, et c'est bien ce que l'on souhaite. Notre problématique de départ était de pouvoir tester des closures. À présent c'est possible simplement.
num
test = adder(10
, 6
)();
assert
(16
== test);
De plus ceci nous permet de connaître rapidement les paramètres utilisés par la fonction en paramètre de execute().
num
sum = execute(adder(right, left));
assert
(5
== sum);
3-8-6. Typedefs▲
Jusqu'à présent nous avons utilisé le type générique Function, or hormis le fait que l'objet est une fonction nous n'avons pas plus d'informations sur cette dernière, telles que ses paramètres et leur type, ainsi que son type de retour si la fonction retourne quelque chose.
typedef permet de créer un type de fonction personnalisé en précisant sa signature.
typedef
int
Operator(num
left, num
right);
Si l'on reprend l'exemple de notre calculatrice simplifiée, en remplaçant le type Function par Operator, nous obtenons le code suivant, qui selon moi est beaucoup plus clair (les fonctions add(), subtract(), multiply() et divide() étant identiques celles présentées précédemment, elles n'ont pas été reprises) :
void
main() {
List
operation1 = ['+'
, '5'
, '2'
];
int
result1 = computeList(operation1);
assert
(result1 == 7
);
List
operation2 = ['-'
, '5'
, '2'
];
int
result2 = computeList(operation2);
assert
(result2 == 3
);
assert
(compute(add, 5
, 2
) == 7
);
assert
(compute(subtract, 5
, 2
) == 3
);
}
int
computeList(List
operation) {
Operator operator
= findOperator(operation[0
]);
int
left = int
.parse(operation[1
]);
int
right = int
.parse(operation[2
]);
return
operator
(left, right);
}
int
compute(Operator operator
, num
left, num
right) {
return
operator
(left, right);
}
Operator findOperator(String
operator
) {
Operator op = null
;
switch
(operator
) {
case
'+'
:
op = add;
break
;
case
'-'
:
op = subtract;
break
;
case
'*'
:
op = multiply;
break
;
case
'/'
:
op = divide;
break
;
}
return
op;
}
3-8-7. Paramètres optionnels▲
Dart ne permet pas la surcharge des fonctions, mais a une notion de paramètres optionnels. Ceci ne résout pas complètement le problème, mais participe à sa solution.
En Dart, contrairement au JavaScript, tous les paramètres doivent être nommés, même les paramètres optionnels.
3-8-7-1. Paramètres optionnels positionnels▲
Les paramètres optionnels sont indiqués entre crochets (toujours séparés par une virgule) dans la déclaration d'une fonction.
void
main() {
assert
(power(2
) == 4
);
assert
(power(2
, 3
) == 8
);
}
int
power(int
number, [int
exponent]) {
int
result = 1
;
if
(exponent == null
) {
result = number * number;
}
else
{
for
(int
i = exponent; i > 0
; i--) {
result *= number;
}
}
return
result;
}
3-8-7-1-1. Valeurs par défaut▲
Les paramètres optionnels peuvent avoir des valeurs par défaut, indiquées par l'opérateur = et une valeur.
void
main() {
assert
(powerWithDefault(2
) == 4
);
assert
(powerWithDefault(2
, 3
) == 8
);
}
int
powerWithDefault(int
number, [int
exponent = 2
]) {
int
result = 1
;
for
(int
i = exponent; i > 0
; i--) {
result *= number;
}
return
result;
}
3-8-7-1-2. Position d'un paramètre▲
Ces paramètres optionnels sont dits positionnels car leur position compte dans l'appel d'une fonction. Si une fonction a plus d'un paramètre optionnel et que l'on souhaite fournir uniquement le deuxième paramètre optionnel lors de l'appel de la fonction il est nécessaire de fournir le premier, et ce, même s'il a une valeur par défaut.
void
main() {
assert
(multiplyAndDivide(2
, 2
) == 2
);
assert
(multiplyAndDivide(2
, 3
, null
) == 6
);
assert
(multiplyAndDivide(2
, 3
, 2
) == 3
);
assert
(multiplyAndDivide(10
, null
, 5
) == 2
);
}
num
multiplyAndDivide(num
number, [num
multiplier, num
divisor = 2
]) {
if
(multiplier != null
) {
number *= multiplier;
}
if
(divisor != null
) {
number /= divisor;
}
return
number;
}
Note importante : seuls les types suivants peuvent avoir une valeur par défaut :
- bool ;
- num ;
- int ;
- double ;
- String.
3-8-7-2. Paramètres optionnels nommés▲
Pour éviter d'avoir à fournir des paramètres null lors de l'appel d'une fonction, Dart permet d'avoir des paramètres optionnels nommés, qui contrairement aux paramètres positionnels sont encadrés d'accolades.
L'appel d'une fonction en fournissant des valeurs à des paramètres optionnels nommés doit toujours indiquer le nom du paramètre (l'ordre n'a pas d'importance). Par conséquent, modifier le nom d'un paramètre nommé impacte tous les appels à la fonction.
Il est aussi possible de fournir des valeurs par défaut à l'aide de l'opérateur « : » suivi de la valeur.
void
main() {
assert
(power(2
) == 4
);
assert
(power(2
, exponent:3
) == 8
);
}
int
power(int
number, {int
exponent: 2
}) {
int
result = 1
;
for
(int
i = exponent; i > 0
; i--) {
result *= number;
}
return
result;
}
À noter qu'il n'est pas possible d'avoir les deux types de paramètres optionnels dans la définition d'une seule et même fonction. Il est par conséquent nécessaire d'analyser au cas par cas, la solution qui convient le mieux.
3-8-8. Types optionnels▲
En Dart, les types étant optionnels, le type de retour ou des paramètres d'une fonction peuvent être omis.
Si le type de retour n'est pas précisé, la valeur retournée sera de type dynamic.
void
main() {
var
squareAnonymous = (i) => i * i;
var
squaredNumAnonymous = squareAnonymous(3
);
assert
(squaredNumAnonymous == 9
);
var
squaredNum = square(2
);
assert
(squaredNum == 4
);
}
square(i) => i * i;
L'exemple suivant est strictement identique à l'exemple précédent, mais cette fois nous avons utilisé explicitement le type dynamic.
void
main() {
dynamic
squareAnonymousDyn = (dynamic
i) => i * i;
dynamic
squaredNumAnonymousDyn = squareAnonymousDyn(3
);
assert
(squaredNumAnonymousDyn == 9
);
dynamic
squaredNumDyn = squareDyn(2
);
assert
(squaredNumDyn == 4
);
}
dynamic
squareDyn(dynamic
i) => i * i;
3-8-9. Wrap Up▲
Dart donne une certaine liberté aux développeurs. Ceci est à la fois une bonne et une mauvaise chose. D'un côté nous avons la possibilité de laisser libre cours à notre imagination et de l'autre nous donne la possibilité de faire de mauvais choix en termes de design et ceci par facilité.
Pour conclure sur les fonctions nous allons voir comment nous pouvons utiliser les différents types de fonctions en pensant « Design First ». N'ayant pas encore abordé les classes et les bibliothèques, nous nous contenterons d'un exemple simple, qui néanmoins nous permet d'avoir une idée des possibilités de Dart.
Dans cet exemple, nous allons créer et utiliser un pseudowidget de type liste. Le « widget » sera créé par une fonction retournant de l'HTML sous la forme d'une chaîne de caractères.
Imaginons que nous avons une liste d'éléments de type String que nous souhaitons utiliser pour créer notre liste HTML (nous prendrons pour commencer le type « Unorder List »).
Le pseudotest unitaire se présente sous cette forme :
String
ul = buildList(['foo'
, 'bar'
, 'foobar'
, 'barfoo'
]);
String
expectedUl = '<ul>'
'<li>foo</li>'
'<li>bar</li>'
'<li>foobar</li>'
'<li>barfoo</li>'
'</ul>'
;
assert
(expectedUl == ul);
Nous pouvons ensuite implémenter la méthode buildList().
String
buildList(List
<String
> list) {
StringBuffer
sb = new
StringBuffer
();
sb.add('<ul>'
);
for
(String
element in
list) {
sb.add('<li>'
);
sb.add(element);
sb.add('</li>'
);
}
sb.add('</ul>'
);
return
sb.toString();
}
Je ne rentrerai pas dans le détail puisque le code est simple et nous avons déjà vu l'utilisation du for.
Concernant le StringBuffer, il s'agit d'une classe nous permettant de concaténer des chaînes de caractères, puisque Dart ne permet pas de le faire sur des objets de type String comme nous l'avons vu dans la partie 3.5.1. Chaînes de caractères.
Si l'on souhaite ensuite pouvoir créer une liste ordonnée, nous pouvons utiliser un paramètre optionnel (nommé ou non c'est au choix)
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
String
buildList(List
<String
> list, {bool
isOl: false
}) {
StringBuffer
sb = new
StringBuffer
();
sb.add(isOl ? '<ol>'
: '<ul>'
);
for
(String
element in
list) {
sb.add('<li>'
);
sb.add(element);
sb.add('</li>'
);
}
sb.add(isOl ? '</ol>'
: '</ul>'
);
return
sb.toString();
}
Nous avons à présent un widget nous permettant de construire une liste HTML, non ordonnée ou ordonnée, à partir d'une liste de chaînes de caractères. Chaque élément de cette liste est affiché tel qu'il a été inséré dans la liste. Si nous souhaitons, par exemple, transformer tous les caractères en majuscules, nous devons changer la ligne 6 en :
sb.add(element.toUpperCase());
Mais ce n'est pas du tout une solution générique. L'utilisateur de notre constructeur de widget doit pouvoir indiquer comment formater les différents éléments de la liste à afficher. Et c'est à cet instant qu'entrent en jeu les fonctions de premier ordre, associées à un typedef.
void
main() {
String
ul = buildList((String
element) => element.toUpperCase(),
['foo'
, 'bar'
, 'foobar'
, 'barfoo'
]);
}
typedef
String
Formatter(String
text);
String
buildList(Formatter format, List
<String
> list, {bool
isOl: false
}) {
StringBuffer
sb = new
StringBuffer
();
sb.add(isOl ? '<ol>'
: '<ul>'
);
for
(String
unformattedText in
list) {
sb.add('<li>'
);
sb.add(format(unformattedText));
sb.add('</li>'
);
}
sb.add(isOl ? '</ol>'
: '</ul>'
);
return
sb.toString();
}
Au travers cet exemple extrêmement simple se dessine une complexification de l'appel de la méthode. En JavaScript, il est assez courant de trouver des bibliothèques avec des fonctions ayant plus de 20 paramètres, dont nombreux sont des closures.
Sources :
3-9. Programmation Orientée Objet▲
Dart permet d'avoir des classes concrètes et abstraites, mais aussi des interfaces et des mixins. Bien que ces éléments soient présents dans de nombreux langages, leur utilisation est légèrement différente.
Dart nous abstrait de la verbosité de certains langages, tout en ajoutant une complexité non négligeable. Ceci additionné à la liberté offerte par le langage nous impose d'être extrêmement rigoureux pour produire des applications de qualité.
De plus, il est important de garder à l'esprit que s'il est possible de créer des sites Web, Dart s'illustre dans la création d'application Web d'envergure d'une seule page.
Note : en Dart, il est possible de définir plusieurs classes dans un même fichier.
3-9-1. Classes concrètes▲
En Dart, une classe peut contenir des champs, des getters et des setters pour ces champs, des constructeurs et des méthodes. Toutes les classes héritent de la classe Object, par défaut (si la classe n'hérite d'aucune classe ou par transitivité).
Une classe est instanciée à l'aide du mot-clé new.
Commençons par voir une simple classe (Person) ayant deux champs (firstName et lastName) et une méthode (getFullName()) retournant le prénom et le nom de la personne séparés par un espace.
class
Person {
String
firstName;
String
lastName;
String
getFullName() {
return
'
$firstName
$lastName
'
;
}
}
Une fois la classe Person instanciée, il est possible d'accéder aux champs de l'instance (nommé person dans cet exemple) de la manière suivante :
person.firstName;
Après instanciation, les deux champs sont null :
assert
(null
== person.firstName);
assert
(null
== person.lastName);
Nous allons donc leur assigner une valeur :
person.firstName = 'Foo'
;
person.lastName = 'Bar'
;
3-9-1-1. Fonction main complète▲
void
main() {
Person person = new
Person();
assert
(null
== person.firstName);
assert
(null
== person.lastName);
person.firstName = 'Foo'
;
person.lastName = 'Bar'
;
assert
('Foo Bar'
== person.getFullName());
}
3-9-2. Constructeurs▲
Si une classe ne définit pas de constructeur, telle que la classe Person, un constructeur implicite par défaut sans paramètre est fourni par la Machine Virtuelle.
Néanmoins, nous souhaitons parfois définir nos propres constructeurs :
Person(String
firstName, String
lastName) {
this
.firstName = firstName;
this
.lastName = lastName;
}
Lorsque l'on définit un constructeur dans une classe, le constructeur par défaut n'est plus accessible. À présent, il est nécessaire de fournir le prénom et le nom dans le constructeur pour pouvoir instancier la classe Person.
Person person = new
Person('Foo'
, 'Bar'
);
Les champs firstName et lastName étant toujours modifiables :
person.firstName = 'FooFoo'
;
Nous verrons par la suite comment restreindre la visibilité de champs, méthodes et classes.
3-9-2-1. this▲
Le mot-clé this représente l'instance courante d'une classe. Il permet au compilateur de faire la distinction entre les champs de la classe et les paramètres du constructeur.
void
main() {
Person person = new
Person('Foo'
, 'Bar'
);
assert
('Foo'
== person.firstName);
assert
('Bar'
== person.lastName);
assert
('Foo Bar'
== person.getFullName());
person.firstName = 'FooFoo'
;
person.lastName = 'BarBar'
;
assert
('FooFoo BarBar'
== person.getFullName());
}
3-9-3. Un constructeur avec un sucre, svp !▲
Dart nous permet de simplifier la définition d'un constructeur :
Person(this
.firstName, this
.lastName);
Ce constructeur fait exactement la même chose que le constructeur précédent, la verbosité en moins.
Notez l'absence des accolades et la présence d'un point-virgule à la fin de la déclaration.
Malheureusement, l'utilisation d'un tel constructeur nous fait perdre une information importante (dans un IDE ou la documentation), le type des paramètres. À présent, pour le connaître nous devons aller voir le code. Mais nous pouvons pallier ce problème simplement, ce qui ne nous empêche pas de rester concis.
Person(String
this
.firstName, String
this
.lastName);
3-9-4. Écriture standard et sucre syntaxique▲
Dart permet de mixer l'écriture des paramètres d'un constructeur :
Person(String
firstName, String
this
.lastName) {
this
.firstName = firstName;
}
ou
Person(String
this
.firstName, String
lastName) {
this
.lastName = lastName;
}
3-9-5. Surchage de constructeurs▲
En Dart, il n'est pas possible de surcharger un constructeur. De ce fait, nous ne pouvons pas avec plusieurs constructeurs (sans nom ou avec le même) avec des paramètres différents.
Pour pouvoir instancier une classe avec des paramètres différents, Dart a la notion de constructeurs nommés.
Dans l'exemple suivant, nous avons défini un constructeur par défaut utilisant le sucre syntaxique et un constructeur nommé withNames prenant en paramètre un prénom et un nom.
// Constructeur sans paramètres
Person();
// Constructeur nommé
Person.withNames(String
this
.firstName, String
this
.lastName);
L'instanciation de la classe Person s'effectue de la manière suivante :
Person personUnknown = new
Person();
Person personInitialized = new
Person.withNames('Foo'
, 'Bar'
);
3-9-6. Constructeur avec paramètres optionnels▲
Tout comme les fonctions, un constructeur peut avoir des paramètres optionnels nommés ou non, avec ou sans valeur par défaut.
Person.withOptPositional(this
.firstName, [String
lastName = '0'
]) {
this
.lastName = lastName;
}
Person.withOptPositionalSugar(this
.firstName, [this
.lastName]);
Person.withOptNamed(this
.firstName, {String
lastName}) {
this
.lastName = lastName;
}
Person.withOptNamedSugar(this
.firstName, {this
.lastName : '0'
});
3-9-7. Préconstructeur▲
Dart permet d'initialiser des champs avant l'exécution du corps du constructeur :
class
Point {
num
x;
num
y;
Point(this
.x, this
.y);
Point.fromJson(Map
json) : x = json['x'
], y = json['y'
] {
print('In Point.fromJson(): (
$x
,
$y
)'
);
}
}
void
main() {
new
Point.fromJson({'x'
: 10
, 'y'
: 20
});
}
Note : la partie à droite du deux-points « : » n'a pas accès au mot-clé this.
3-9-8. Méthodes▲
Une méthode est comparable à une fonction à l'exception qu'elle est définie à l'intérieur d'une classe et n'est accessible qu'à partir d'une instance de classe d'où le nom de méthode d'instance.
Une méthode est définie et est appelée de la même manière qu'une fonction de haut niveau. Elle peut prendre des paramètres optionnels ou non et retourner une valeur.
Pour illustrer la déclaration et l'utilisation des méthodes, nous allons reprendre la calculatrice de la partie sur les fonctions et la transformer pour en faire une application orientée objet.
Pour limiter la taille du code, toutes les parties non essentielles ont été supprimées.
Commençons par créer un objet (Data) destiné à contenir le détail de l'opération (contenu dans une liste dans les exemples précédents).
class
Data {
String
operator
;
num
left;
num
right;
Data(String
this
.operator
, num
this
.left, num
this
.right);
}
La classe Data ne définit qu'un seul constructeur nous permettant de fournir toutes les données utiles pour effectuer le calcul.
N'ayant pas encore vu comment rendre un objet immuable, ni comment lever une exception, nous partirons du principe que les données fournies dans le constructeur sont correctes et qu'elles ne seront pas modifiées après instanciation.
Nous définissons ensuite le contrat auquel les différentes opérations (addition, soustraction, etc.) doivent souscrire pour pouvoir être utilisées par notre calculatrice.
typedef
double
Operator(num
left, num
right);
Il est important de noter qu'un typedef est la déclaration d'un type à part entière et par conséquent il ne peut être inclus dans une classe.
Il ne nous reste plus qu'à créer la classe permettant d'effectuer le calcul.
L'instanciation de la calculatrice pourra s'effectuer de deux façons :
new
Calculator(new
Data('+'
, 5
, 5
));
new
Calculator.buildData('-'
, 5
, 10
);
Nous allons donc avoir besoin de deux constructeurs. Toutes les méthodes ne sont qu'une reprise des fonctions de l'exemple précédent.
class
Calculator {
Data data;
Calculator(this
.data);
Calculator.buildData(String
operator
, num
left, num
right) {
this
.data = new
Data(operator
, left, right);
}
double
compute() {
Operator operator
= this
.findOperator(data.operator
);
return
operator
(data.left, data.right);
}
Operator findOperator(String
operator
) {
// retourne la fonction correspondant à l'opérateur
}
double
add(num
left, num
right) => (left + right).toDouble();
// subtract(), multiply(), divide()
}
Nous terminons cet exemple par la fonction main() qui nous permet de tester notre minicalculatrice.
void
main() {
Calculator calculator1 = new
Calculator(new
Data('+'
, 5
, 5
));
assert
(10
== calculator1.compute());
Calculator calculator2 = new
Calculator.buildData('-'
, 5
, 10
);
assert
(-5
== calculator2.compute());
}
Dans cet exemple il n'a pas été question de fonctions anonymes pour la bonne et simple raison que nous n'en avions pas besoin. Notez néanmoins que leur utilisation dans une méthode est strictement identique à celle dans une fonction.
3-9-9. Variables et méthodes statiques▲
Bien que déclarées à l'intérieur d'une classe, les variables et méthodes statiques ne sont pas liées à une instance de cette classe.
3-9-9-1. Variables statiques▲
Une variable statique existe de sa première utilisation (moment auquel elle est appelée) jusqu'à la fin de l'exécution d'une application. Son état est partagé à travers toute l'application. De fait, une variable statique est similaire à une variable globale. Par conséquent, il conviendra d'être prudent quant à leur utilisation devant se limiter à des cas bien précis. Il est important de noter que la notion de constante statique n'entre pas dans cette limitation.
Une variable statique est définie à l'aide du mot-clé static de la manière suivante :
static
int
counter = 0
;
La variable counter étant statique et donc non liée à une instance précise d'une classe, son accès est différent de l'accès à un champ d'une classe.
Si la variable counter est définie dans une classe nommée Point, nous pouvons y accéder de la manière suivante :
Point.counter
La valeur de cette variable pouvant être bien évidemment modifiée.
Point.counter = 50
;
Vous trouverez ci-dessous un exemple plus complet :
void
main() {
Point pointOne = new
Point();
assert
(1
== Point.counter);
Point.counter = 50
;
Point pointTwo = new
Point();
assert
(51
== Point.counter);
}
class
Point {
static
int
counter = 0
;
// La variable statique counter est incrémentée
// Ã chaque instanciation de la classe Point.
Point() {
counter++;
}
}
3-9-9-2. Méthodes statiques▲
Tout comme une variable statique, une méthode statique n'est pas liée à une instance d'une classe. Par conséquent une méthode statique ne peut accéder aux champs d'instance d'une classe.
Une méthode statique est sans état et se limite à un rôle utilitaire.
void
main() {
assert
(StringUtils.isEmpty(null
));
assert
(!StringUtils.isEmpty(""
));
assert
(!StringUtils.isNotEmpty(null
));
assert
(StringUtils.isNotEmpty(""
));
}
class
StringUtils {
static
bool
isEmpty(String
s) {
return
s == null
;
}
static
bool
isNotEmpty(String
s) {
return
!StringUtils.isEmpty(s);
}
}
Il est important de noter que si une méthode statique est utilisée au sein d'une classe, il n'est pas utile de la faire précéder du nom de la classe. Nous aurions pu écrire la méthode isNotEmpty() de la manière suivante :
static
bool
isNotEmpty(String
s) {
return
!isEmpty(s);
}
Avec l'existence de variables globales (que nous verrons plus loin) et de méthodes de haut niveau, l'intérêt d'avoir des champs et des méthodes statiques est difficile à définir.
3-9-9-3. Variables et méthodes statiques vs champs et méthodes d'instances▲
Bien qu'une méthode statique ne puisse accéder à un champ ou une méthode d'instance, un constructeur ou une méthode d'instance peut accéder à une variable ou une méthode statique. De plus, il est possible d'assigner une variable statique à un champ d'instance.
void
main() {
Person personOK = new
Person('Foo'
, 'Bar'
);
assert
('Foo Bar'
== personOK.getFullName());
Person personKO = new
Person('Foo'
, null
);
assert
(Person.ANONYMOUS == personKO1.getFullName());
}
class
Person {
static
String
ANONYMOUS = 'Anonymous'
;
bool
isValidName = true
;
String
firstName;
String
lastName;
Person(String
this
.firstName, String
this
.lastName) {
if
(isBlank(this
.firstName) || isBlank(this
.lastName)) {
isValidName = false
;
}
}
String
getFullName() {
if
(this
.isValidName) {
return
'
$firstName
$lastName
'
;
} else
{
return
ANONYMOUS;
}
}
static
bool
isBlank(String
s) {
return
s == null
|| s.length == 0
;
}
}
3-9-10. Héritage▲
Dart permet l'héritage simple. Le mot-clé extends est réservé à cet effet.
class
Person {
}
class
Developer extends
Person {
}
De la même manière que toute classe est de type Object, toute instance d'une sous-classe est du type de son parent.
//---- Type is Type
assert
(Person is
Object
);
assert
(Developer is
Object
);
//---- Instance is Type
assert
(new
Person() is
Object
);
assert
(new
Developer() is
Object
);
assert
(new
Developer() is
Person);
// Person n'est pas de type Developer
assert
(!(new
Person() is
Developer));
L'héritage d'une classe permet d'accéder à ses champs et méthodes :
class
Person {
String
firstName;
String
lastName;
String
getFullName() {
return
'
$firstName
$lastName
'
;
}
}
class
Developer extends
Person {
String
language;
String
getNameAndLanguage() {
return
'
${
this
.getFullName()}
knows
$language
'
;
}
}
Comme nous pouvons le voir, dans la classe Developer, nous utilisons la méthode getFullName() qui est déclarée dans la classe Person.
Nous utilisons aussi le mot-clé this, qui dans ce contexte n'est pas obligatoire, pour démontrer que nous n'avons bien qu'une seule instance et que les méthodes et champs de la classe Person appartiennent, par héritage, à la classe Developer.
En Dart, les classes enfants n'héritent pas des constructeurs de leur classe parente. Par conséquent, si vous souhaitez utiliser le constructeur d'une superclasse, il est nécessaire de définir un constructeur dans la classe enfant qui fera référence à constructeur parent à l'aide du mot-clé super. De même, si la classe parente définit uniquement des constructeurs avec des paramètres, tous les constructeurs de la classe enfant doivent fournir, à l'aide de super, les paramètres permettant d'utiliser celui qui nous intéresse.
NomDeLaClasse(<paramètres>) : super
<paramètres du parent>);
Ou
NomDeLaClasse(<paramètres>) : super
(<paramètres du parent>) {
}
Le mot-clé super doit être utilisé dans le « bloc d'initialisation » qui se trouve après les paramètres du constructeur et avant son corps.
class
Person {
String
firstName;
String
lastName;
Person(this
.firstName, this
.lastName);
}
class
Developer extends
Person {
String
language;
Developer.empty() : super
('No'
, 'Name'
);
Developer(String
firstName, String
lastName) : super
(firstName, lastName);
}
Pour accéder à un constructeur nommé, il suffit d'ajouter le nom à la suite de super, séparé par un point.
class
Person {
// ...
Person.broken(this
.firstName);
// ...
}
class
Developer extends
Person {
// ...
Developer.repaired(String
firstName, String
lastName) : super
.broken(firstName) {
this
.lastName = lastName;
}
// ...
}
Bien évidemment, il est possible de fournir au constructeur de la classe Developer, plus de paramètres que n'attend celui de la classe Person.
Developer.full(String
firstName, String
lastName, String
this
.language) :
super
(firstName, lastName);
Developer.fullWithBody(String
firstName, String
lastName, String
language) :
super
(firstName, lastName) {
this
.language = language;
}
Bien que non lié au concept d'héritage, il est aussi possible de rediriger un constructeur vers un autre constructeur (un tel constructeur ne peut pas avoir de corps).
Developer.withRedirectionToNotNamed() : this
('No'
, 'Name'
);
Developer.withRedirectionToNamed(String
firstName, String
lastName) :
this
.full(firstName, lastName, 'Dart'
);
Je vois deux utilités à la redirection vers un autre constructeur :
- fournir des paramètres par défaut ;
- changer le nom d'un constructeur. L'ancien est donc déprécié, mais toujours présent pour ne pas casser le code pouvant toujours l'utiliser.
Il est important de noter que seuls les champs et méthodes d'instance sont hérités. Les variables et méthodes statiques ont un comportement différent. Pour appeler les champs et méthodes statiques d'une classe, il est nécessaire de les préfixer par le nom de la classe les définissant.
void
main() {
ExtendedStringUtils utils = new
ExtendedStringUtils();
assert
('StringUtils'
== utils.getParentClassName());
// Incorrect
//ExtendedStringUtils.className;
// Incorrect
//ExtendedStringUtils.isEmpty('');
// Correct
assert
(StringUtils.isEmpty(null
));
// Correct
assert
(ExtendedStringUtils. isBlank(''
));
}
class
StringUtils {
static
String
className = 'StringUtils'
;
static
bool
isEmpty(String
s) {
return
s == null
;
}
}
class
ExtendedStringUtils extends
StringUtils {
static
bool
isBlank(String
s) {
return
StringUtils.isEmpty(s) || s.length == 0
;
}
String
getParentClassName() {
return
StringUtils.className;
}
}
3-9-11. Surcharge d'une méthode▲
Une sous-classe peut définir une méthode qui possède exactement la même signature qu'une méthode se trouvant dans sa classe parente. Dans un tel cas, on dit que la méthode se trouvant dans la sous-classe surcharge la méthode de la superclasse.
class
Developer extends
Person {
String
language;
String
getFullName() {
return
'
${
super
.getFullName()}
knows
$language
'
;
}
}
Vous noterez qu'à l'aide du mot-clé super, il est toujours possible, depuis la classe enfant, d'utiliser la méthode de la classe parente.
3-9-12. Classes abstraites▲
Une classe abstraite est déclarée à l'aide du mot-clé abstract.
abstract
class
Controller {
}
Contrairement à une classe concrète, une classe abstraite ne peut être instanciée. Une classe abstraite permet de définir certains comportements à l'aide de méthodes concrètes, mais aussi des méthodes abstraites (sans corps). Les méthodes abstraites définissent un contrat que doit accepter la classe concrète héritant de cette classe abstraite. Accepter le contrat signifie implémenter les méthodes abstraites.
abstract
class
Controller {
void
execute() {
start();
doSomething();
end();
}
void
start() {
print("Start"
);
}
void
end() {
print("End"
);
}
void
doSomething();
}
class
Showcase extends
Controller {
void
doSomething() {
print("Showcase"
);
}
}
class
Login extends
Controller {
void
doSomething() {
print("Login"
);
}
}
La classe précédente a quatre méthodes, dont une abstraite (doSomething()). La méthode execute() appelle les trois autres méthodes.
Prenons un exemple simple d'un serveur HTTP, contenant une liste de contrôleurs de type Controller et appelant la méthode execute(), en fonction d'une URL :
void
main() {
Map
<String
, Controller> controllers =
{'showcase/'
: new
Showcase(), 'login/'
: new
Login()};
// 1st Request
print('---- First Request'
);
String
request = 'login/'
;
controllers[request].execute();
// 2nd request
print('---- Second Request'
);
request = 'showcase/'
;
controllers[request].execute();
}
3-9-13. Héritage de classes abstraites▲
Si l'on souhaite surcharger une (ou plus) méthode de la classe Controller, sans avoir à implémenter la méthode doSomething() et sans casser notre pseudoserveur de l'exemple précédent, nous pouvons créer une nouvelle classe abstraite héritant de la classe Controller. Tout comme les classes concrètes, l'héritage des classes abstraites est un héritage simple.
abstract
class
MyController extends
Controller {
void
execute() {
preStart();
super
.execute();
}
void
preStart() {
print("PreStart"
);
}
void
end() {
print("New End"
);
}
}
En rajoutant la classe abstraite MyController et en la faisant devenir le parent des classes ShowCase et Login. Nous avons défini un nouveau comportement commun, qui ne nécessite aucune modification de la fonction main() de l'exemple précédent.
À noter que l'héritage de constructeurs entre deux classes abstraites ou une classe abstraite et une concrète fonctionne de la même manière qu'en cas d'héritage entre deux classes concrètes.
3-9-14. Interfaces▲
Telle une classe abstraite, une interface est utilisée pour définir un contrat - sans pour autant définir de comportement par défaut -, sous la forme de méthodes abstraites uniquement. De plus, une classe peut implémenter une ou plusieurs interfaces.
Le mot-clé implements permet d'indiquer qu'une classe implémente une interface.
3-9-14-1. Types multiples▲
En Dart, les interfaces n'existent pas à proprement parler. Il s'agit en réalité de classes (concrètes ou abstraites) utilisées en tant qu'interfaces.
Pour cet exemple, nous utiliserons des Value Objects dynamiques. Nous verrons dans la partie sur les « Generics » comment nous pouvons avoir une construction dans l'esprit POO.
abstract
class
DynaBean {
Map
<String
, Object
> map;
DynaBean() {
this
.map = new
Map
();
}
Object
get
(String
key) => this
.map[key];
Object
set
(String
key, String
value) => this
.map[key] = value;
Iterable
<Object
> getValues() => this
.map.values;
}
class
User extends
DynaBean {
static
final
String
us = 'username'
;
static
final
String
ps = 'password'
;
User.empty() : super
() {}
User(String
username, String
password) : super
() {
this
.map[us] = username;
this
.map[ps] = password;
}
}
Nous définissons ensuite deux classes. Une concrète (Validator) et une abstraite (Processor). La méthode validate() de la classe Validator vérifie qu'aucun champ du DynaBean n'est nul. Quant à la méthode process() de la classe Processor, il s'agit d'une méthode abstraite.
class
Validator {
bool
validate(DynaBean dynaBean) {
for
(Object
obj in
dynaBean.getValues()) {
if
(StringUtils.isEmpty(obj)) {
return
false
;
}
}
return
true
;
}
}
abstract
class
Processor {
bool
process(DynaBean dynaBean);
}
Bien que nous ne puissions rien faire avec la classe Processor en l'état puisqu'elle ne peut être instanciée, la classe Validator est tout à fait utilisable :
void
main() {
Validator validator = new
Validator();
bool
isValid = validator.validate(new
User(''
, ''
));
assert
(isValid);
}
Mais ces deux classes peuvent aussi être utilisées en tant qu'interface :
class
LoginService implements
Validator, Processor {
static
List
<User> USERS_DB = [new
User('root'
, 'secure'
)];
bool
validate(User user) {
if
(StringUtils.isBlank(user.get
(User.ps))) {
return
false
;
}
if
(StringUtils.isBlank(user.get
(User.us))) {
return
false
;
}
return
true
;
}
bool
process(User user) {
if
(LoginService.contains(USERS_DB, user)) {
return
true
;
} else
{
return
false
;
}
}
static
bool
contains(List
<User> users, User user) {
for
(User u in
users) {
if
(u.get
(User.us) == user.get
(User.us)
&& u.get
(User.ps) == user.get
(User.ps)) {
return
true
;
}
}
return
false
;
}
}
Lorsqu'une classe est utilisée en tant qu'interface toutes ses méthodes, y compris celles contenant une implémentation sont considérées comme abstraites. D'où la nécessité d'implémenter les méthodes validate() et process().
Note : étant donné que nous n'avons pas encore vu la redéfinition des opérateurs, nous sommes contraints d'avoir la méthode contains().
void
main() {
//---- Validator as interface
Validator userValidator = new
LoginService();
bool
isValid2 = userValidator.validate(new
User(''
, ''
));
assert
(!isValid2);
bool
isValid3 =
userValidator.validate(new
User('Foo'
, r'pa$$word'
));
assert
(isValid3);
//---- Check as inteface
Processor processor = userValidator as
Processor;
bool
processed = processor.process(new
User('not'
, 'found'
));
assert
(!processed);
processed = processor.process(new
User('root'
, 'secure'
));
assert
(processed);
//---- Full Login
User user = new
User('root'
, 'secure'
);
LoginService login = new
LoginService();
assert
(login.validate(user));
assert
(login.process(user));
}
Comme nous pouvons le voir, la classe LoginService est à la fois du type LoginService, mais aussi des types Validator et Processor. Dans le cas présent ceci n'a que peu d'intérêt, mais si l'on reprend un exemple tel que le contrôleur, utiliser des interfaces nous permet de passer la variable login à des méthodes attendant un paramètre de type Validator ou Processor.
3-9-14-2. Inversion of Control▲
Le fait d'utiliser des classes en tant qu'interface peut être troublant à première vue, mais est incontestablement un plus, notamment pour les tests unitaires. Au lieu d'avoir une interface utilisée par une « vraie » implémentation et un mock, nous avons la vraie implémentation faisant office d'interface pour le mock.
class
AuthenticationService {
User checkUser(String
username, String
password) {
// Check if user exist in database
return
null
;
}
}
class
LoginController {
AuthenticationService authService;
LoginController(AuthenticationService this
.authService);
User login(String
username, String
password)
=> this
.authService.checkUser(username, password);
}
class
User {
String
userName;
String
password;
User(String
this
.userName, String
this
.password);
}
void
main() {
LoginController loginController =
new
LoginController(new
AuthenticationService());
User user = loginController.login('root'
, 'secure'
);
assert
(null
== user);
}
Nous utilisons à présent la classe AuthenticationService en tant qu'interface implémentée par MockAuthenticationService.
class
MockAuthenticationService implements
AuthenticationService {
static
List
<User> USERS_DB = [new
User('root'
, 'secure'
)];
User checkUser(String
username, String
password) {
User user = new
User(username, password);
if
(contains(USERS_DB, user)) {
return
user;
} else
{
return
null
;
}
}
static
bool
contains(List
<User> users, User user) {
//...
}
}
void
main() {
LoginController loginControllerWithMock
= new
LoginController(new
MockAuthenticationService());
User userWithMock = loginControllerWithMock.login('root'
, 'secure'
);
assert
(null
!= userWithMock);
}
Tout comme les classes, une interface peut hériter d'une autre interface, mais dans ce cas il n'est plus question de constructeur.
3-9-15. Factories▲
Le design pattern factory est un pattern créationnel permettant d'instancier des objets en fonction de la situation. L'implémentation étant par conséquent inconnue de l'utilisateur.
Note : les constructeurs de type factory n'ont pas accès à this et par conséquent aux champs et méthodes d'instance.
3-9-15-1. Implémentations par défaut▲
Comme nous l'avons vu, utiliser des interfaces permet de changer facilement d'implémentation, mais offre aussi la possibilité de mocker certaines implémentations pour pouvoir tester une classe unitairement. Néanmoins, avoir une implémentation par défaut simplifie les choses pour le développeur qui n'a pas besoin de connaître le nom d'une interface et d'une classe l'implémentant.
Pour ce faire, Dart a le mot-clé factory permettant d'avoir l'impression d'instancier une classe abstraite.
abstract
class
Validator {
factory
Validator() {
return
new
NoneBlankValidator();
}
bool
validate(DynaBean dynaBean);
}
class
NoneBlankValidator implements
Validator {
bool
validate(DynaBean dynaBean){
for
(Object
obj in
dynaBean.getValues()) {
if
(StringUtils.isEmpty(obj)) {
return
false
;
}
}
return
true
;
}
}
Le test suivant est strictement identique à celui instanciant la classe Validator qui est une classe concrète (cf. 3.10.14.1. Types multiples).
void
main() {
Validator validator = new
Validator();
bool
isValid = validator.validate(new
User(''
, ''
));
assert
(isValid);
}
3-9-15-2. Caching▲
Outre le fait de proposer une implémentation par défaut, les factories introduisent une notion de caching.
class
Logger {
final
String
name;
bool
isEnabled = false
;
static
final
Map
<String
, Logger> cache = <String
, Logger>{};
factory
Logger(String
name) {
if
(cache.containsKey(name)) {
return
cache[name];
} else
{
final
logger = new
Logger.internal(name);
cache[name] = logger;
return
logger;
}
}
Logger.internal(this
.name);
void
log(String
msg) {
if
(isEnabled) {
print('
$name
-
$msg
'
);
}
}
}
Avec le test suivant, nous constatons qu'à la seconde instanciation de la classe Logger, nous obtenons la même instance que précédemment (le champ isEnabled est toujours à true, alors qu'il est initialisé à false).
void
main() {
Logger logger = new
Logger('caching_factory'
);
logger.isEnabled = true
;
assert
(1
== Logger.cache.length);
Logger newLogger = new
Logger('caching_factory'
);
assert
(logger.isEnabled);
}
Si au lieu de mettre en cache une liste d'instances, nous n'en mettons qu'une, nous obtenons un singleton.
3-9-16. Generics▲
Il arrive parfois que nous ayons besoin d'une classe utilisant un (ou plus) objet dont le type n'a aucune importance. C'est dans ce cas précis qu'interviennent les Generics.
Dart étant optionnellement typé, nous pouvons très bien n'en utiliser aucun, voire utiliser le type Object. Mais n'utiliser aucun type, rend le code difficile à lire et utiliser le type Object n'est pas une bonne pratique. Si une méthode retourne une valeur de type Object, pour pouvoir l'utiliser dans un contexte typé, nous sommes dans l'obligation de la caster dans le bon type.
L'exemple de la classe de validation est un cas se prêtant parfaitement aux Generics.
Une classe utilisant des Generics est identifiée par l'utilisation du symbole en diamant encadrant le type générique.
class
MyClass<T> {
}
Dans cet exemple T est le type générique utilisé selon les besoins dans la classe, mais n'ayant aucune existence avant instanciation. Nous utilisons T par convention pour signifier Type, mais nous pouvons utiliser tout identifiant valide (cf 3.3 Noms de variables, fonctions, classes et bibliothèques).
class
Validator<T> {
bool
validate(T object) {
for
(Object
obj in
object.valuesToValidate()) {
if
(StringUtils.isEmpty(obj.toString())) {
return
false
;
}
}
return
true
;
}
}
Nous avons à présent un validateur générique, à condition d'implémenter la méthode valuesToValidate().
class
User {
String
username;
String
password;
User(this
.username, this
.password);
List
<Object
> valuesToValidate() {
return
[username, password];
}
}
Dart étant optionnellement typé une telle utilisation est autorisée, néanmoins si vous utilisez des Generics, autant le faire dans les règles de l'art.
Si vous ajoutez le code précédent dans un IDE effectuant une analyse statique, vous aurez un warning sur la méthode valuesToValidate(). Ceci n'est pas une surprise, puisque T est un type inconnu et par conséquent nous ne savons pas s'il possède cette méthode.
Pour nous en prémunir, il suffit de créer une interface :
abstract
class
Validatable {
List
<Object
> valuesToValidate();
}
Et de l'ajouter aux classes Validator et User :
class
Validator<T extends
Validatable> {
// ...
}
class
User implements
Validatable {
// ...
}
Il peut paraître surprenant d'utiliser extends au lieu de implements à l'intérieur du symbole en diamant, mais en réalité cela n'a pas d'importance, puisque l'analyse statique va vérifier si la méthode existe dans le type (Validatable dans notre cas). Que cette méthode soit abstraite ou concrète ne change absolument rien puisque dans un cas comme dans l'autre le paramètre object de la méthode validate() de type T référence une instance d'une classe, qui par définition implémente (ou l'un de ses ancêtres) cette méthode.
Pour tester ce nouveau code, nous pouvons reprendre celui utilisé lorsque la méthode validate() prenait un DynaBean, et nous pouvons constater que le test est toujours valide.
void
main() {
//---- Validator
Validator validator = new
Validator();
bool
isValid = validator.validate(new
User(''
, ''
));
assert
(isValid);
}
3-9-17. Exceptions▲
Il est possible en Dart de lever et de catcher des exceptions. Des exceptions sont des erreurs indiquant que quelque chose d'inattendu est arrivé. Si une exception n'est pas catchée, l'isolate dans lequel elle a été levée est interrompu, et généralement l'isolate et son programme sont arrêtés.
En Dart, toutes les exceptions sont de type « unchecked ». Par conséquent, les méthodes ne déclarent pas les exceptions quelles peuvent potentiellement lever, ce qui implique qu'il n'y a aucune obligation à catcher une exception quelle qu'elle soit.
Dart fournit les types Exception et Error, ainsi que de nombreux sous-types. Bien évidemment, vous pouvez définir vos propres exceptions.
De plus, en Dart, tout objet non null peut être utilisé en tant qu'exception, utilisant le mot-clé throw.
Dans l'exemple suivant nous levons une exception de type List et l'attrapons à l'aide du mot-clé on.
bool
exceptionThrown = false
;
try
{
throw
new
List
();
} on
List
{
print('List thrown'
);
exceptionThrown = true
;
}
assert
(exceptionThrown);
Dans l'exemple précédent, nous avons attrapé l'exception, mais nous n'avons pas récupéré l'instance de l'exception. Pour ce faire, nous devons utiliser le mot-clé catch.
bool
exceptionThrown = false
;
try
{
throw
new
Exception
('This is an exception'
);
} on
Exception
catch
(e) {
print('Exception thrown:
${e.toString()}
'
);
exceptionThrown = true
;
assert
('Exception: This is an exception'
== e.toString());
}
assert
(exceptionThrown);
Il est aussi possible d'attraper n'importe quel type d'exception. Il nous suffit de ne pas utiliser :
on
<Type>
par exemple :
bool
exceptionThrown = false
;
try
{
throw
new
Exception
();
} catch
(e) {
print('Something really unknown:
$e
'
);
exceptionThrown = true
;
}
assert
(exceptionThrown);
Bien évidemment il est possible de définir une série de catchs.
try
{
throw
new
MyError();
} on
MyError { // Exception attrapée ici
print('Too bad something unexpected happened'
);
} on
Exception
catch
(e) {
print('Unknown exception:
$e
'
);
} catch
(e) {
print('Something really unknown:
$e
'
);
}
class
MyError {
}
Dans cet exemple, l'exception de type MyError ne fait rien. Sa seule raison d'être est de pouvoir créer une « exception personnalisée ». Néanmoins, il est tout à fait possible de lui ajouter des paramètres (comme un message par exemple). Ceci permet de déterminer plus facilement l'origine du problème.
Pour terminer sur les exceptions, il est aussi possible de définir une partie de code qui sera exécutée qu'une exception soit levée ou non à l'aide de la clause finally.
Avec une exception :
bool
exceptionThrown = false
;
bool
wentThroughFinally = false
;
try
{
throw
new
MyError();
} catch
(e) {
exceptionThrown = true
;
} finally
{
wentThroughFinally = true
;
}
assert
(exceptionThrown);
assert
(wentThroughFinally);
Sans exception :
bool
exceptionThrown = false
;
bool
wentThroughFinally = false
;
int
sum = 0
;
try
{
sum = 10
+ 10
;
} catch
(e) {
exceptionThrown = true
;
} finally
{
wentThroughFinally = true
;
}
assert
(!exceptionThrown);
assert
(wentThroughFinally);
assert
(20
== sum);
3-9-18. noSuchMethod▲
En Dart, utiliser un champ ou une méthode d'une classe n'existant pas n'empêche pas l'exécution d'une application.
Quand un champ ou une méthode, qui n'existent pas au niveau de la classe courante, est appelé, Dart vérifie tout d'abord si ce champ ou cette méthode existe dans l'un de ses ancêtres. Si ce n'est pas le cas, Dart essaie de trouver une méthode appelée noSuchMethod() tout d'abord dans la classe puis dans ses ancêtres. Si cette méthode n'a pas été explicitement définie, Dart appelle cette méthode présente dans la classe Object, qui lève une exception NoSuchMethodError.
void
main() {
bool
exceptionThrown = false
;
try
{
MyClass myClass = new
MyClass();
myClass.doSomething(); // La method n'existe pas
} on
NoSuchMethodError {
exceptionThrown = true
;
}
assert
(exceptionThrown);
}
class
MyClass {
}
3-9-18-1. Invocation▲
La méthode noSuchMethod() prend en paramètre un objet de type Invocation. Pour savoir comment est valorisé cet objet en fonction de différents cas nous allons créer un cas de test très simple.
Créons tout d'abord une classe n'ayant que la méthode noSuchMethod().
class
MyClassWithNoSuchMethod {
dynamic
noSuchMethod(Invocation invocation) {
print('isAccessor:
${invocation.isAccessor}
'
);
print('isSetter:
${invocation.isSetter}
'
);
print('isGetter:
${invocation.isGetter}
'
);
print('isMethod:
${invocation.isMethod}
'
);
print('memberName:
${invocation.memberName}
'
);
print('positionalArguments:
${invocation.positionalArguments}
'
);
print('namedArguments:
${invocation.namedArguments}
'
);
}
}
Nous verrons comment définir un accesseur (getter ou setter) dans la partie 3.10 Bibliothèques, pour l'instant seul leur appel (présenté dans l'exemple) nous intéresse.
void
main() {
MyClassWithNoSuchMethod obj = new
MyClassWithNoSuchMethod();
print('------ Getter ------ obj.hello '
);
obj.hello;
print('------ Setter ------ obj.hello = 10'
);
obj.hello = 10
;
print('------ Method ------ obj.doSomething(25, clean:true)'
);
obj.doSomething(25
, clean:true
);
}
Affiche :
------ Getter ------ obj.hello
isAccessor: true
isSetter: false
isGetter: true
isMethod: false
memberName: Symbol("hello")
positionalArguments: []
namedArguments: {}
------ Setter ------ obj.hello = 10
isAccessor: true
isSetter: true
isGetter: false
isMethod: false
memberName: hello=
positionalArguments: [10]
namedArguments: {}
------ Method ------ obj.doSomething(25, clean:true)
isAccessor: false
isSetter: false
isGetter: false
isMethod: true
memberName: Symbol("doSomething")
positionalArguments: [25]
namedArguments: {Symbol("clean"): true}
3-9-18-2. Définir la méthode noSuchMethod()▲
Voyons un exemple montrant comment définir la méthode noSuchMethod().
import
'dart:mirrors'
;
class
User extends
DynaBean {
User(Map
<String
, Object
> user) : super
(user);
}
abstract
class
DynaBean {
Map
<String
, Object
> map;
DynaBean(this
.map);
dynamic
noSuchMethod(Invocation invocation) {
if
(invocation.isGetter) {
Symbol symbol = invocation.memberName;
String
getterName = MirrorSystem.getName(symbol);
return
this
.map[getterName];
} else
if
(invocation.isSetter) {
Symbol symbol = invocation.memberName;
String
setterName = MirrorSystem.getName(symbol).replaceAll('='
, ''
);
this
.map[setterName] = invocation.positionalArguments[0
];
} else
{
super
.noSuchMethod(invocation);
}
}
}
Le principe est très simple. En étendant notre DynaBean déjà vu précédemment, nous souhaitons pouvoir initialiser notre User à l'aide d'une Map (au format JSON) ainsi que récupérer, modifier, et rajouter des données.
Pour récupérer une valeur, nous utilisons un getter, et pour modifier ou rajouter un champ associé à une valeur nous utilisons un setter. Dans tous les autres cas, nous transférons l'action à notre parent (Object dans le cas présent).
En pratique, les choses sont un peu plus compliquées, puisque nous sommes obligés d'importer la bibliothèque dart:mirrors pour pouvoir récupérer le nom - sous la forme d'une chaîne de caractères - du Symbol retourné par invocation.memberName en utilisant MirrorSystem.getName(symbol).
void
main() {
User user = new
User({'username'
: 'Foo'
, 'password'
: 'secret'
});
assert
('Foo'
== user.username);
assert
('secret'
== user.password);
user.password = r'newPa$$word'
;
assert
(r'newPa$$word'
== user.password);
user.firstname = 'Bar'
;
assert
('Bar'
== user.firstname);
}
Si nous ne gérions pas le cas du setter, nous aurions créé un objet immuable (ou presque, puisque nous avons toujours accès au champ map. Ce problème disparaîtra dans les parties suivantes).
3-9-18-3. Délégation▲
Outre les champs que nous avons vus au cours des deux précédentes parties, nous pouvons déléguer des appels a des champs ou méthodes d'un attribut de la classe définissant la méthode noSuchMethod().
Pour ce faire, nous devons une nouvelle fois faire appel à la bibliothèque dart:mirrors, qui définit la classe InstanceMirror et la méthode de premier ordre reflect().
import
'dart:mirrors'
;
class
MyList {
List
<int
> list;
MyList() {
this
.list = new
List
();
}
int
get
sum => this
.list.reduce((p, e) => p + e);
dynamic
noSuchMethod(Invocation invocation) {
InstanceMirror mirror = reflect(this
.list);
return
mirror.delegate(invocation);
}
}
La classe List étant abstraite nous ne pouvons pas l'étendre sans avoir à implémenter toutes ses méthodes. Par conséquent, nous définissons un champ list de type List et si nous utilisons un champ ou une méthode inconnue de la classe MyList, nous transférons l'appel au champ list.
void
main() {
MyList list = new
MyList();
list.add(5
);
assert
(1
== list.length);
list.add(9
);
assert
(2
== list.length);
assert
(14
== list.sum);
}
Nous avons à présent l'impression d'utiliser une liste ayant une méthode supplémentaire. Malheureusement, en utilisant la méthode noSuchMethod() nous perdons les warnings de l'analyseur statique.
3-9-19. Cascade▲
L'opérateur cascade « .. » permet de chaîner les appels à un objet sans avoir à le réutiliser explicitement.
Sans l'opérateur cascade, devons à chaque fois réutiliser la variable sb.
void
main() {
int
age = 30
;
StringBuffer
sb = new
StringBuffer
();
sb.write('He is '
);
sb.write(age);
sb.write(' years old.'
);
assert
('He is 30 years old.'
== sb.toString());
}
Avec l'opérateur cascade, ce n'est plus le cas.
void
main() {
int
age = 30
;
// With casade
StringBuffer
sb = new
StringBuffer
()
..write('He is '
)
..write(++age)
..write(' years old.'
);
assert
('He is 31 years old.'
== sb.toString());
}
Note : en situation réelle l'utilisation d'un StringBuffer ne serait pas du tout adaptée, mais dans le cas présent, il permet d'illustrer simplement l'utilisation de l'opérateur.
3-9-20. Constantes▲
Pour éviter d'avoir à instancier de multiple fois le même objet, ce qui est consommateur de mémoire, ou pour créer un objet immuable, nous avons besoin de constantes. Dart propose deux types de constantes.
3-9-20-1. final▲
Le mot-clé final permet d'indiquer qu'une variable pointera toujours vers la même instance d'un objet.
final
Object
object = new
Object
();
Si vous essayez d'assigner une autre à la variable object, une erreur sera levée.
object = new
Object
(); // Erreur
En utilisant le mot-clé final, une variable doit être initialisée en même temps qu'elle est déclarée.
Le fait d'utiliser le mot-clé final empêche une variable d'avoir une nouvelle valeur, en revanche, les champs de cet objet sont toujours modifiables.
class
User {
String
firstName;
String
lastName;
User(String
this
.firstName, String
this
.lastName);
}
void
main() {
// The variable can't be reassigned but its fields can
final
User user = new
User('Foo'
, 'Bar'
);
assert
('Foo'
== user.firstName);
assert
('Bar'
== user.lastName);
user.firstName = 'aaa'
;
user.lastName = 'bbb'
;
assert
('aaa'
== user.firstName);
assert
('bbb'
== user.lastName);
}
Une solution pour rendre un objet immuable est de déclarer les champs « final ».
class
ImmutableUser {
final
String
firstName;
final
String
lastName;
ImmutableUser(String
this
.firstName, String
this
.lastName);
}
void
main() {
final
ImmutableUser immutableUser =
new
ImmutableUser('FooFoo'
, 'BarBar'
);
assert
('FooFoo'
== immutableUser.firstName);
assert
('BarBar'
== immutableUser.lastName);
// firstName et lastName sont "final" et par conséquent ne peuvent
// être modifiés
//immutableUser.firstName = 'aaa';
//immutableUser.lastName = 'bbb';
}
Déclarer des champs final en leur assignant une valeur dans le constructeur est la seule exception pour laquelle une variable peut être déclarée sans être initialisée. La raison est simple, en utilisant le sucre syntaxique, une valeur est assignée aux champs avant l'initialisation de la classe.
De ce fait, il est aussi possible d'assigner des valeurs aux champs final dans un préconstructeur.
class
ImmutableUser {
final
String
firstName;
final
String
lastName;
ImmutableUser.withDefault()
: this
.firstName = 'FooBar'
,
this
.lastName = 'BarFoo'
;
}
void
main() {
final
ImmutableUser immutableUserDefault =
new
ImmutableUser.withDefault();
assert
('FooBar'
== immutableUserDefault.firstName);
assert
('BarFoo'
== immutableUserDefault.lastName);
}
3-9-20-2. const▲
Le mot-clé const - placé au niveau du constructeur - impose la réutilisation de l'instance si le constructeur est appelé une deuxième fois avec les mêmes paramètres. Ceci est le rôle du mot-clé const.
class
Color {
final
String
name;
const
Color(this
.name);
}
À noter que tous les champs de la classe doivent être final.
L'instanciation de telles classes est légèrement différente de la manière habituelle. Nous utilisons const à la place de new :
void
main() {
final
Color blue1 = const
Color('blue'
);
final
Color blue2 = const
Color('blue'
);
assert
(blue1 == blue2);
assert
(identical(blue1, blue2));
}
Note : la méthode identical() permet de déterminer si deux objets ont la même référence. En utilisant ==, nous avons le même comportement puisque nous héritons de cet opérateur de la classe Object qui fait appel à cette méthode. Jusqu'à présent nous avons uniquement utilisé l'opérateur d'égalité puisque nous ne comparions que des types immuables (int, String, bool, etc.) qui sont comparés sur leur valeur primitive. Il est donc important d'éviter de définir une méthode nommée identical() avec deux paramètres de type Object pour éviter de surcharger la méthode de la classe Object. Pour l'opérateur d'égalité, les choses sont légèrement différentes puisque sa raison d'être est de déterminer si deux objets sont égaux en termes de contenu et pas forcément de référence, ce qui devrait être réservé à la méthode identical().
En utilisant, le mot-clé new, nous n'aurions pas eu le même résultat.
final
Color blue3 = new
Color('blue'
);
final
Color blue4 = new
Color('blue'
);
assert
(blue3 != blue4);
3-9-20-3. Énumérations▲
Dart n'ayant pas de type « Énumération » nous pouvons utiliser des constantes de type const.
class
Enum {
final
String
name;
const
Enum(this
.name);
String
toString() => name;
}
class
Unit extends
Enum {
static
const
Unit CM = const
Unit.internal('cm'
);
static
const
Unit PX = const
Unit.internal('px'
);
const
Unit.internal(String
name) : super
(name);
}
void
main() {
assert
(Unit.CM == Unit.CM);
assert
('cm'
== Unit.CM.toString());
}
Nous verrons dans la partie 3.10 Bibliothèques comment nous pouvons empêcher les utilisateurs de notre bibliothèque de définir de nouvelles valeurs.
3-9-21. Redéfinition d'opérateurs▲
En Dart certains opérateurs peuvent être redéfinis.
3-9-21-1. Opérateurs pouvant être redéfinis▲
< | + | | | [] | > | / | ^ | []= | <= | ~/ |
& | ~ | >= | * | << | == | - | % | >> | Â |
Imaginons que nous souhaitions effectuer des opérateurs arithmétiques sur un objet Point. Grâce à la redéfinition des opérateurs, il n'est pas nécessaire d'avoir des méthodes add(), subtract(), etc.
Redéfinir un opérateur est comparable à la déclaration d'une fonction à la différence que nous devons ajouter le mot-clé operator :
<Type de retour> operator <operateur>(Type Paramètre)
Le paramètre doit être unique et être du type de la classe redéfinissant l'opérateur.
class
Point {
int
x;
int
y;
Point(int
this
.x, int
this
.y);
Point operator
+(Point p) {
return
new
Point(x + p.x, y + p.y);
}
Point operator
-(Point p) {
return
new
Point(x - p.x, y - p.y);
}
}
Nous pouvons à présent utiliser les deux nouveaux opérateurs :
void
main() {
Point a = new
Point(2
, 3
);
Point b = new
Point(2
, 2
);
Point c = a + b;
assert
(c.x == 4
);
assert
(c.y == 5
);
}
Bien que redéfinir des opérateurs soit réservé à des cas bien particuliers, il y en a un que vous surchargerez constamment, l'opérateur d'égalité.
Outre le fait de pouvoir comparer si deux objets ont plusieurs champs en commun, surcharger cet opérateur nous permettra d'utiliser des méthodes des classes de type List, telles que la méthode contains() que nous avions dû créer dans un exemple précédent.
class
User {
String
username;
String
password;
User(this
.username, this
.password);
bool
operator
==(User other) {
if
(other == null
) return
false
;
if
(this
.username != other.username) return
false
;
if
(this
.password != other.password) return
false
;
return
true
;
}
}
À présent, si les variables username et password de deux objets sont égales, les objets sont égaux, alors que les instances sont différentes :
void
main() {
User user1 = new
User('root'
, 'secure'
);
User user2 = new
User('root'
, 'secure'
);
assert
(!identical(user1, user2));
assert
(user1 == user2);
}
Nous pouvons ensuite reprendre notre exemple de LoginService :
class
LoginService {
static
List
<User> USERS_DB = [new
User('root'
, 'secure'
)];
bool
exists(User user) {
if
(USERS_DB.contains(user)) {
return
true
;
} else
{
return
false
;
}
}
}
void
main() {
LoginService loginService = new
LoginService();
bool
existsYes = loginService.exists(user1);
assert
(existsYes);
bool
existsNo = loginService.exists(new
User('guest'
, 'secure'
));
assert
(!existsNo);
}
3-9-22. Mixins▲
Grâce aux Mixins, Dart permet d'avoir un semblant d'héritage multiple. Néanmoins, pour pouvoir être utilisée en tant que Mixin, une classe doit respecter les trois principes suivants :
- elle ne doit pas déclarer de constructeur ;
- elle doit hériter de Object (implicitement ou explicitement) ;
- elle ne doit pas avoir d'appel utilisant super.
De plus, la classe utilisant une Mixin doit hériter d'une superclasse (Object ou autre). L'utilisation d'une Mixin se fait à l'aide du mot-clé with.
class
MyClass extends
Object
with
MyMixin {
}
Voyons un exemple simple. Nous définissions tout d'abord un simple Value Object.
class
Student {
String
firstName;
String
lastName;
Student(String
this
.firstName, String
this
.lastName);
String
getFullName() {
return
'
$firstName
$lastName
'
;
}
}
Nous créons ensuite un objet, qui sera utilisé en tant que Mixin, en respectant les trois principes présentés précédemment.
import
'dart:mirrors'
;
class
MyList {
List
list = new
List
();
dynamic
noSuchMethod(Invocation invocation) {
InstanceMirror mirror = reflect(this
.list);
return
mirror.delegate(invocation);
}
}
Passons à présent à l'utilisation de la classe MyList en tant que Mixin :
class
StudentManager extends
Object
with
MyList {
}
Et pour finir le test :
void
main() {
StudentManager studentManager = new
StudentManager();
studentManager.add(new
Student('Foo'
, 'Bar'
));
assert
(1
== studentManager.size());
}
Comme nous pouvons le constater, la classe StudentManager a accès à toutes les méthodes de la Mixin MyList.
3-10. Bibliothèques▲
Outre le fait de pouvoir avoir une application composée de multiples fichiers, comme de nombreux langages, Dart permet d'utiliser des bibliothèques externes, mais aussi de découper son code en bibliothèques.
3-10-1. Importer une bibliothèque▲
Jusqu'à présent presque toutes les fonctions (print()) et classes (int, List, etc.) que nous avons utilisées font partie de la bibliothèque dart:core qui n'a pas besoin d'être importée, tel que le package java.lang en Java.
Mais pour pouvoir utiliser toute fonctionnalité d'une autre bibliothèque, il est nécessaire de l'importer, comme nous l'avons fait plusieurs fois avec la bibliothèque dart:mirrors. Pour ce faire nous avons à notre disposition le mot-clé import.
import
'dart:math'
;
Nous pouvons à présent utiliser la fonction de haut niveau max() définie dans la bibliothèque dart:math appartenant à l'API standard de Dart.
void
main() {
assert
(10
== max(10
, 5
));
}
3-10-2. Aliases▲
Tel quel, nous n'avons aucun moyen d'identifier à quelle bibliothèque appartient la fonction max(). Pour pallier ce problème nous pouvons utiliser un alias pour la bibliothèque dart:math, à l'aide du mot-clé as.
import
'dart:math'
as
Math
;
À présent, pour pouvoir utiliser la fonction max() nous devons la préfixer avec l'alias suivi d'un point, telle une méthode statique d'une classe.
void
main() {
assert
(10
== Math
.max(10
, 5
));
}
Outre le fait de pouvoir identifier à quelle bibliothèque appartient une fonction ou une classe, les alias ont pour principal intérêt d'éviter les conflits entre plusieurs bibliothèques utilisant des fonctions ou classes ayant le même nom.
Si une bibliothèque est aliasée, l'alias doit être utilisé à chaque fois que l'on utilise un élément de cette bibliothèque.
3-10-3. Créer une bibliothèque▲
Créer une bibliothèque n'a rien de compliqué. Il suffit d'ajouter au début d'un fichier .dart le mot-clé library suivi d'un nom. Cette déclaration doit être la première ligne de code. Contrairement à Java, il n'y a aucune relation entre le nom d'un fichier, la structure des répertoires le contenant et le nom d'une bibliothèque.
En reprenant, l'exemple du Logger créé dans la partie 3.9.15 Factories :
library
myFirstLib;
class
Logger {
//..
}
Utiliser cette bibliothèque n'a rien de compliqué :
// uses_simple_library.dart
import
'simple_library.dart'
;
void
main() {
Logger logger = new
Logger('user'
);
logger.log("I'm here"
);
}
Contrairement aux imports de l'API de Dart, nous importons à présent un fichier, en utilisant un chemin relatif.
Sources :
3-10-4. Scope▲
Le fait de pouvoir interdire l'utilisation de certains fonctions, classes, champs ou méthodes est l'un des principes fondamentaux de la programmation objet nommé encapsulation. Contrairement à des langages comme C# et Java pour lesquels la notion de confidentialité (privacy) est au niveau d'une classe (voire d'un package), en Dart, elle existe uniquement au niveau d'une bibliothèque. Par conséquent, tout élément défini comme privé est accessible à l'ensemble de la bibliothèque dans laquelle il est défini, mais pas à une bibliothèque ou application l'utilisant.
Pour définir quelque chose de privé, il suffit de préfixer son nom par un underscore (_).
Partons à nouveau de notre exemple de Logger.
library
mySecondLib;
class
Logger {
final
String
_name;
bool
isEnabled = true
;
static
final
Map
<String
, Logger> _cache = <String
, Logger>{};
factory
Logger(String
name) {
if
(_cache.containsKey(name)) {
return
_cache[name];
} else
{
final
logger = new
Logger._internal(name);
_cache[name] = logger;
return
logger;
}
}
Logger._internal(this
._name);
void
log(String
msg) {
if
(!isEnabled) {
new
_Printer('
$_name
-
$msg
'
)._print();
}
}
}
class
_Printer {
String
_text;
_Printer(String
this
._text);
void
_print() {
_doPrint(_text);
}
}
void
_doPrint(String
string) {
print(string);
}
Le code s'est légèrement compliqué pour que l'on puisse étudier les différents éléments de code pouvant être « privés ».
import
'privacy_library.dart'
;
void
main() {
Logger logger = new
Logger('user'
);
logger.log("I'm here"
);
//---------------------------
// Les lignes suivantes essayent d'utiliser des éléments
// privés, ce qui n'est pas possible à l'extérieur d'une
// bibliothèque.
//---------------------------
// Champ d'instance privé
//print(logger._name);
// Champ statique privé
//print(Logger._cache);
// Constructeur privé
//new Logger._internal('Test');
// Classe privée
//new _Printer('Test');
// Fonction de haut niveau privée
//_doPrint('Test');
}
Sources :
3-10-5. Getters et Setters▲
En continuant à suivre l'un des principes les plus importants de la programmation objet, tous les champs d'instance doivent être privés. Tout accès à ces champs doit se faire à l'aide de getters et setters. Des getters et setters sont des méthodes permettant l'accès en lecture et en écriture à des champs d'une classe.
Dart permet de définir des getters et des setters simplement grâce aux mots-clés get et set :
class
Logger {
final
String
_name;
bool
_isEnabled = false
;
//...
bool
get
enabled => _isEnabled;
set
enabled(bool
isEnabled) => _isEnabled = isEnabled;
}
La construction est la suivante :
// Getters
<type de retour> get
nom_de_laccesseur => _champ_privé;
<type de retour> get
nom_de_laccesseur {
return
_champ_privé
}
// Setters
set
nom_de_laccesseur(<type> param) => _champ_privé = param;
void
get
nom_de_laccesseur(<type> param) {
_champ_privé = param ;
}
L'appel à ces accesseurs donne l'impression d'utiliser des champs directement :
import
'getset_library.dart'
;
void
main() {
Logger logger = new
Logger('getset_library'
);
assert
(!logger.enabled);
logger.log("Doesn't print anything"
);
logger.enabled = true
;
logger.log("I'm here"
);
}
Il est aussi possible de définir des getters et setters abstraits :
abstract
class
 MyAbstractClass {
int
get
age;
void
set
age;
}
Sources :
3-10-6. Multifichiers▲
Avoir une bibliothèque composée d'un seul fichier est assez rare.
Pour faire les choses simplement, nous allons utiliser l'exemple du service de Login. Chaque classe sera dans un fichier et les méthodes utilitaires seront extraites dans une autre bibliothèque nommée « utils».
Notre miniapplication sera organisée de la manière suivante :
multi
|-- app
| `-- uses_multifiles_lib.dart
`-- lib
|-- security
| |-- src
| | |-- data.dart
| | |-- processing.dart
| | |-- service.dart
| | `-- validation.dart
| `-- security.dart
`-- utils
|-- src
| `-- string_utils.dart
`-- utils.dart
Commençons par la bibliothèque utils. Dans le fichier utils.dart, nous devons tout d'abord indiquer de quels fichiers elle est composée à l'aide du mot-clé part. Étant donné qu'il n'y en a qu'un, les choses sont simples. Sans surprise, nous retrouvons le mot-clé library associé à un identifiant.
library
utils;
part
'src/string_utils.dart'
;
Le fichier string_utils.dart, quant à lui, doit indiquer à quelle bibliothèque il appartient.
part
of
utils;
bool
isEmpty(String
s) {
return
s == null
;
}
//...
Nous pouvons ensuite passer à la bibliothèque security.
library
security;
import
'../utils/utils.dart'
;
part
'src/data.dart'
;
part
'src/processing.dart'
;
part
'src/service.dart'
;
part
'src/validation.dart'
;
Notes
- La bibliothèque security importe la bibliothèque utils (en utilisant le chemin relatif menant au fichier contenant le mot-clé library).
- Les imports ne doivent être faits que dans le fichier contenant le mot-clé library.
- L'ordre des déclarations ne peut être modifié (library, import et part).
Les quatre fichiers composant la bibliothèque security, n'ayant rien de particulier, ils n'ont pas été intégrés dans ce document. Je vous invite à consulter le code pour voir l'exemple complet, qui intègre tous les éléments du langage nécessaires à en faire une application suivant les principes de la programmation orientée objet.
Le fichier contenant la fonction main() est composé de la manière suivante :
library
myApp;
import
'../lib/security/security.dart'
;
void
main() {
// ...
}
3-10-7. Réexporter une bibliothèque▲
En Dart, il est possible de combiner et de repackager des bibliothèques en les réexportant en partie ou en totalité. Par exemple, vous pouvez avoir une bibliothèque de taille imposante que vous souhaitez découper en de petites bibliothèques. Ou vous pouvez créer une bibliothèque qui fournit un sous-ensemble des méthodes d'une autre bibliothèque. L'intérêt est somme toute limité.
library
completeLib;
hello() => print('Bonjour!'
);
goodbye() => print('Au Revoir!'
);
// partial_lib.dart
library
partialLib;
import
'complete_lib.dart'
;
export
'complete_lib.dart'
show
hello;
// using_partial_lib.dart
import
'partial_lib.dart'
;
void
main() {
hello(); //Affiche 'Bonjour!'
goodbye(); //Erreur
}
3-10-8. Bibliothèques externes▲
Utiliser des bibliothèques externes nécessite pub. Or Pub ne faisant pas à proprement parler du langage, nous aborderons ce sujet ultérieurement. En attendant, je vous invite à consulter la documentation officielle à ce sujet.
3-11. Variables Globales▲
Une variable globale est comparable à une variable statique. La discussion de ce sujet a été différée jusqu'à la fin de cette partie puisqu'il est contraire au concept de Programmation Orienté Objet. Il est donc déconseillé de les utiliser.
Déclarer une variable globale est identique à la déclaration d'une variable locale.
Conclusion▲
Au terme de notre exploration de Dart en tant que langage, force est de constater qu'il a été conçu avec pour objectif de permettre à un panel varié de développeurs de l'utiliser très rapidement. Néanmoins, la liberté offerte par le langage est piégeuse. La possibilité « de faire n'importe quoi » devra être considérée comme un point d'attention de premier plan et des règles précises, voire restrictives, devront être définies en amont de la phase de développement. Ceci aura pour effet de limiter toute dérive dans le style de codage.
Dart est indéniablement une bonne idée. Le fait d'avoir un langage structuré et orienté objet, accompagné d'une Machine Virtuelle cliente s'exécutant dans un navigateur est une avancée dans la recherche de productivité. Mais attention de ne pas confondre productivité et mauvaise qualité.
En revanche, Google frôle la ligne rouge ! Si un effort sur la qualité de l'API et des outils n'est pas fait dans les prochains mois, donnant un signal de confiance aux early adopters, nous pouvons émettre des doutes quant à son futur. De plus, de nombreuses voix commencent à s'élever en raison de l'absence d'une bibliothèque de widgets à la sortie de la version 1.0, réduisant Dart à un simple langage avec une API extrêmement basique, et par conséquent inutilisable dans le monde de l'entreprise.
Si Dart ne prend pas une longueur d'avance, l'ECMAScript 6 pointant le bout de son nez, son intérêt sera plus que discutable. De plus, en partant du principe que tous les navigateurs implémenteront cette nouvelle version de JavaScript, à fonctionnalité équivalente, un langage ne nécessitant pas d'être compilé et pouvant être utilisé avec des outils/bibliothèques existants aura généralement la préférence des développeurs. En tout cas pour ceux n'ayant pas une aversion pour le JavaScript. Ces derniers continueront à se contenter de frameworks permettant de créer des IHM très simplement grâce à des widgets, ainsi que de nombreux outils généralement disponibles « out of the box ».
Nous pouvons aussi nous poser la question de la pertinence de continuer à utiliser un langage impératif pour faire du Web. Des langages basés sur le FRP (Functional Reactive Programming) tels que Elm, offrent une solution intéressante méritant toute notre attention.
Remerciements▲
Nous tenons à remercier Mickael Baron pour sa relecture technique et Claude Leloup pour ses corrections orthographiques.