Il y a bien fort peu de chose qui puisse faire regretter un delphiste d'être passé à C#, et quand je parle de regrets, c'est un mot bien fort, disons plus modestement des manques agaçants.
Rien qui puisse faire réellement pencher la balance au point de revenir à Delphi, non, ce langage est bel et bien mort, assassiné par Borland/Inprise/Borland/CodeGear et son dernier big boss, tod nielsen qui ne mérite pas même les majuscules à son nom mais là n'est pas la question.
Donc il existe syntaxiquement trois choses qui peuvent agacer le delphiste, même, comme moi, quand cela fait au moins cinq ans maintenant que je ne pratique presque plus que C# (et un peu de Delphi pour maintenir des vieilleries et aider certains clients à tourner la page en migrant sous C#).
La première qui revient à longueur de code, ce sont ces satanés parenthèses dans les "if". On n'arrête pas de revenir en arrière parce qu'on rajoute un test dans le "if" et qu'il faut remettre tout ça entre de nouvelles parenthèses qui repartent depuis le début. Certes on gagne l'économie du "then" pascalien, mais que ces parenthèses du "if" sont épouvantables et ralentissent la frappe bien plus qu'un "then" unique et ponctuel qu'on ne touche plus une fois écrit même si on rajoute "and machin=truc" ! A cela pas d'astuce ni truc. Et aucun espoir que les parenthèses de C# dans les "if" soient abandonnées un jour pour revenir au "then" ... Donc faut faire avec ! Le dogme "java/C++" est bien trop fort (quoi que C# possède le Goto de Delphi, ce qui n'est pas la meilleure idée du langage :-) ).
La seconde tracacerie syntaxique est cette limitation totalement déroutante des indexeurs : un seul par classe et il ne porte pas de nom. this[], un point c'est tout. Je sais pas ce que notre bon Hejlsberg avait en tête, mais pourquoi diable n'a-t-il repris de Delphi qu'un seul indexeur et non pas la feature entière ? Il fait beaucoup de recherche, et le fait bien, mais je présume qu'il n'a jamais plus codé un "vraie" appli depuis des lustres... Car dans la vraie vie, il existe plein de situations où un objet est composé de plus d'une collections. Une voiture a 4 roues et 2 ou 4 portières. Pourquoi lorsque je modélise la classe Voiture je devrais donner plus d'importance aux roues qu'aux portières et choisir lesquelles auront le droit d'être l'unique indexeur de la classe ? Pourquoi tout simplement ne pas avoir Voiture.Roues[xx] et Voiture.Portières[yy] ? Mon incompréhension de ce choix très gênant dans plus d'un cas a été et reste des années après totale. Surtout après toutes les évolutions de C# sans que jamais cette erreur de conception ne soit corrigée. Pas suffisant pour faire oublier toute la puissance de C# et le bonheur qu'on a à travailler dans ce langage, mais quand même, ça agace.
Enfin, dans la même veine d'ailleurs, l'absence de "Set of" est cruelle. Pourquoi avec zappé cette feature de Delphi dans C# alors que bien d'autres ont été reprises (avec raison ou moins comme le Goto) ?
Mais là on peut trouver des astuces et certains (dont je fais partie) ont écrit ou essayer d'écrire des classes "SetOf" qui permettent de gérer des ensembles comme Delphi, mais que c'est lourd tout ce code au lieu d'écrire "variable machin : set of typeTruc" !
Autre astuce moins connue, et c'est pour ça que je vous la livre, est l'utilisation fine des Enums. En effet, tout comme sous Delphi, il est possible d'affecter des valeurs entières à chaque entrée d'une énumération. On peut donc déclarer "enum toto { item1 = 5, item2 = 28, item3 = 77 }".
Mais ce que l'on sait moins c'est que rien ne vous interdit d'utiliser des puissances de 2 explicitement car les Enums savent d'une part parser des chaînes séparées par des virgules et d'autre part savent reconnaître les valeurs entières cumulées.
Ainsi, avec enum Colors { rouge=1, vert=2, bleu=4, jaune=8 }; on peut déclarer :
Colors orange = (Colors)Enum.Parse(typeof(Colors),"rouge,jaune"); // étonnant non ?
La valeur de "orange" sera 9 qu'on pourra décomposer facilement, même par un simple Convert.ToInt64.
Pour ne pas réinventer la roue et éviter de faire des coquilles je reprends cet exemple tel que fourni dans la doc MS. Voici le code complet qui vous permettra, peut-être, d'oublier plus facilement "Set of" de Dephi...
Stay tuned!
Code de la doc MS :
using System;
public class ParseTest
{
[FlagsAttribute]
enum Colors { Red = 1, Green = 2, Blue = 4, Yellow = 8 };
public static void Main()
{
Console.WriteLine("The entries of the Colors Enum are:");
foreach (string colorName in Enum.GetNames(typeof(Colors)))
{
Console.WriteLine("{0}={1}", colorName,
Convert.ToInt32(Enum.Parse(typeof(Colors), colorName)));
}
Console.WriteLine();
Colors myOrange = (Colors)Enum.Parse(typeof(Colors), "Red, Yellow");
Console.WriteLine("The myOrange value {1} has the combined entries of {0}",
myOrange, Convert.ToInt64(myOrange));
}
}
/*
This code example produces the following results:
The entries of the Colors Enum are:
Red=1
Green=2
Blue=4
Yellow=8
The myOrange value 9 has the combined entries of Red, Yellow
*/
Class Helpers
Les class helpers sont une nouvelle feature du langage C# 3.0 (voir mon billet et mon article sur les nouveautés de C# 3.0).
Pour résumer il s'agit de "décorer" une classe existante avec de nouvelles méthodes sans modifier le code de cette classe, les méthodes en questions étant implémentées dans une autre classe.
Le principe lui-même n'est pas récent puisque Borland l'avait "inventé" pour Delphi 8.Net (la première version de Delphi sous .Net) afin de permettre l'ajout des méthodes de TObject à System.Object (qu'ils ne pouvaient pas modifier bien entendu) afin que la VCL puisse être facilement portée sous .Net. Borland avait déposé un brevet pour ce procédé (ils le prétendaient à l'époque en tout cas) et je m'étonne toujours que Microsoft ait pu implémenter exactement la même chose dans C# sans que cela ne fasse de vagues. Un mystère donc, mais là n'est pas la question.
Mauvaises raisons ?
Les deux seules implémentations de cet artifice syntaxique que je connaisse (je ne connais pas tout hélas, si vous en connaissez d'autres n'hésitez pas à le dire en commentaire que l'on puisse comparer) sont donc celle de Borland dans Delphi 8 pour simplifier le portage de la VCL sous .NET et celle de Microsoft dans C# pour faciliter l'intégration de Linq dans le langage.
Deux exemples, deux fois pour la même raison un peu spécieuse à mes yeux : simplifier le boulot du concepteur du langage pour supporter de nouvelles features. Deux fois une mauvaise raison à mes yeux, un peu trop puristes peut-être, qui pensent qu'un élément syntaxique doit se justifier d'une façon plus forte, plus théorique que simplement "pratique".
Résultat, je me suis toujours méfié des class helpers car leur danger est qu'un objet se trouve d'un seul coup affublé de méthodes "sorties d'un chapeau", c'est à dire qu'il semble exposer des méthodes publiques qui ne sont nulles part dans son code. J'ai horreur de ce genre de combines qui, à mon sens, favorise un code immaintenable. Si j'adore la magie, à voir ou à pratiquer, je la déteste lorsque je porte ma casquette d'informaticien... J'ai donc toujours conseillé la plus grande circonspection vis à vis de cet artifice syntaxique, que ce soit à l'époque (déjà lointaine.. le temp passe!) où je faisais du Delphi que maintenant sous C# qui vient d'ajouter cette fioriture à sa palette.
Jusqu'à aujourd'hui, faute d'avoir trouver une utilisation intelligente des class helpers qui ne puissent être mise en oeuvre plus "proprement", les class helpers étaient à mon sens plutôt à classer du côté enfer que paradis. Interface et héritage m'ont toujours semblé une solution préférable à ces méthodes fantomes.
Trouver une justification
Mais il n'y a que les imbéciles qui ne changent pas d'avis, n'est-ce pas... Et je cherche malgré tout toujours à trouver une utilité à un outil même si je le trouve inutile de prime abord, réflexe d'ingénieur qui aime trouver une place à toute chose certainement.
J'ai ainsi essayé plusieurs fois dans des projets de voir si les class helpers pouvaient rendre de vrais services avec une réelle justification, c'est à dire sans être un cache misère ni une façon paresseuse de modifier une classe sans la modifier tout en la modifiant...
Comme j'ai enfin trouvé quelques cas (rares certes) dans lesquels les class helpers me semblent avoir une justification pratique, je me suis dit que vous en toucher deux mots pourraient éventuellement faire avancer votre propre réflexion sur le sujet (même si j'ai bien conscience que je dois être assez seul à me torturer la cervelle pour trouver absolument une utilité aux class helpers :-) ).
Bref, passons à la pratique.
Un cas pratique
Premier cas, les chaînes de caractères. Voilà un type de données vieux comme la programmation qui pourrait être un bon candidat à l'exploitation des possibilités des class helpers. En effet, hors de question de dériver la classe System.String et encore moins de pouvoir modifier le mot clé "string" de C#. Pourtant nous utilisons très souvent les mêmes fonctions "personnelles" sur les chaînes de caractères d'un même projet.
Par exemple, j'ai pour principe que les chaînes exposées par une classe (propriétés de type string donc) ne soient jamais à null. De ce fait, dans toutes les classes qui exposent un string, j'ai, dans le setter, la même séquence qui change l'éventuel null de value à string.Empty. C'est assez casse-pieds à répéter comme ça mécaniquement dans toutes les propriétés string de toutes les classes.
Et là, pas de possibilité de faire supporter une interface à System.String, ni de la dériver comme je le disais plus haut. C'est ici que les class helpers peuvent trouver une première justification pratique pour le développeur en dehors d'avoir facilité la vie à MS pour implémenter Linq.
Prenons le code suivant que nous plaçons dans une unité "Tools" de notre projet :
public static class Utilities
{
public static string NotNullString(this string s)
{ return !string.IsNullOrEmpty(s) ? s.Trim() : string.Empty; }
...
}
La classe Utilities est une classe statique qui contient tous les petits bouts de code utilisables dans tout le projet. Parmi ces méthodes, on voit ici l'implémentation du class helper "NotNullString". D'après cette dernière, la méthode ne sera visible que sur les instances de la classe "string". Le code lui-même est d'une grande simplicité puisqu'il teste si la chaîne en question est vide ou non, et, selon le cas, retourne string.Empty ou bien un Trim() de la chaîne. J'aime bien faire aussi systématiquement un Trim() sur toutes les propriétés string, je trouve ça plus propre surtout si on doit tester des équalités et que les chaînes proviennent de saisies utilisateurs.
Dans la pratique il suffira maintenant n'importe où dans notre projet d'écrire la chose suivante pour être certain qu'à la sortie de l'affectation la chaîne résultante ne sera jamais nulle et qu'elle ne comportera jamais d'espaces en trop en début ou en fin :
public string BusinessID
{ get { return _BusinessID; }
set { if (value != _BusinessID)
{ _BusinessID = value.NotNullString().ToUpperInvariant();
DoChange("BusinessID");
}
}
}
On voit ici une propriété string "BusinessID" qui, dans son setter, utilise désormais la nouvelle méthode fantome de la classe string... En fin de séquence nous sommes certains que _BusinessID est soit vide, soit contient un chaîne sans espace de tête ou de queue (et qu'elle est en majuscules, en plus dans cet exemple).
Voici donc une première utilisation "intelligente" des class helpers, la décoration d'une classe du framework (ou d'une lib dont on n'a pas le code source) pour lui ajouter un comportement, éventuellement complexe, dont on a souvent l'utilité dans un projet donné.
On pourrait penser ainsi à une fonction "ProperCase" qui passe automatiquement la casse d'une chaîne en mode "nom de famille", c'est à dire première lettre de chaque mot en majuscule, le reste en minuscule, ou à bien d'autres traitements qu'on aurait besoin d'appliquer souvent à des chaînes dans un projet.
Encore un autre cas
Dans la même veine, une application doit souvent manipuler des données issues d'une base de données et les modifier (en plus d'en insérer de nouvelles). On le sait moins (mais on s'aperçoit vite quand cela bug!) que le framework .NET possède, pour les dates par exemple, ses propres mini et maxi qui ne sont pas compatibles à toutes les bases de données, notamment SQL Server. Si vous attribuer à une date la valeur DateTime.MinValue et que vous essayez d'insérer cette dernière dans un champ Date d'une base SQL Server vous obtiendrez une exception de la part du serveur : la date passée n'est pas dans la fourchette acceptée par le serveur.
Dommage... DateTime.MinValue est bien pratique...
On peut bien entendu fixer une constante dans son projet et l'utiliser en place et lieu de DateTime.MinValue. Voci un exemple pour MaxValue (le problème étant le même):
DateTime MaxiDate = new DateTime(3000, 1, 1, 0, 0, 0);
Il suffira donc d'utiliser MaxiDate à la place de DateTime.MaxValue. La date considérée comme "maxi" est ici arbitraire (comme on le ferait pour la date mini) et est choisie pour représenter une valeur acceptable à la fois pour l'application et pour la base de données. Ici on notera que je prépare le terrain pour le bug de l'an 3000. Moins stupide qu'un coboliste et le bug de l'an 2000, vous remarquerez que je me suis arrangé ne plus être joignable à la date du bug et que mes héritiers sont aussi à l'abris de poursuites judiciaires pour quelques siècles :-)
L'utilisation d'une constante n'a rien de "sale" ou de "moche", c'est un procédé classique en programmation, même la plus éthérée et la plus sophistiquée. Toutefois, Puisque DateTime existe, puisque DateTime est un type complexe (une classe) et non pas un simple emplacement mémoire (comme les types de base en Pascal par exemple), puisque cette classe expose déjà des méthodes, dont Min et MaxValue, il serait finalement plus "linéaire" et plus cohérent d'ajouter notre propre MaxValue à cette dernière en place et lieu d'une constante.
Encore un bon exemple d'utilisation des class helpers. Ici nous homogénéisons notre style de programmation en évitant le mélange entre méthodes de DateTime et utilisation d'une constante. De plus, en ajoutant une méthode spécifique à DateTime, celle-ci sera visible par Intellisense come membre de DateTime, ce qui ne serait pas le cas de la constante.
Dans la même classe Utilities nous trouverons ainsi :
public static DateTime SQLMaxValue(this DateTime d)
{ return new DateTime(3000, 1, 1, 0, 0, 0); }
Et nous voici avec la possibilité d'écrire : MaDate = DateTime.SQLMaxValue();
Conclusion
Enfer ou paradis ? Les class helpers, comme tout artifice syntaxique peuvent se ranger dans les deux catégories, ils ne sont qu'un outil à la disposition du développeur. Un simple marteau peut servir à bâtir une maison où vivre heureux ou bien à assassiner sauvagement son voisin... Les objets inanimés n'ont pas de conscience, ou plutôt, si, ils en ont une : celle de celui qui les utilise. A chacun d'utiliser les outils que la technologie humaine met à sa disposition pour créer un enfer ou un paradis...
Techniquement, je n'enfoncerai pas les portes ouvertes en donnant l'impression d'avoir découvert une utilisation miraculeuse des class helpers. Cela serait stupide, puisque justement cette syntaxe a été créée par Borland puis MS pour justement décorer des classes dont on ne possède pas le source (pas d'ajout de méthode ni d'interface) et qui ne sont pas héritables. En ajoutant des class helpers à string ou DateTime nous ne faisons rien d'autre que d'utiliser les class helpers exactement en conformité avec ce pour quoi ils ont été créés.
L'intérêt de ce billet se situe alors dans deux objectifs : vous permettre de réfléchir à cette nouveauté de C#3.0 que vous ne connaissez peut-être pas ou mal et vous montrer comment, en pratique, cela peut rendre des services non négligeables.
Si l'un ou l'autre de ces objectifs a été atteint, vous tenez alors votre récompense d'avoir tout lu jusqu'ici, et moi d'avoir écrit ce billet :-)
Stay tuned !
[EDIT: voir cet autre billet plus récent sur les class helpers]
La pluie et le beau temps, je parle de la météo et de ma station Lacrosse, vous l'aurez compris.
Et quand un geek possède un tel engin, par force il sort son compilateur pour bricoler. Inévitable.
Il tripote, il essaye et par force il y a des ratés, donc besoin de réparer les fichiers historiques issus de la machine.
Comme je sais n'être pas le seul à posséder une Lacrosse série 2300-2350 et que réparer le fichier "history.dat" est certainement un besoin courant et non comblé par les logiciels fournis avec la station, voici deux utilitaires :
-
History Inspector
pour inspecter, modifier les données, consolider les données du pluiviomètre, ...
-
DatMerger
pour fusionner deux historiques avec consolidation des données.
Il s'agit de logiciels fournis à titre expérimental, sans aucune garantie ni aucun support même si je reste à l'écoute de vos retours.
Le code source est fourni, deux projets VS 2005 (tout le monde n'a peut-être pas encore VS 2008) en C#.
Les séquences de lecture/écriture ont été reprises du logiciel eWs2300 d'Antoine Guilmard avec quelques modifications (donc ne mélanger les sources!) que je tiens à remercier pour la publication de son code.
C'est gratuit, avec le source. Les exécutables sont fournis aussi pour ceux qui n'ont pas VS2005/2008 sous la main. Mais il faudra alors installer le runtime de .NET 2.0 au minimum (téléchargeable gratuitement sur les sites de Microsoft).
Le tout est à télécharger à cette adresse : [consultez la page "Ressources" du site e-naxos en cliquant sur le logo "e-naxos" en haut à droite !]
Pour les fans de méteo et de connectique de zinzins électroniques, j'ai mis à jour ma page "La météo de Royan-Pontaillac en direct". Un nouveau diagramme mensuel automatique est ajouté avec moultes indications dont la rose des vents dominants.
Au passage j'ai conçu un programme C# qui permet de recoller des fichiers history.dat de HeavyWheather pour les fusionner le tout en remettant d'équerre certaines infos (l'enregistrement de fin qui parfois est désynchro et internent à heavy weather d'ajouter de nouvelles lignes à l'historique. Les intéressés n'ont qu'à me laisser un commenaire sur ce billet..).
Le multithreading c'est l'épouvantail du développeur. Vous en parlez, hop! tout le monde s'en va de la machine à café... et s'il y en a un qui ne part pas , c'est le genre fanatique qui va débaler une science opaque sur les AppDomains, les mutex et autres mots qui fâchent, du coup, c'est vous qui partez :-)
Je carricature à peine...
C'est tout le problème du multithreading. Pratiqué avec simplicité c'est une technique de plus en plus indispensable pour tirer partie des microprocesseurs multicoeurs et fluidifier les interfaces, mais voilà, comment faire simple avec une telle technique ?
Les puristes vous diront qu'il faut absolument comprendre la technique, et qu'en suite c'est facile... Un peu comme Coluche qui expliquait dans l'un de ses sketchs que son professeur de violon lui avait dit d'apprendre à jouer avec des gants de boxe parce que quand on les enlève ça semble facile...
Je ne vais pas vous dire qu'une démarche rigoureuse est inutile, j'ai un module de multithreading avancé dans mes plans de cours et, bien entendu, voir les choses en profondeur au sein d'une formation est le seul moyen de maîtriser cette technique. Mais il existe aussi des façons simples d'introduire un peu de multitâche dans vos applications.
Il s'agit du composant BackgroundWorker des Windows Forms. Certes le sujet tranche avec mes billets généralement plus orientés vers les super nouveautés hypra fraîches à tel point qu'elles sont même parfois en bêta... Mais il faut bien maintenir les applications existantes, les améliorer, et pour cela il existe, comme le BackgroundWorker des solutions pratiques qui ne nécessitent pas d'installer le framework 3.5 puisque cette classe a été fournie avec .NET 2.0.
De plus, ce composant Windows Forms n'impose pas de connaître les mécanismes du multithreading, il suffit de programmer ces événements comme un bouton. Trop facile ? Peut-être que cela choquera les puristes parce que "cela cache la réalité de ce qui se passe vraiment dans la machine", je leur répondrais que faire du C# au lieu de faire de l'assembleur c'est un peu pareil... Là où je les rejoindrais c'est que, bien entendu, la classe backgroundWorker ne doit pas être utilisée à tord et à travers. Si l'on désire concevoir des classes gérant finement le multitâche, il faut réellement comprendre et donc apprendre. Mais dans de nombreux cas, le BackgroundWorker pourra vous être utile et rendre plus fluide vos applications Windows Forms sans avoir à entrer dans les détails d'une technique un peu aride.
Mais trève de mots, le plus simple c'est de jouer avec ce composant pour se rendre compte de son utilité. Pour facilité la.. tâche... je vous ai préparé un petit projet VS 2005 qui montre comment se servir de backgroundWorker : BGW.zip (41,83 kb)
Un peu de douceur multitâche dans ce monde de multicoeurs...
Le voilà enfin ! [Updated ! Version 1.1 en ligne]
un PDF de 32 36 pages et 5 6 projets exemples sous VS 2008 pour vous présenter les différentes facettes de LINQ. Je n'en voyais plus le bout de cet article ! Non par lassitude, bien au contraire, mais parce que LINQ est d'une incroyable richesse et que je voulais vous en dire la maximum.
Sans entrer dans les détails trop techniques de la syntaxe (la doc Microsoft est très complète et n'a nul besoin d'une redite), cet article présente le pourquoi et le comment de LINQ au travers d'explications et d'exemples de code.
-
LINQ to Objects
-
LINQ to SQL
-
LINQ to Dataset
-
LINQ to XML
-
LINQ to Entities
Sans prétendre que toutes ces versions de LINQ n'auront plus de secret pour vous après avoir lu l'article, vous en saurez certainement plus pour mieux comprendre pourquoi il y a eu un avant LINQ et qu'il va y avoir un après LINQ...
Pour télécharger l'article cliquez ici !
Avant de lire cet article il est préférable de connaître les nouveautés syntaxiques de C# 3.0, si ce n'est pas votre cas vous pouvez télécharger mon précédent article.
Pour la liste de tous mes billets sur LINQ cliquez ici.
Note de la version 1.1 : table des matières ajoutée + plus de détails sur Linq to Entities et un projet utilisant la bêta 3.
Je suis en train de boucler mon article de présentation de LINQ. Au départ je pensais m'en sortir en une dizaine de pages, mais j'en suis à plus du double... juste pour faire le tour des principales possibilités et sans détailler la syntaxe (ce n'est pas le but de cet article).
J'ai eu maintes fois l'occasion de vous dire ici que LINQ est l'innovation la plus fantastique que j'ai vue depuis longtemps dans un langage. Vous en saurez plus en lisant le prochain article qui sera en ligne ce week-end, mais pour vous prouver que LINQ peut servir partout et tout le temps voici deux exemples à contre-courant de l'idée qu'on se fait des utilisations possibles de LINQ :
Cas 1 : Lister les services actifs de Windows.
C'est bête mais balayer et filtrer une simple liste comme celle là (et de bien d'autres du même genre retournées par le framework .NET ou par vos applications), c'est produire du code pas très marrant... Avec LINQ ça devient :
using System.ServiceProcess;
var srv = from s in ServiceController.GetServices()
where s.Status == ServiceControllerStatus.Running
select s.DisplayName;
ListBox1.DataSource = srv.ToList();
Je trouve ça élégant, pas vous ?
Cas 2 : Remettre à unchecked tous les Checkbox d'une form
Balayer certains contrôles d'une fiche n'est là non plus pas l'endroit où l'on s'attend à trouver du LINQ... Et pourtant ! Imaginons une fiche de saisie avec des tas de checkbox et un bouton "raz" de remise à zéro de la fiche. Balayer tous les contrôles de la fiche pour ne sélectionner que les checkbox n'est pas un code bien complexe mais avec LINQ ça devient tellement plus chouette !
var cb = (from Control c in this.Controls select c).OfType<CheckBox>();
foreach (var c in cb) c.Checked=false;
C'est pas plus joli et plus clair écrit comme ça ? (et encore on pourrait se passer de la variable "cb" et intégrer directement la requête LINQ après le "in" du "foreach")?
Bref, LINQ j'adore. Et j'espère bien que le papier à venir va vous transformez en fans vous aussi ! alors... Stay tuned !
Les expressions Lambda vous connaissez ?
Et l'inférence des types locaux, les méthodes d'extension, les expressions d'initialisation des objets ou les types anonymes ?
Si tout cela sonne bizarrement à vos oreilles, n'hésitez plus une seconde : téléchargez le nouvel article que j'ai mis en ligne gratuitement !
Voici le lien, il n'y a qu'à cliquer ici !
Vous êtes encore là ?
... Alors ne loupez pas le tout dernier article "Présentation de LINQ avec C#", un papier de 32 pages riche en exemples !
Cette conférence était l'une de celle que j'attendais le plus. De ce que j'avais déjà vu de LINQ et de ce qui tournait autour de cette nouveauté du framework 3.5 je savais qu'il y avait là un joyau... Les quelques essais que j'avais faits avec la Bêta de VS 2008 n'avaient qu'attisé ma curiosité. Je désirais donc à tout prix voir la session DAT 201 : Entity Framework Introduction pour refaire le tour complet de la technologie pour être sûr de ne pas passer à côté d'un élément essentiel.
Et c'est encore mieux que ce que je pensais. C'est une pure merveille, une avancée aussi spectaculaire que la naissance du framework .NET lui-même. Rien de moins.
L'Entity Framework
La session DAT 201, présentée par Carl Perry, Senior Program Manager Lead chez Microsoft, se voulait une introduction, un tour du propriétaire de l'Entity Framework. Mais kesako ? Le "cadre de travail pour entité" en français. Avec ça vous êtes bien avancé !
Pour faire le plus court possible disons que l'EF (Entity Framework) est une nouvelle couche d'abstraction se posant sur ADO.NET et permettant tout simplement d'accéder aux données de façon totalement objet et surtout dans le respect d'un schéma conceptuel et non plus en travaillant avec la base de données, son schéma physique et son SQL.
Est-ce que vous êtes plus avancé ? j'ai comme un doute... Mais les choses vont s'éclaircir avec la suite de ce billet.
Pour mieux comprendre la différence ne serait-ce que dans le code, comparons les deux types d'accès aux données, ADO.NET 2.0 et EF.
L'accès aux données de ADO.NET 2.0
Voici un code typique d'accès aux données via ADO.NET 2.0 tel qu'on en voit dans toutes les applis ou presque :
On retrouve toute la chaîne d'opérations usuelles, de la création et l'ouverture de la connexion jusqu'à l'itération dans le resultset retournée par le DataReader suite à l'envoi d'une Command dont le contenu est du pur SQL exprimé dans le dialect spécifique de la base de données cible.
Cette méthode est puissante, souple, mais elle possède de nombreux pièges. Par exemple l'accès aux données du DataReader s'effectue par les noms de champ. Une simple coquille à ce niveau et le bug ne sera détecté qu'à l'exécution, peut-être dans 6 mois chez le client quant il utilisera cette fonction particulière du logiciel. Le coût de cette coquille peut être énorme s'il s'agit d'un logiciel diffusé en grand nombre (hot line de maintenance, détection du bug - pas toujours simple, création d'une mise à jour - les sources elles-mêmes ont bougé, problème de versionning, diffusion de la mise à jour et découverte de nouveaux problèmes - incompatibilités d'une des modifications, etc, etc). J'arrête là le scénario catastrophe car selon les lois de murphy je suis encore en dessous de la réalité, et vous le savez comme moi...
Le pire c'est que je n'ai relevé qu'une seule des erreurs possibles, ce code possède bien d'autres pièges qui agiront comme des aimants à bugs ! Un autre exemple, toujours sur l'accès aux données lui-même : Il n'y a aucun contrôle de type. Je passe sur les conséquences que vous pouvez imaginer dans certains cas.
Car il y a plus grave encore : pour obtenir les données il faut taper un ordre SQL spécifique à la base cible... D'une part cela n'est pas portable, d'autre part ce n'est que du texte pour le compilateur. On écrirait "coucou" à la place de l'ordre SELECT ça compilerait tout aussi bien.
Je ne vous refais pas l'énumération des problèmes en cas de coquille (fort probable) dans une longue chaîne SQL. Je vous épargnerais aussi la liste des ennuis si l'admin de la base de données change le nom d'un champ ou le type de l'un d'entre eux ou s'il réorganise une table en la coupant en 2 ou 3 autres pour des raisons d'optimisation. C'est tout le soft qui devra être corrigé à la main sans rien oublier.
Bref, aussi génial que soit le framework .NET, aussi puissant que soit ADO.NET, on en était encore au même style de programmation des accès aux données que ce que ADO Win32 ou dbExpress chez Borland avec Delphi permettaient. Une logique proche, collée devrais-je dire, au schéma physique de la base de données et totalement inféodée à son dialect SQL, le tout avec des accès non typés aux données.
Certes ADO.NET 2.0 permettait d'aller un cran plus loin avec la génération des DataSet typés sous VS 2005. Une réelle avancée, mais qui n'était qu'un "arrangement" avec le modèle physique.
L'accès aux données avec ADO.NET Entity Framework
Dans cet exemple (qui fait exactement la même chose que le précédent) et dont nous comprendrons plus loin les détails on peut distinguer plusieurs choses :
-
Il n'y a plus de SQL, il a disparu.
-
On ne s'occupe plus de l'ouverture et de la fermeture de la connexion ni même de sa création.
-
Les données sont interrogées en C#, bénéficiant d'Intellisense et du contrôle syntaxique.
-
On itère à travers une liste d'objets dont les propriétés sont typées.
Non, ne cherchez pas l'astuce de ce côté là... si si, vous vous dîtes "le SQL a été tapé ailleurs il est simplement caché maintenant". Non. Dans cette façon d'accéder aux données personne n'a tapé de SQL, personne n'a créé de connexion à la base. Enfin, si, quelqu'un s'est chargé de tout cela de façon transparente : l'Entity Framework.
L'évolution dans les accès aux données
Sur le schéma ci-dessus on voit en colonne, respectivement : le niveau d'abstraction, le langage d'interrogation et le résultat des requêtes. En ligne nous trouvons ADO.NET 2.0 et, en dessous, ADO.NET EF.
-
Le niveau d'abstraction de ADO.NET 2.0 est de type bas niveau, c'est à dire qu'il colle au schéma de la base de données (le modèle physique, le MPD).
Le niveau d'abstraction de EF change totalement puisqu'ici on travaille en haut niveau, celui du schéma conceptuel (le MCD).
-
Côté langage d'interrogation, ADO.NET repose sur le dialecte SQL de la base cible exprimé sous la forme de chaînes de caractères dans le code.
De son côté EF offre LINQ, ou Requêtage intégré au Langage (Language-Integrated Query) totalement indépendant de la base cible.
-
Enfin, pour les résultats des requêtes, ADO.NET offre un accès non typé source d'erreurs, là où EF offre des objets aux propriétés typées.
On le voit ici clairement, le bon technologique est faramineux, c'est bien au niveau d'un modèle conceptuel qu'on va travailler en ignorant tout du modèle physique et du langage de la base de données !
C'est d'autant plus extraordinaire lorsque, comme moi, on commence à avoir quelques heures de vols et qu'on a connu moultes tentatives couteuses et infructueuses chez plusieurs éditeurs de logiciels de créer des couches d'abstraction pour accéder aux données de différentes bases sans modifier l'application... Ce rêve de pouvoir fournir qui sa compatibilité, qui son logiciel de gestion médical sous SQL Server autant que Oracle ou MySQL en configurant juste l'application mais sans la recompiler ni même en maintenir plusieurs versions, ce rêve est aujourd'hui une réalité, une extension naturelle de .NET !
Pour l'instant tout ceci se trouve au niveau du développeur mais il semble évident qu'on puisse pousser la logique au niveau de l'utilisateur. J'y suis particulièrement sensible puisque, vous le savez certainement, je suis l'auteur et l'éditeur de MK Query Builder, un ensemble de composants permettant à l'utilisateur final d'interroger librement les données d'une application (il s'agit de ce qu'on appelle un requêteur visuel ou Visual Query Builder). Quand je vois le temps, le code nécessaire pour offrir modestement à mon niveau les fonctionnalités avancées de MKQB aux utilisateurs finaux, je ne peux que rester admiratif et ressentir un respect immense pour l'équipe Microsoft qui a développée LINQ et l'Entity Framework !
L'Entity Data Model (EDM)
L'EDM est le schéma conceptuel des données dans EF. C'est un vocabulaire qui décrit le schéma conceptuel pour être exact. Visual Studio 2008 le représente graphiquement sous forme d'entités et de liens comme un diagramme de classe.
Il permet au développeur de fixer le cadre des données qu'il souhaite voir, conceptuellement et sans rapport direct avec le schéma physique de la base de données.
EDM représentent d'une part les entités et des relations.
Les entités sont des types distincts (des classes) qui possèdent des propriétés qui peuvent être simples (scalaires) ou complexes. Les relations décrivent pour leur part la façon dont les entités sont liées. Il s'agit d'une déclaration explicites des noms de relation et des cardinalités.
Mais quand met-on les mains dans le camboui ? Il faut bien qu'à un moment ou un autre ce modèle conceptuel puisse correspondre avec le schéma de la base de données... Oui, bien entendu, mais une fois encore, pas de bas niveau ici.
Le schéma EDM peut être créé et modifié totalement graphiquement par le développeur (sous VS 2008) mais ce dernier possède aussi le moyen de faire le mapping entre les entités "idéales" de EDM et les entités physiques de la base de données. Pour cela deux façons coexistent : la plus simple consiste à faire un drag'n drop entre l'onglet des connexions SGDB de VS 2008 et l'espace de travail du schéma EDM ! On prend des tables, on les pose, et le schéma s'écrit de lui-même, relations comprises.
Par exemple la pattern courante "many to many" qui relie une table de lycéens à la table des options suivies par chacun fait intervenir une troisième table dans le schéma physique, celle qui contient les paires de clés lycéen/option. Ainsi un lycéen peut suivre plusieurs options et une option peut être suivies par plusieurs lycéens. Un classique des classiques. Cette pattern est détectée par EDM qui créé automatiquement une jointure n-n entre les deux entités, comme dans le schéma conceptuel de la base de données.
EDM automatise ainsi 95% de la création du schéma conceptuel, même si celui-ci ressemble encore beaucoup au modèle physique de la base. C'est là qu'interviennent les 5% restant : le développeur peut à sa guise supprimer ou ajouter des champs, des relations, et il indique explicitement à EDM comment les mapper sur les tables existantes dans la base de données.
Prenons l'exemple d'une base de données correctement optimisée et standardisée. Une fiche client pouvant posséder une adresse de livraison et une adresse de facturation, le concepteur de la base à séparer les deux informations. On a d'une part une table des clients et de l'autre une table des adresses plus deux jointures AdresseFacturation et AdresseLivraison qui parte de Client vers Adresse avec une cardinalité 0..1/1..1, c'est à dire qu'un client peut posséder 0 ou 1 adresse de facturation, 0 ou 1 adresse de livraison et que chaque adresse doit correspondre à 1 et 1 seul client (si on ajoute un type dans Adresse pour indiquer Facturation ou Livraison, le MCD pourra d'ailleurs exprimer la cardinalité sous la forme d'un lien dit identifiant avec Client).
Sous EDM, si on place les tables par drag'n drop, par force on trouvera dans le schéma les deux entités Client et Adresse avec ses liens. Toutefois cela n'est pas réellement intéressant. D'un point de vue conceptuel nous allons travailler sur des fiches Client, et ses fiches possèdent une adresse de livraison et une autre de facturation. Pour nous, conceptuellement, ces adresses ne sont que des propriétés de Client. On se moque totalement des problèmes d'optimisation ou des règles de Codd que l'admin de la base de données a ou non suivi pour en arriver à créer deux tables... Cela appartient au modèle physique et nous ne voulons plus nous embarrasser de sa lourdeur et de son manque de pertinence conceptuelle.
Le développeur peut alors ajouter à Client deux propriétés, AdresseFacturation et AdresseLivraison, qui seront des propriétés complexes (retournant une fiche Adresse). Il supprimera aussi l'entité Adresse du schéma et enfin indiquera à EDM le mapping de ces deux nouveaux champs.
A partir de maintenant l'application pourra obtenir, trier, filtrer, des instances de Client qui possèdent, entre autres propriétés, une adresse de livraison et une autre de livraison. Plus aucun lien avec le schéma de la base de données, uniquement des choses qui ont un sens du point de vue conceptuel. C'est EF qui s'occupera de générer les requêtes sur les deux tables pour créer des entités Client.
Cela fonctionne aussi en insertion et en mise à jour... Le rêve est bien devenu réalité. Mieux, on peut personnaliser les requêtes SQL (en langage de la base cible) si vraiment on désire optimiser certains accès. De même on peut utiliser des procédures stockées au lieu de table ou de vues, etc.
L'aspect purement génial de EF c'est qu'il offre un mode automatique totalement transparent pour travailler au niveau conceptuel sans jamais perdre la possibilité, à chaque niveau de sa structure, de pouvoir intervenir manuellement. Qu'il s'agisse des entités, de leur mappings, des relations autant que la possibilité de saisir du code SQL de la base cible. Certes ce genre de choses n'est absolument pas à conseiller, en tout cas pour l'écriture manuelle de SQL, c'est un peu un contre emploi de EF... Mais que la possibilité existe montre surtout l'intelligence de la construction de EF qui n'est pas une "boîte noire" sur laquelle il est impossible d'intervenir et qui, par force, limiterait les possibilités dans certains cas.
Interroger les données
Pour interroger les données, EF nous donne le choix entre deux méthodes :
LINQ to Entities
C'est l'exemple de code donné en début de ce billet. LINQ To Entities est une extension de C# qui possède des mots clés proches de SQL (from, where, select..). Mais soyons clairs : LINQ to Entities permet d'interroger le modèle conceptuel, l'EDM, et non pas la base de données ! C'est le mélange entre LINQ et EDM qui permet à EF de générer lui-même le code SQL nécessaire.
LINQ est d'une extraorinaire puissance. D'abord on reste en C#, on bénéficie donc de Intellisense, du contrôle de type, etc. Ensuite, LINQ to Entities n'est qu'une des émanations de LINQ puisqu'il en existe des variantes comme LINQ to XML qui permet globalement la même chose mais non plus en interrogeant des schémas EDM mais des données XML. Le fonctionnement de LINQ dépasse le cadre de ce billet et j'y reviendrai tellement il y a de choses à en dire.
Entity SQL
Si tout s'arrêtait là, Entity Framework serait déjà énorme. J'en pers un peu mes superlatifs tellement je trouve le concept et son implémentation aboutis. Je suis un fan de EF et de LINQ, vous l'avez remarqué et vous me le pardonnerez certainement une fois que vous l'aurez essayé...
Mais en fait EF propose un second moyen d'interroger les données, la tour de babel SQL, c'est à dire Entity SQL.
Il existe en effet de nombreux cas où LINQ pourrait s'avérer plus gênant que génial. Par exemple dans certains cas où le requêtage doit être dynamique (la requête est construite en fonction de choix de l'utilisateur, un écran de recherche multicritère par exemple). Décrocher de EF pour ces cas (pas si rares) imposerait de revenir à du SQL spécifique de la base cible, écrit dans des strings non testables, recevoir des données non typées, créer et gérer des connexions à la base en parallèle de LINQ utilisé ailleurs dans le code, et plein d'autres horreurs de ce genre.
Heureusement, Entity SQL existe. Il s'agit d'un langage SQL spécial, universel dirons-nous, qui ajoute la sémantique EDM à SQL. Ce SQL là est spécial à plus d'un titre donc. Le premier c'est qu'il permet d'interroger l'EDM, le modèle conceptuel, comme LINQ, mais en SQL. Le second c'est que ce SQL "comprend l'objet" et sait utiliser non pas des tables et des champs mais des entités et des propriétés. Enfin, il est universel puisque lui aussi, comme LINQ, sera traduit par EF en "vrai" SQL de la base cible. Le même code Entity SQL fonctionne donc sans modification ni adaptation que la base soit Oracle, MySQL ou SQL Server !
Le rêve de certains éditeurs de logiciels dont je parlais plus haut s'arrêtait d'ailleurs là : créer un SQL interne à l'application qui serait traduit en SQL cible par un interpréteur lors de l'exécution. L'un des projets comme celui-là qui me reste en tête à coûté une fortune pour ne jamais marcher correctement... Entity SQL lui fonctionne de façon performante et s'intègre à Entity Framework ce qui permet de bénéficier de l'abstration conceptuelle et de l'environnement objet. La réalité dépasse largement le rêve.
La traduction des requêtes
Toutes les requêtes sont exécutées par la base de données. Je le sous-entend depuis le début mais c'est mieux de le (re)dire pour qu'il n'y ait pas de méprise...
Les requêtes LINQ et Entity SQL sont transformées en requêtes SQL de la base cible par Entity Framework grâce à l'EDM qui contient la description des entités, des jointures et du mapping.
LINQ et eSQL fonctionnent sur le même pipeline de requêtag. Il transforme les constructions de EDM en tables, vues et procédures stockées. Ce socle commun est basé sur un modèle de fournisseurs qui autorise la traduction vers différents dialectes SQL (donc des bases cibles différentes).
EntityClient, le fournisseur d'accès aux données de EF
"N'en jetez plus !" pourraient s'écrier certains... Et oui, ce n'est pas fini. En effet, et comme je le soulignais, Entity Framework permet d'intervenir à tous les niveaux, du plus haut niveau d'abstraction au plus bas, dans la cohérence et sans pour autant être obligé de faire des choix ou se passer de tel ou tel avantage.
On a vu que par exemple on pouvait injecter du SQL cible dans le modèle EDM, même si cela n'est pas souhaitable, c'est faisable. On a vu qu'on pouvait préférer eSQL à LINQ dans certains cas. De même on peut vouloir totalement se passer de l'objectivation des entités pour simplement accéder aux données de façon "brutes" mais sans perdre tous les avantages de EF.
Pour ce cas bien particulier EF propose un fournisseur de données appelé EntityClient. Il faut le voir comme n'importe quel fournisseur de données ADO.NET. Il propose le même fonctionnement à base d'objets Connexion, Command et DataReader. On s'en sert comme on se sert d'un fournisseur SQL Server ou OleDB sous ADO.NET 2.0.
Toutefois, EntityClient interroge lui aussi le schéma conceptuel, l'EDM. Le langage naturel de ce provider d'accès est eSQL (Entity SQL) présenté plus haut.
Utiliser EntityClient peut s'avérer utile lorsque l'application n'a pas du tout besoin d'objets, ou lorsqu'elle possède sa propre couche objet (business object layer par exemple) qu'elle souhaite alimenter sans passer par les instances d'entités de EF tout en bénéficiant de l'abstraction de EF (utiliser un SQL universel notamment).
Les métadonnées
L'Entity Framework fournit un service d'accès aux métadonnées. Il s'agit d'une infrastructure commune permettant de décrire le stockage, le modèle, et la structure des objets, les procédures stockées, les informations de mapping et même les annotations personnalisées.
L'API publique permet de créer et d'utiliser des MetadataWorkspace directement, d'obtenir des MetadataWorkspace depuis des ObjectContext, d'utiliser les métadata retournées par les enregistrements de données de EntityClient.
Conclusion
La présentation de Carl Perry était entrecoupée de quelques démos permettant bien entendu de mieux voir et comprendre les concepts exposés. Visual Studio 2008 permet d'utiliser l'Entity Framework de façon simple et naturelle, le voir est toujours préférable à le croire sur parole. En tant que témoin de la session, et ayant "joué" avec la bêta de VS 2008 bien avant et étant depuis hier comme tous les abonnés MSDN l'heureux possesseur de la version finale de VS 2008, je peux vous certifier que tout ce que j'ai dit ici est.. en dessous de la vérité ! Testez vous-mêmes Entity Framework et vous comprendrez à quel point les temps ont changé, vous sentirez par vous-mêmes ce fantastique bond technologique qui relègue toutes les autres tentatives de ce type ou approchant (comme ECO de Borland par exemple) au rang des antiquités d'un autre millénaire... Entity Framework c'est une claque aussi forte que la sortie de .NET lui-même, on ne programmera jamais plus après comme on le faisait avant..
Le pass vient d'arriver par la poste, l'heure approche ! Olé !

(j'ai brouillé le datagram, sait-on jamais..)