Un meilleur job mieux payé ?

Deviens chef de projet, développeur, ingénieur, informaticien

Mets à jour ton profil pro

ça m'intéresse

Cours sur Dart le langage orienté Web de Google

Image non disponible

Dart, le nouveau langage orienté Web de Google, a été dévoilé au monde en octobre 2011. Il se veut un langage structuré, non pas révolutionnaire, mais facile d'apprentissage pour tout développeur, quel que soit son background (C#, Java ou JavaScript) puisque Dart est un agrégat de ces trois langages avec d'autres, tel que Smalltalk. Ce nouveau langage a pour objectif d'être tout ce qu'aurait pu être JavaScript s'il avait été inventé aujourd'hui. En d'autres termes, garder la nature dynamique du JavaScript, tout en offrant un langage et des outils facilitant le développement de grosses applications Web.

Pour réagir à ce support de cours, un espace de dialogue vous est proposé sur le forum : 7 commentaires Donner une note à l'article (5)

Article lu   fois.

L'auteur

Profil ProSite personnel

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

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

Image non disponible

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) :
Image non disponible

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

pubspec.yaml
Sélectionnez
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 (/** */).

 
Sélectionnez
/// 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 :

Image non disponible
  • 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.

Image non disponible

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 :

 
Sélectionnez
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?).

Image non disponible

Dans la console, nous voyons le message issu de la fonction print().

 
Sélectionnez
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…
    Image non disponible
  • puis dans Script arguments, ajoutez les paramètres que vous souhaitez fournir à votre application :
    Image non disponible
    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 :

 
Sélectionnez
1.
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.

Image non disponible

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.

helloweb.html
Sélectionnez
<!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 ») :

 
Sélectionnez
<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) :

 
Sélectionnez
<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 :

 
Sélectionnez
<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.

helloweb.dart
Sélectionnez
1.
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 :

 
Sélectionnez
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 :

variables/variables.dart
Sélectionnez
var name = 'Bob';
var age = 30;

Par défaut toutes les variables non initialisées ont pour valeur null.

variables/variables.dart
Sélectionnez
var uninitialized;
assert(uninitialized == null);

Source

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.

types/strings.dart
Sélectionnez
// 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 :

types/strings.dart
Sélectionnez
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.

types/strings.dart
Sélectionnez
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 :

types/strings.dart
Sélectionnez
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 (+).

 
Sélectionnez
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.

types/strings.dart
Sélectionnez
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.

types/strings.dart
Sélectionnez
print('''With
      triple simple quotes 
      I can define
      a string
      over multiple
      lines
      ''');
print("""And with
triple double
quotes 
""");

Affiche

types/strings.dart
Sélectionnez
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 « »"

types/strings.dart
Sélectionnez
print(r'Hello \n $singleQuotes ${doubleQuotesWithType.toUpperCase()}');

affiche :

 
Sélectionnez
Hello \n $singleQuotes ${doubleQuotesWithType.toUpperCase()}

Source

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)

types/numbers.dart
Sélectionnez
// 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;

Source

3.4.3. Booléens

Pour représenter des booléens, Dart a le type bool.

types/booleans.dart
Sélectionnez
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.

types/booleans.dart
Sélectionnez
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.

Source

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 :

types/lists.dart
Sélectionnez
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.

 
Sélectionnez
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 :

types/lists.dart
Sélectionnez
fixedList[0] = 1;
fixedList[1] = true;
fixedList[2] = 'String';
fixedList[3] = 5.6e5;

Rajouter un élément à cette liste provoquera une exception :

types/lists.dart
Sélectionnez
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.

types/lists.dart
Sélectionnez
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.

types/lists.dart
Sélectionnez
List dynamicList = new List();

Pour ajouter des éléments, nous devons utiliser la méthode add().

types/lists.dart
Sélectionnez
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.

types/lists.dart
Sélectionnez
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.

types/lists.dart
Sélectionnez
List<int> genericsList = new List();
genericsList.add(1);

Néanmoins, rien ne nous empêche d'écrire :

types/lists.dart
Sélectionnez
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 :

types/lists.dart
Sélectionnez
List multiDimensionList = [[10, 20, 30], [40, 50, 60]];
print('${multiDimensionList[0][1]} ${multiDimensionList[1][2]}');

Source

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 :

types/maps.dart
Sélectionnez
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 :

types/maps.dart
Sélectionnez
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 :

types/maps.dart
Sélectionnez
Map<String, int> genericMap = new Map();
genericMap['one'] = 1;
genericMap[2] = '2';
print(genericMap);

Source

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 :

operators/arithmetic_operators.dart
Sélectionnez
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);

Source

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 :

operators/equality_relational_operators.dart
Sélectionnez
assert(1 == 1);
assert(1 != 2); 
assert(1 > 0); 
assert(0 < 1);
assert(1 >= 0); 
assert(1 >= 1); 
assert(0 <= 1);
assert(0 <= 0);

Source

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 :

operators/type_test_operators.dart
Sélectionnez
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);

Source

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 :

operators/logical_operators.dart
Sélectionnez
assert(!false);
  
var a = true;
var b = false;
assert(a && !b);
assert(a || b);
assert(b || a && !b); // && a une priorité supérieure à ||

Source

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 :

operators/bitwise_shift_operators.dart
Sélectionnez
// 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

Source

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.

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 /* … */).

comments/code_comments.dart
Sélectionnez
// Ceci est un commentaire d'une ligne.

/*
 * Ceci est 
 * un commentaire 
 * s'étendant sur plusieurs lignes.
 */

Source

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 /** … */). :

comments/doc_comments.dart
Sélectionnez
/// 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) :

 
Sélectionnez
$ dartdoc <répertoire>

Le répertoire de documentation (nommé docs) est généré à la racine du répertoire courant.

Source

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.

loops/for_loops.dart
Sélectionnez
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);

Source

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 :

loops/forin_loops.dart
Sélectionnez
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);

Source

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.

loops/foreach_loops.dart
Sélectionnez
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);

Source

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.

loops/dowhile_loops.dart
Sélectionnez
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);

Source

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é.

conditions/ifelse_conditions.dart
Sélectionnez
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]');

Source

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.

conditions/switch_conditions.dart
Sélectionnez
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.

Source

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.

loops/break_loops.dart
Sélectionnez
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.

loops/break_loops.dart
Sélectionnez
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.

Source

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 »).

conditions/continue_switch.dart
Sélectionnez
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;
    }
  }
}

Source

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.

functions/top_level_functions.dart
Sélectionnez
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;
}

Source

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 :

functions/first_class_functions.dart
Sélectionnez
Function objFunc = square;

Nous pouvons ensuite, soit appeler la fonction square() en passant par la variable objFunc :

functions/first_class_functions.dart
Sélectionnez
int squaredNum = objFunc(2);

soit passer la variable objFunc en paramètre d'une autre fonction :

functions/first_class_functions.dart
Sélectionnez
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 :

functions/first_class_functions.dart
Sélectionnez
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 :

functions/first_class_functions.dart
Sélectionnez
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 :

functions/first_class_functions.dart
Sélectionnez
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);
}
functions/first_class_functions.dart
Sélectionnez
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 :

 
Sélectionnez
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.

Source

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

functions/anonymous_functions.dart
Sélectionnez
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.

 
Sélectionnez
Function sayHello = null;
sayHello = () {
  print('Hello');
  sayHello();    // Provoque une boucle infinie
};

Source

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.

functions/standard_local_functions.dart
Sélectionnez
void main() {
  square(int i) {
    return  i * i;
  }

  int squaredNum = square(2);
  assert(squaredNum == 4);
}

Source

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.

functions/shorthand_functions.dart
Sélectionnez
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.

functions/shorthand_functions.dart
Sélectionnez
Function returnNull = () => print('This function return null');
assert(null == returnNull());

C'est aussi le cas d'une fonction « retournant » void :

functions/shorthand_functions.dart
Sélectionnez
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.

functions/shorthand_functions.dart
Sélectionnez
void main() => print('hello');

Source

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.

functions/closures_functions.dart
Sélectionnez
1.
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.

Source

3.8.5.1. Imbrication de closures

Il est aussi possible d'imbriquer les closures. L'exemple suivant utilise la forme simplifiée.

functions/imbricated_functions.dart
Sélectionnez
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 :

functions/imbricated_functions.dart
Sélectionnez
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.

Source

3.8.5.2. Closure hell

Une closure peut aussi être une fonction retournant une autre fonction définie dans son scope :

functions/closures_hell.dart
Sélectionnez
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.

Source

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 :

functions/wrapped_closures.dart
Sélectionnez
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 :

functions/wrapped_closures.dart
Sélectionnez
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().

functions/wrapped_closures.dart
Sélectionnez
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 :

functions/wrapped_closures.dart
Sélectionnez
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.

functions/wrapped_closures.dart
Sélectionnez
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().

functions/wrapped_closures.dart
Sélectionnez
num sum = execute(adder(right, left));
assert(5 == sum);

Source

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.

functions/typedef_functions.dart
Sélectionnez
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) :

functions/typedef_functions.dart
Sélectionnez
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;
}

Source

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.

functions/optional_positional_parameters.dart
Sélectionnez
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.

functions/optional_positional_parameters.dart
Sélectionnez
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.

functions/optional_positional_parameters.dart
Sélectionnez
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.

Source

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.

functions/optional_named_parameters.dart
Sélectionnez
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.

Source

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.

functions/optional_typing_functions.dart
Sélectionnez
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.

functions/optional_typing_functions.dart
Sélectionnez
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;

Source

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 :

functions/wrapup1_functions.dart
Sélectionnez
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().

functions/wrapup1_functions.dart
Sélectionnez
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)

functions/wrapup2_functions.dart
Sélectionnez
1.
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 :

 
Sélectionnez
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.

functions/wrapup3_functions.dart
Sélectionnez
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.

oop/simple_class.dart
Sélectionnez
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 :

oop/simple_class.dart
Sélectionnez
person.firstName;

Après instanciation, les deux champs sont null :

oop/simple_class.dart
Sélectionnez
assert(null == person.firstName);
assert(null == person.lastName);

Nous allons donc leur assigner une valeur :

oop/simple_class.dart
Sélectionnez
person.firstName = 'Foo';
person.lastName = 'Bar';

3.9.1.1. Fonction main complète

oop/simple_class.dart
Sélectionnez
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());
}

Source

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 :

oop/simple_constructor.dart
Sélectionnez
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.

oop/simple_constructor.dart
Sélectionnez
Person person = new Person('Foo', 'Bar');

Les champs firstName et lastName étant toujours modifiables :

oop/simple_constructor.dart
Sélectionnez
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.

oop/simple_constructor.dart
Sélectionnez
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());
}

Source

3.9.3. Un constructeur avec un sucre, svp !

Dart nous permet de simplifier la définition d'un constructeur :

oop/sugar_constructor.dart
Sélectionnez
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.

oop/sugar_constructor.dart
Sélectionnez
Person(String this.firstName, String this.lastName);

Source

3.9.4. Écriture standard et sucre syntaxique

Dart permet de mixer l'écriture des paramètres d'un constructeur :

oop/mixed_constructor.dart
Sélectionnez
Person(String firstName, String this.lastName) {
  this.firstName = firstName;
}

ou

oop/mixed_constructor.dart
Sélectionnez
Person(String this.firstName, String lastName) {
  this.lastName = lastName;
}

Source

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.

oop/named_constructor.dart
Sélectionnez
// 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 :

oop/named_constructor.dart
Sélectionnez
Person personUnknown = new Person();
Person personInitialized = new Person.withNames('Foo', 'Bar');

Source

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.

oop/optional_parameters_constructor.dart
Sélectionnez
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'});

Source

3.9.7. Préconstructeur

Dart permet d'initialiser des champs avant l'exécution du corps du constructeur :

oop/pre_constructor.dart
Sélectionnez
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.

Source

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

oop/instance_methods.dart
Sélectionnez
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.

oop/instance_methods.dart
Sélectionnez
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 :

oop/instance_methods.dart
Sélectionnez
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.

oop/instance_methods.dart
Sélectionnez
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.

oop/instance_methods.dart
Sélectionnez
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.

Source

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 :

oop/static_fields.dart
Sélectionnez
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 :

oop/static_fields.dart
Sélectionnez
Point.counter 

La valeur de cette variable pouvant être bien évidemment modifiée.

oop/static_fields.dart
Sélectionnez
Point.counter = 50;

Vous trouverez ci-dessous un exemple plus complet :

oop/static_fields.dart
Sélectionnez
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++;
  }
}

Source

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.

oop/static_methods.dart
Sélectionnez
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 :

oop/static_methods.dart
Sélectionnez
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.

Source

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.

oop/static_none_static.dart
Sélectionnez
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;
  }
}

Source

3.9.10. Héritage

Dart permet l'héritage simple. Le mot-clé extends est réservé à cet effet.

oop/instance_inheritance.dart
Sélectionnez
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.

oop/instance_inheritance.dart
Sélectionnez
//---- 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 :

oop/instance_inheritance.dart
Sélectionnez
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.

Source

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.

oop/constructor_inheritance.dart
Sélectionnez
NomDeLaClasse(&lt;paramètres>) : super&lt;paramètres du parent>);

Ou

oop/constructor_inheritance.dart
Sélectionnez
NomDeLaClasse(&lt;paramètres>) : super(&lt;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.

oop/constructor_inheritance.dart
Sélectionnez
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.

oop/constructor_inheritance.dart
Sélectionnez
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.

oop/constructor_inheritance.dart
Sélectionnez
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).

oop/constructor_inheritance.dart
Sélectionnez
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.

Source

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.

oop/static_inheritance.dart
Sélectionnez
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;
  }
}

Source

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.

oop/methods_overloading.dart
Sélectionnez
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.

Source

3.9.12. Classes abstraites

Une classe abstraite est déclarée à l'aide du mot-clé abstract.

oop/abstract_classes.dart
Sélectionnez
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.

oop/abstract_classes.dart
Sélectionnez
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 :

oop/abstract_classes.dart
Sélectionnez
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();
}

Source

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.

oop/abstract_classes_inheritance.dart
Sélectionnez
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.

Source

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.

oop/simple_interfaces.dart
Sélectionnez
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.

oop/simple_interfaces.dart
Sélectionnez
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 :

oop/simple_interfaces.dart
Sélectionnez
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 :

oop/simple_interfaces.dart
Sélectionnez
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().

oop/simple_interfaces.dart
Sélectionnez
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.

Source

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.

oop/mock_interfaces.dart
Sélectionnez
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.

oop/mock_interfaces.dart
Sélectionnez
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.

Source

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.

oop/default_impl_factory.dart
Sélectionnez
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).

oop/default_impl_factory.dart
Sélectionnez
void main() {
  Validator validator = new Validator();
  bool isValid = validator.validate(new User('', ''));
  assert(isValid);
}

Source

3.9.15.2. Caching

Outre le fait de proposer une implémentation par défaut, les factories introduisent une notion de caching.

oop/caching_factory.dart
Sélectionnez
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).

oop/caching_factory.dart
Sélectionnez
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.

Source

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.

oop/generics.dart
Sélectionnez
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).

oop/generics.dart
Sélectionnez
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().

oop/generics.dart
Sélectionnez
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 :

oop/generics.dart
Sélectionnez
abstract class Validatable {
  List<Object> valuesToValidate();
}

Et de l'ajouter aux classes Validator et User :

oop/generics.dart
Sélectionnez
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.

oop/generics.dart
Sélectionnez
void main() {
  //---- Validator
  Validator validator = new Validator();
  bool isValid = validator.validate(new User('', ''));
  assert(isValid);
}

Source

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.

oop/exceptions.dart
Sélectionnez
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.

oop/exceptions.dart
Sélectionnez
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 :

 
Sélectionnez
on <Type>

par exemple :

oop/exceptions.dart
Sélectionnez
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.

oop/exceptions.dart
Sélectionnez
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 :

oop/exceptions.dart
Sélectionnez
bool exceptionThrown = false;
bool wentThroughFinally = false;

try {
  throw new MyError();
} catch(e) {
  exceptionThrown = true;
} finally {
  wentThroughFinally = true;
}

assert(exceptionThrown);
assert(wentThroughFinally); 

Sans exception :

oop/exceptions.dart
Sélectionnez
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);

Source

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.

oop/intro_noSuchMethod.dart
Sélectionnez
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().

oop/intro_noSuchMethod.dart
Sélectionnez
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.

oop/intro_noSuchMethod.dart
Sélectionnez
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 :

 
Sélectionnez
------ 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}

Source

3.9.18.2. Définir la méthode noSuchMethod()

Voyons un exemple montrant comment définir la méthode noSuchMethod().

oop/defining_noSuchMethod.dart
Sélectionnez
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).

oop/defining_noSuchMethod.dart
Sélectionnez
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).

Source

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().

oop/delegate_noSuchMethod.dart
Sélectionnez
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.

oop/delegate_noSuchMethod.dart
Sélectionnez
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.

Source

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.

oop/cascade.dart
Sélectionnez
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.

oop/cascade.dart
Sélectionnez
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.

Source

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.

oop/final.dart
Sélectionnez
final Object object = new Object();

Si vous essayez d'assigner une autre à la variable object, une erreur sera levée.

oop/final.dart
Sélectionnez
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.

oop/final.dart
Sélectionnez
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 ».

oop/final.dart
Sélectionnez
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.

oop/final.dart
Sélectionnez
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);
}

Source

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.

oop/const.dart
Sélectionnez
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 :

oop/const.dart
Sélectionnez
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.

oop/const.dart
Sélectionnez
final Color blue3 = new Color('blue');
final Color blue4 = new Color('blue');
assert(blue3 != blue4);

Source

3.9.20.3. Énumérations

Dart n'ayant pas de type « Énumération » nous pouvons utiliser des constantes de type const.

oop/enumerations.dart
Sélectionnez
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);
}
oop/enumerations.dart
Sélectionnez
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.

Source

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.

oop/operators_overriding.dart
Sélectionnez
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 :

oop/operators_overriding.dart
Sélectionnez
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);
}

Source

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.

oop/egality_overloading.dart
Sélectionnez
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 :

oop/egality_overloading.dart
Sélectionnez
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 :

oop/egality_overloading.dart
Sélectionnez
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);
}

Source

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.

oop/mixins.dart
Sélectionnez
class MyClass extends Object with MyMixin {
}

Voyons un exemple simple. Nous définissions tout d'abord un simple Value Object.

oop/mixins.dart
Sélectionnez
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.

oop/mixins.dart
Sélectionnez
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 :

oop/mixins.dart
Sélectionnez
class StudentManager extends Object with MyList {
}

Et pour finir le test :

oop/mixins.dart
Sélectionnez
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.

Source

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.

libraries/imports.dart
Sélectionnez
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.

libraries/imports.dart
Sélectionnez
void main() {
  assert(10 == max(10, 5));
}

Source

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.

libraries/aliases.dart
Sélectionnez
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.

libraries/aliases.dart
Sélectionnez
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.

Source

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 :

libraries/simple_library/simple_library.dart
Sélectionnez
library myFirstLib;

class Logger {
  //..
} 

Utiliser cette bibliothèque n'a rien de compliqué :

libraries/simple_library/uses_simple_library.dart
Sélectionnez
// 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.

libraries/privacy/privacy_library.dart
Sélectionnez
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 ».

libraries/privacy/uses_privacy_library.dart
Sélectionnez
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 :

libraries/getset/getset_library.dart
Sélectionnez
class Logger {
  final String _name;
  bool _isEnabled = false;
  
  //...
  
  bool get enabled => _isEnabled;
  
       set enabled(bool isEnabled) => _isEnabled = isEnabled;
}

La construction est la suivante :

 
Sélectionnez
// 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 :

libraries/getset/uses_getset_library.dart
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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.

libraries/multi/lib/utils/utils.dart
Sélectionnez
library utils;

part 'src/string_utils.dart';

Le fichier string_utils.dart, quant à lui, doit indiquer à quelle bibliothèque il appartient.

libraries/multi/lib/utils/src/string_utils.dart
Sélectionnez
part of utils;

bool isEmpty(String s) {
  return s == null;
}

//...

Nous pouvons ensuite passer à la bibliothèque security.

libraries/multi/lib/security/security.dart
Sélectionnez
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 :

libraries/multi/app/uses_multifiles_lib.dart
Sélectionnez
library myApp;

import '../lib/security/security.dart';

void main() {
  // ...
}

Sources

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é.

libraries/reexport/complete_lib.dart
Sélectionnez
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
}

Sources

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.

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

Copyright © 2013 Soat. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.