• [^] # Re: l'héritage et les exemples pourris

    Posté par . En réponse au lien « Clean code » : performances lamentables. Évalué à 4.

    La semaine dernière, je n'avais pas assez de temps pour développer ma pensée et revenir sur le fond de l'article du lien; aujourd'hui je le peux.

    Je voudrais d'abord revenir sur un point qu'il rejette : l'encapsulation ou les types de données abstraits, résumés sous le principe "Code should not know about the internals of objects it’s working with". C'est là un principe que je ne renierai jamais, et dont les développeurs OCaml font un usage abondant : on s'en fout, la plupart du temps, de la représentation concrète d'un type, ce que l'on veut connaître c'est son interface ou API, c'est-à-dire ce que l'on peut faire avec.

    J'ai toujours aimé la présentation qu'en faisait John C. Reynolds dans son article Types, abstraction and parametric polymorphism. Il présente l'idée sous la forme d'une fable où des étudiants d'université suivent des cours sur les nombres complexes. Certains étudiants ont pour professeur Descartes qui les définit comme une paire de nombres réels (partie réelle et partie imaginaire), tandis qu'une autre partie des étudiants a pour professeur Bessel qui les définit par leurs coordonnées polaires. Chacun définit ensuite les notions usuels sur les complexes. Puis au cours du semestre, les deux sections sont interchangées, et pourtant les étudiants ne rencontre aucune difficultés bien que les représentations concrètes choisies par chacun des enseignants soient distinctes. C'est cela l'essence des types de donnés abstraits.

    Bon revenons à nos moutons, et à l'article qui critique ce principe pour des raisons de performances. Dans un premier temps, il ne fait pas que suivre ce principe mais applique toute la panoplie de la POO. Ensuite, au lieu de faire une hiérarchie de classes, il va définir un type somme de quatre figures et faire un switch statement pour comparer les performances. Ce qui revient à implémenter quelque chose du genre :

    module Forme_simple = struct
     type t =
     | Square of float
     | Recangle of float * float
     | Triangle of float * float
     | Circle of float
     let area = function
     | Square width -> width *. width
     | Rectangle (length, witdth) -> length *. width
     | Triangle (base, height) -> 0.5 *. base *. height
     | Circle radius -> Float.pi *. radius *. radius
    end

    Maintenant, constatant que les formules de calculs de surfaces ont des similarité, il tente son optimisation ultime. En Ocaml cela ressemblerait à cela :

    module Forme_simple_bis = struct
     type t = {
     kind : int ;
     width : float ;
     height : float ;
     }
     let square w = {kind = 0; width = w; height = w}
     let rectangle width height = {kind = 1; width; height}
     let triangle width height {kind = 2; widht; height}
     let circle radius = {kind = 3; width = radius; height = radius}
     let coeff = [|1.0 ; 1.0; 0.5; Float.pi|]
     let area {kind; width; height} = coeff.(kind) *. width *. height
    end

    Il y a déjà un point problématique dans son code s'il ne cache pas la représentation de son type à l'extérieur : comment garantir l'invariant qu'un carré a nécessairement une largeur égale à sa hauteur ?

    Ensuite au niveau des interfaces, chacun des deux modules peuvent se voir donner exactement la même :

    module type Forme_simple = sig
     type t
     (* constructeurs de formes *)
     val square : float -> t
     val rectangle : float -> float -> t
     val triangle : flaot -> float -> t
     val circle : float -> t
     (* méthodes sur les formes *)
     val area : t -> float
    end

    Et maintenant, il y a même mieux entre ces deux modules : algébriquement, ces deux structures sont totalement isomorphes (comme pour Descartes et Bessel dans la fable de Reynolds). Autrement dit, elles sont complétement équivalentes pour l'utilisateur et, en tant qu'implémenteur, je peux passer de l'une à l'autre sans perturber aucunement le code client, à la condition de respecter le principe Code should not know about the internals of objects it’s working with. ;-)

    Enfin, si l'on revient à mon graphe de la catégorie des structures algébriques correspondantes, comme pour le type Top il n'y a pas qu'une implémentation possible pour la somme de quatre structures. Ce qu'il a constaté c'est qu'en choisissant une implémentation adaptée, on peut avoir un code plus performant. En revanche, si on ne veut pas perturber le code client de nos modules, il faut cacher les détails d'implémentation. D'ailleurs, si seule la méthode area l'intéresse, je peux lui proposer un choix encore plus drastique pour la représentation du type t, à savoir type t = float :-P Autrement dit, on calcule une bonne fois pour toute la surface à la construction (ce qui ne marche réellement que si les valeurs sont immuables ;-)

    Une dernière petite remarque pour la route : ces deux modules de formes simples ne sont, par contre, pas isomorphes à la structure Top de la catégorie (celle que l'on manipule en faisant de la POO). Elles n'en sont que des sous-structures, car elles ne contiennent pas toutes les formes possibles et imaginables mais seulement quatre. Ce qui signifie que toutes le formes simples sont des formes, notion qui n'a absolument rien à voir avec l'héritage et une hiérarchie de classes (mais c'est là une toute autre histoire... ;-).

    Sapere aude ! Aie le courage de te servir de ton propre entendement. Voilà la devise des Lumières.