IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Présentation du langage NICE


précédentsommairesuivant

III. Et bien plus encore…

Après avoir montré que Nice était relativement proche de Java et que des classes Nice pouvaient interagir avec des classes Java, je vous propose maintenant une partie sur des aspects propres au langage Nice.

III-A. La sécurité et l'efficacité avant tout

Comme cela a déjà été évoqué, Nice fait un effort important sur les aspects sécurité et efficacité de programmation. En détectant des erreurs, qui apparaissent en général lors de l'exécution, à la compilation, Nice propose à la fois d'accompagner le développeur dans la détection de certaines erreurs, mais permet également d'assurer le non-déclenchement de ces mêmes erreurs à l'exécution.

III-A-1. Détecter davantage d'erreurs à la compilation

Le développeur n'est pas parfait, un moment d'égarement peut être à l'origine d'erreurs minimes à conséquences importantes (non initialisation de variables… ). Les capacités du développeur résident dans sa faculté à réfléchir sur des aspects complexes pour arriver à modéliser son application. Tout le contraire du développeur, le compilateur ne sait pas réfléchir (ce n'est pas lui qui va corriger des erreurs de modélisation), mais il est capable de prendre en charge des tâches plus proches du code et notamment sur la vérification de certains aspects comme l'initialisation de variables ou les erreurs prévisibles à l'exécution. Le travail du compilateur est de valider le code écrit, et on pourrait presque aller jusqu'à dire qu'idéalement un code qui compile ne doit pas générer d'erreurs lors de l'exécution (c'est évidemment utopique puisque certains aspects dynamiques comme la connexion à une base de données ou l'ouverture d'un fichier ne peuvent pas vraiment être pris en compte à la compilation). En anglais, cela s'appelle « static safety ».

III-A-2. Comment faire disparaître l'exception NullPointerException

La détection des appels sur les objets susceptibles d'être à null fait notamment partie des vérifications faites par le compilateur Nice.

Le compilateur Nice contrôle donc si les références (sous-entendu pointeurs) sont à null ou pas. Étant donné qu'il faudrait enrichir le code de tests pour chaque variable susceptible de prendre la valeur null, cette initiative permet donc au développeur de se concentrer sur la partie utile du code en laissant au compilateur de faire les vérifications pour lui.
Néanmoins, il y a toujours ces variables qui restent susceptibles d'être à null, et qu'il faut donc distinguer des autres sous peine de voir le compilateur renvoyer une erreur. Une notation spécifique a donc été introduite en préfixant le type dans la déclaration par un point d'interrogation :

 
Sélectionnez
?String nom;

Cette déclaration permet au compilateur de savoir que la variable est susceptible d'être à null et que le développeur devra prendre en charge le test de la variable :

 
Sélectionnez
int longueur = (nom == null) ? 0 : nom.length();

III-B. Un effort sur la modularité

Un second point fort de Nice est la modularité. Tout en imposant au développeur une certaine rigueur dans le code (notamment à travers la détection d'erreurs à la compilation), Nice propose un certain nombre de nouveautés par rapport à Java, lesquelles permettent d'avoir un code plus clair, de supprimer la redondance, et d'offrir aux développeurs des moyens efficaces d'implémenter des modèles pour lesquels ils pourraient être amenés à « bricoler » avec d'autres langages.

III-B-1. Les méthodes locales

Il est possible avec Nice de définir des méthodes locales à l'intérieur d'une autre méthode. Cela peut par exemple permettre d'alléger un traitement répétitif pour lequel la méthode qu'il faut définir n'a de sens et ne va qu'être appelée dans une autre méthode. Il peut donc être intéressant de ne pas laisser cette méthode visible par les autres méthodes, et par conséquent de limiter les possibilités de l'appeler à une méthode particulière.
Voici un exemple illustrant ce propos :

 
Sélectionnez
class Local
{
  void afficher()
  {
    void afficherLocal(String s)
    {
      System.out.print("Texte à afficher : ");
      System.out.println(s);
    }

    afficherLocal("a");
    afficherLocal("b");
  }
}

Le résultat est le suivant :
Texte à afficher : a
Texte à afficher : b

III-B-2. Des méthodes et variables de paquetage

Dans un paquetage Nice, il est possible de définir des méthodes et des variables/constantes indépendantes de toute classe. L'apparition de méthodes et variables de paquetage dans le langage Nice entraîne la disparition des méthodes et variables de classes (c'est-à-dire static). L'approche de Nice ressemble donc (à la structure de fichiers près) à ce qui est employé par le langage Ada (plus précisément Ada95) et met donc l'accent sur la notion de paquetage.

Conséquence immédiate de ce choix : étant donné que les méthodes statiques n'existent pas avec Nice, il n'y a donc pas la fameuse méthode public static void main(String[] args). On retrouve cette méthode comme méthode de paquetage : void main(String[] args).

Étant donné que l'approche de Nice attribue une place importante à la notion de paquetage, quoi de plus logique pour le compilateur de compiler ces derniers. En effet, contrairement à Java, l'unité de compilation de Nice est le paquetage et cela est parfaitement cohérent avec ce qui a été dit précédemment. Il est donc naturel de n'avoir qu'une méthode main dans un paquetage MonPaquetage qui sera appelée par java -jar MonPaquetage.jar.

III-B-3. Des méthodes prenant en paramètre d'autres méthodes

Le lecteur attentif aura décelé un aspect important de Nice dans la syntaxe de la méthode foreach, à savoir la possibilité pour une méthode de prendre en paramètre une autre méthode.

Ainsi, la méthode foreach prend en paramètre la méthode qui va être appelée sur les différents éléments de la collection en question.

III-B-4. Les multimethods

Ceux qui connaissent le langage Ada sont familiers des notations du type f(x,y,z) pour l'appel de méthode. Cette notation a pour effet d'harmoniser l'appel des méthodes qu'elles soient définies au niveau d'un paquetage ou au niveau d'une classe. En effet, pour une méthode de classe, le premier paramètre correspond dans cette notation à l'objet qui, dans des langages comme Java, appelle la méthode. Ainsi, la notation f(x,y,z) en Ada correspond à la notation x.f(y,z) en Java.

Étant donné que Nice met l'accent sur la notion de paquetage, et toujours dans un souci de forte modularité, il prend en charge les deux notations. Nous allons voir à travers les exemples suivants ce que la prise en compte de la notation f(x,y,z) peut permettre.

Voici un exemple simple :

 
Sélectionnez
class MoyenDeTransport
{ 
  String marque; 
  int masse; 
  
  String affiche(); 
} 

class Camion extends MoyenDeTransport
{ 
  int nbRoues; 
} 

affiche(MoyenDeTransport m) 
{ 
  return m.marque + " (" + m.masse + " kg)"; 
} 

affiche(Camion c)
{ 
  return c.marque + " (" + c.masse + " kg, " + c.nbRoues + " roues)"; 
}

Bien que la déclaration se fasse en dehors de la classe, il est possible d'appeler la méthode affiche() à partir d'un objet comme on le fait habituellement en Java par o.affiche();

III-B-4-a. Le multiple-dispatch
 
Sélectionnez
class Aliment
{
  String nom;
  float prix;
}

equals(Aliment a1, Aliment a2) = a1.nom.equals(a2.nom) && a1.prix==a2.prix;

Avec ce petit morceau de code, vous pourrez très bien exécuter le code suivant :

 
Sélectionnez
void main(String[] args)
{
  Aliment ppois = new Aliment(nom: "petits pois", prix: 0.8);
  Aliment carottes = new Aliment(nom: "carottes", prix: 0.75);

  if(carottes.equals(ppois))
  {
    println("Ce ne sont pas les mêmes aliments.");
  }
}

Qu'apporte ce code par rapport à la version équivalente en Java (qui consiste à déclarer la méthode equals au sein de la classe Aliment directement) me direz-vous. Premièrement, cela vous évite d'avoir à tester si l'objet passé en paramètre à equals est bien du type Aliment ainsi que de procéder à un cast.
De plus, cela permet tout simplement d'adapter et de faire évoluer le contenu d'un paquetage sans avoir à modifier les classes. En effet, imaginez qu'une classe AlimentEnVrac héritant de la classe Aliment soit créée et qu'il faille définir l'opérateur equals pour tester l'équivalence entre un objet Aliment et un autre AlimentEnVrac. En Java, il vous faudrait éditer la classe Aliment et y rajouter cette méthode, alors qu'avec Nice vous n'avez qu'à rajouter la méthode au niveau du paquetage.

 
Sélectionnez
class AlimentEnVrac extends Aliment
{
  // prix correspondra ici au prix au kg
  float quantite; // en kg
}

equals(Aliment a1, AlimentEnVrac a2) = a1.nom.equals(a2.nom) && (a1.prix==(a2.prix*a2.quantite));
III-B-4-b. La flexibilité apportée dans la déclaration de méthodes (Value Dispatch)

L'utilisation des i« multimethods » constitue un apport considérable pour la lisibilité et la maintenance de certains types de méthodes.
Imaginez le problème suivant : vous devez écrire une méthode qui renvoie un booléen en fonction de la valeur qui lui est fournie. L'écriture de la méthode va donc correspondre à un amoncellement de if et de return.

L'utilisation du Value Dispatch en Nice permet actuellement de traiter les méthodes prenant en paramètre le type integer, boolean, string, character, une énumération (enum) ou encore une instance constante de classe.
Voici comment cela se matérialise pour notre exemple :

 
Sélectionnez
class Personne
{
  String nom;

  boolean faitPartieEquipe()
  {
    return membreEquipe(nom);
  }
}

boolean membreEquipe(String s);

membreEquipe("Arnaud") = true;
membreEquipe("Eric") = true;
membreEquipe(s) = false;

Même si l'exemple n'est pas forcément très pertinent, on constatera qu'il est très facile de mettre à jour le code en rajoutant un nouveau cas pour membreEquipe dans le cas où l'équipe est modifiée.

III-B-5. Les interfaces abstraites

Dernier point concernant la modularité, la possibilité de créer et d'implémenter des interfaces dites abstraites. Ce sont des interfaces qui ressemblent en tout point à celles qu'on a l'habitude de manipuler en Java, à un détail près. En effet, une interface abstraite Nice est beaucoup moins riche qu'une interface Java, car elle n'a pour unique but que de lister les méthodes que les classes l'implémentant devront définir. De plus, avec les différents concepts que nous avons pu voir, il est possible de préciser qu'une classe déjà créée implémente une interface abstraite (attention ce n'est pas vrai pour une interface classique) sans avoir à la modifier directement.

Pour ceux qui ne verraient pas en quoi cela se distingue de ce qui est fait en Java, il faut savoir qu'il est possible d'utiliser le nom d'une interface dans la déclaration d'une variable comme dans ce qui suit :

 
Sélectionnez
interface MonInterface
{
  ...
}

class MaClasse implements MonInterface
{
  ...
}

class MonMain
{
  
  MonMain()
  {
  };

  public static void main(String[] args)
  {
    MonInterface test = new MaClasse();
  }

}

Les interfaces abstraites Nice ne peuvent donc en quelque sorte pas être considérées comme une classe et leur rôle se limite à de la spécification.

L'exemple précédent, repris avec une interface abstraite en Nice pourrait prendre la forme suivante :

 
Sélectionnez
abstract interface MonInterface
{
  void maMethode();
}

class MaClasse
{
  ... // déclarations propres à la classe (ne tient pas compte de l'interface)
}

class MaClasse implements MonInterface; // peut très bien apparaître dans un autre fichier

maMethode(MaClasse c)
{
  ...
}

III-C. L'écriture de code intelligent (« Expressivity »)

Le dernier aspect mis en avant par Nice est l'écriture de code intelligent, c'est-à-dire l'introduction de certains concepts permettant à la fois de rendre un code plus expressif et donc maintenable, mais également la mise en place de moyens évitant au développeur un code répétitif et difficile à comprendre.

III-C-1. La notation

On vous a probablement plusieurs fois dit que la première étape dans l'écriture d'une méthode était de lui trouver un nom assez explicite pour qu'un autre développeur puisse facilement comprendre à quoi elle sert. Lorsque le nombre d'arguments de la méthode est supérieur à un, les mêmes exigences se répercutent sur la liste des arguments, arguments que l'on peut en règle générale également nommer de façon intelligente pour faciliter la compréhension.

Néanmoins, ces efforts ne seront bénéfiques qu'au développeur qui va appeler ces méthodes et leur passer des paramètres, car l'environnement de développement qu'il utilise pourra le guider et lui indiquer les noms et types des paramètres par exemple. Au contraire, toute personne qui va relire le code va être obligée de consulter constamment la documentation pour mieux comprendre l'appel d'une méthode.

C'est pour améliorer et faciliter la compréhension et le développement que Nice propose certaines fonctionnalités originales, dont certaines bien connues par les utilisateurs du langage Ada.

III-C-1-a. Le passage de paramètres

Premier aspect important : l'appel d'une méthode en Nice peut se faire de manière classique en passant dans l'ordre de déclaration les paramètres, mais également le passage de paramètres par nom.
Le passage de paramètres par nom permet de s'affranchir de l'ordre d'appel des paramètres et rend le code davantage compréhensible et maintenable. Voici quelques exemples d'utilisation :

Deux déclarations identiques

 
Sélectionnez
Voiture v1 = new Voiture(Carburant: "Essence", Couleur: "Gris", Constructeur: "Peugeot", Modele: "206");

Voiture v1 = new Voiture(Constructeur: "Peugeot", Modele: "206", Carburant: "Essence", Couleur: "Gris");

Comment rendre un code extrêmement lisible

 
Sélectionnez
void vendre(Voiture voiture, Personne acheteur, float prix) { ... }

vendre(prix: 12000, voiture: v1, acheteur: dupont);

Cette possibilité, conjuguée à la notion de multiméthodes vue précédemment, rend le code beaucoup plus explicite et permet d'éviter les erreurs classiques qu'on peut rencontrer lorsqu'une méthode dispose d'un grand nombre de paramètres.

III-C-1-b. L'appel des méthodes

Comme cela a déjà été dit, l'appel d'une méthode avec Nice peut se faire d'un point de vue objet par x.f(y,z) ou d'un point de vue paquetage par f(x,y,z). L'introduction du passage des paramètres par nom permet même d'appeler cette même méthode par f(param3: z, param2: y, param1: x);

III-C-2. Le Design By Contract

Nice supporte des préconditions et des postconditions telles qu'on peut en définir lorsqu'on précise certains aspects de notre modèle avec O.C.L. (Object Constraint Language).
Les mots-clés introduits par Nice sont requires et ensures.

Voici l'exemple donné par les auteurs de Nice :

 
Sélectionnez
interface Buffer<Elem>
{
  int size();

  boolean isFull();
  boolean isEmpty() ensures result == (size() == 0);

  void add(Elem element)
    requires
         !isfull() : "buffer must not be not full"  // A comma here is optional
    ensures
         !isEmpty() : "buffer must not be empty",   // Note the comma
         size() == old(size()) + 1 : "count inc";
}

III-C-3. Les valeurs par défaut dans les méthodes

Qui n'a pas déjà été confronté en Java à l'écriture d'une méthode pouvant avoir des paramètres optionnels qui deviennent, lorsqu'ils ne sont pas fournis, des valeurs par défaut ? Dans ce type de situation, le développeur est contraint de déclarer autant de variantes de la méthode qu'il y a de variantes occasionnées par les paramètres optionnels (pour 2 paramètres optionnels, on pourrait être amené à définir 4 variantes de la méthode, pour 3 on arrive déjà à 8).
Nice propose une gestion très efficace des valeurs par défaut, laquelle permet de ne définir qu'une seule méthode, quel que soit le nombre de paramètres optionnels (il est bien entendu que certains concepts vus précédemment facilitent l'introduction des paramètres optionnels et valeurs par défaut).

Voici quelques exemples :

 
Sélectionnez
void copier(File de, File vers, int tailleBuffer = 1024) { ... }
copier(tailleBuffer: 256, vers: f2, de: f1);

void extraire(File de, File vers, long taille = de.length()) { ... }
extraire(de: f1, vers: f2);

Vous voyez donc qu'il est possible de spécifier une valeur par défaut dépendant d'un autre paramètre de la méthode. L'utilisation de valeurs par défaut facilite considérablement la compréhension et le codage d'une méthode.

III-C-4. Les constructeurs en Nice

Dans le même esprit de simplification, Nice permet de s'affranchir de l'écriture des constructeurs. Il ne vous est pas possible de définir votre propre constructeur, et Nice crée par lui-même un constructeur à partir des attributs que vous avez définis dans votre classe.
Il y a bien évidemment une raison à ce choix, et c'est là que rejaillit l'aspect sécurité mis en avant par Nice. En effet, en laissant l'utilisateur définir un constructeur comme il peut actuellement le faire en Java, il lui est possible d'aboutir à des erreurs de fonctionnement inattendu :

 
Sélectionnez
class Parent 
{
  Parent()
  {
    Logger.log("Crée: " + this);
  }

  public String toString()
  {
     return "Parent";
  }
}

class Enfant extends Parent 
{
  
  Enfant(File fichier)
  {
    this.fichier = fichier;
  }

  private File fichier;

  public String toString()
  {
    return "Enfant, taille du fichier: " + fichier.length());
  }
}

À première vue, on ne voit pas pourquoi ce code serait susceptible d'entraîner un comportement inattendu. Pourtant, si nous créons un nouvel objet de type Enfant, l'appel sera d'abord fait au constructeur Parent et celui-ci va appeler la méthode toString() par l'intermédiaire de la ligne Logger.log(« Crée: » + this);. Or, à cet instant précis, l'objet n'a pas encore son champ fichier et il va donc y avoir déclenchement d'une exception NullPointerException.

C'est donc un argument suffisant pour justifier une réflexion sur la façon de définir un constructeur, et c'est ce que Nice se propose de faire. En effet, il est actuellement possible d'appeler un constructeur avec pour paramètres l'ensemble des champs définis dans la classe. Pour être complet, il faut également préciser qu'il n'y a pas pour autant de contrainte au niveau du constructeur : au contraire, il est obligatoire de spécifier dans le constructeur au moins les champs pour lesquels aucune valeur n'est définie dans leur déclaration. Enfin, il faut savoir que l'appel du constructeur se fait exclusivement en passant les paramètres par nom.

L'état actuel du constructeur d'une classe Nice n'est bien évidemment pas satisfaisant, car l'auteur de la classe peut souhaiter définir les champs pouvant être initialisés ou non dans le constructeur. Les auteurs de Nice semblent se diriger vers un marquage spécial de ces champs, lequel permettrait de faire cette fameuse distinction.

Aux dernières nouvelles, il est désormais possible de définir ses propres constructeurs et de définir une section particulière qui sera exécutée à chaque création d'un objet de la classe, avec bien entendu des contraintes (exemple repris) :

 
Sélectionnez
class Point
{
  double x;
  double y;

  {
    System.out.println("Création d'un point.");
  }
}

new Point(double angle, double distance) 
{ 
  this(x: distance * cos(angle), y: distance * sin(angle));
}

Quelques commentaires sur ce code. Il est donc possible de définir un constructeur (à l'extérieur de la classe) qui va lui même appeler le constructeur défini par Nice à partir des champs de la classe. Dans ce constructeur, il n'est pas possible d'accéder à l'objet this et il n'y a donc aucun risque d'appeler une méthode susceptible d'être redéfinie dans une classe enfant.
De plus, la section délimitée par des accolades à l'intérieur même de la classe, permet de préciser un code à exécuter après la création d'un objet. Cela répond au problème précédemment identifié, car ce code sera exécuté après l'initialisation complète des champs.

III-C-5. Le renvoi de tuples

Dernier aspect intéressant du langage Nice que je voulais citer, la possibilité de définir une méthode renvoyant un tuple permet au développeur une plus grande flexibilité et peut lui éviter par exemple d'avoir à créer une classe intermédiaire.

Voici un exemple d'utilisation :

 
Sélectionnez
(int, int) minMax(int x, int y) = x < y ? (x, y) : (y, x);

void afficherCouple((int, int) couple)
{
  (int x, int y) = couple;
  System.out.println("(" + x + ", " + y + ")");
}

void main(String[] args)
{
  printTuple(minMax(14, 17));
  printTuple(minMax(42, 41));
}

précédentsommairesuivant

Copyright © 2004 Ricky81. Aucune reproduction, même partielle, ne peut être faite de ce site ni 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.