Introduction à la programmation orientée objet avec tcl

 


Par vincent Wartelle le 4 février 2002


A qui s'adresse cette page


Cette page s'adresse aux personnes qui ont pris contact avec le langage Tcl, qui ont quelques connaissances en programmation orientée objet, et qui se demandent comment programmer objet avec tcl.

Différents moyens de faire du tcl orienté objet sont listés ici.


A la base: le mécanisme des namespaces


Le mécanisme des namespaces, présent dans tcl depuis la version 8.0, permet , à condition de le vouloir, une programmation orientée objet, mais sans mécanisme d'héritage.

Ce type de programmation permet d'organiser les procédures autour des structures de données, d'homogénéiser le code (si toute l'application est construite sur ce même principe), et d'éviter la profusion des variables globales.

Exemple: je veux créer la classe "client", dont chaque instance sera identifiée par un numéro arbitraire, et aura pour attributs nom, prénom, ville.

    #
    #    classe client
    #
    namespace eval client {
        variable _props ; # le tableau des attributs de chaque instance
        variable _maxid; # le dernier numéro attribué à une client
        if {[[info exist _maxid]] == 0 } {
            set _maxid -1
        }
    }
    #    constructeur
    proc client::new { args } {
        variable _maxid
        variable _props
        # affectation d'un identifiant à cette instance
        set this [[incr _maxid]]
        foreach { property value } $args {
            set _props($this,$property) $value
        }
        return $this
    }
    #    destructeur
    proc client::delete { this } {
        variable _props
        array unset _props $this,*
    }
    #    accesseur de propriétés (lecture)
    proc client::get_prop { this property } {
        variable _props
        return $_props($this,$property)
    }
    #    accesseur de propriétés (mise à jour)
    proc client::set_prop { this property value } {
        variable _props
        set _props($this,$property) $value
    }
    #
    #    test
    #
    proc client::test {} {
        set p1 [[client::new -nom Dulac -prenom Jean -ville Marseille]]
        set p2 [[client::new -nom Dumont -prenom Louise -ville Bourges]]

        puts "nom de $p1: [[client::get_prop $p1 -nom ]]"
        puts "ville de $p2: [[client::get_prop $p2 -ville]]"
    }

Raffiner le mécanisme des namespaces


Ecrire des interfaces

Avec un peu d'imagination et quelques connaissances de tcl, on peut facilement, toujours avec du tcl standard, mettre au point un mécanisme d'interfaces.

Pour mémoire, en programmation orientée objet, une interface est une liste de fonctions à laquelle doivent se conformer les classes qui implémentent l'interface; ce mécanisme a été popularisé par Java, qui utilise ce principe pour se passer de l'héritage multiple.

Imaginons qu'il y ait, en plus de la classe "client", une classe "fournisseur", avec des attributs assez voisins. On aimerait qu'un composant graphique sache présenter indifféremment un client ou un fournisseur, en accédant à ses propriétés nom, prénom, ville.

Supposons que l'on ait la portion de code suivant pour afficher un client

    ...affiche_client { id } {
        label .l.client -text "client"
        label .l.nom_client -text [[client::get_prop $id nom]] -relief sunken
    } }

Une possibilité est d'écrire notre composant graphique de la façon suivante:

    ...affiche_tiers { classetiers id } {
        label .l.classetiers -text "$classetiers"
        label .l.nom_tiers -text [[[[set classetiers]]::get_prop $id nom]] -relief sunken
    }

En procédant ainsi, nous avons écrit une forme d'interface objet implicite, en utilisant le fait que "client::get_prop" et "fournisseur::get_prop" existent tous les deux et fonctionnent de la même façon.

Ecrire un mécanisme d'héritage maison

On peut, de différentes façons, réinventer des mécanismes qui commencent à ressembler à de l'héritage objet. Par exemple, voici à quoi pourrait ressembler un mécanisme de calcul d'âge, commun à deux classes client et fournisseur.

    proc client::calculer_age { id } {
        return [[tiers::calculer_age client $id]]
    }
    proc fournisseur::calculer_age { id } {
        return [[tiers::calculer_age fournisseur $id]]
    }
    proc tiers::calculer_age { classetiers id } {
        set date_naissance [[[[set classetiers]]::get_prop $id date_naissance]]
        # ici algo de calcul d'âge (...)
        return $age
    }

Cependant cela commence à devenir un peu lourd, quand on compare ce mécanisme à celui que proposent les extensions objets de tcl. En particulier, si dans l'exemple donné, l'algorithme n'est présent qu'une fois, la procédure est quand même écrite trois fois.

Et puis ce n'est pas vraiment de l'héritage: ce qu'il faudrait dans l'exemple ci-dessus, c'est une proc "tiers::calculer_age" ayant id pour seul argument, où id soit indifféremment l'identifiant d'un client ou d'un fournisseur.

Ces limitations m'amènent maintenant à parler des extensions objets de tcl.


Les extensions objets de tcl


Il existe différentes extensions objets pour tcl.

La plus répandue et la plus populaire est "itcl". Le nom complet est Incr Tcl, construit par analogie avec C++ (l'opérateur incr de tcl étant le synonyme du ++ du C.)

D'autres extensions existent, parmi lesquelles XOTCL, ClassyTcl, OTcl, Stooop. Chacune de ces extensions objets est une implémentation originale, qui reprend les mêmes concepts avec une syntaxe différente: aussi en choisir une est forcément structurant.

Personnellement j'ai choisi Itcl, parce qu'il est maintenant (suite à un sondage) inclus dans la distribution ActiveTcl et représente donc "le standard".

Stooop est également un choix intéressant, parce qu'il est très compact et écrit en pur tcl. Et puis (scoop..) il est maintenant distribué dans la tcllib qui fait aussi partie de la distribution ActiveTcl. En revanche, sa syntaxe d'utilisation est différente de celle d'Itcl.

Je ne me prononcerai pas sur les autres extensions objet, que je n'ai pas regardé de plus près.


Comment utiliser Itcl : installation et déploiement


Pour accéder aux commandes itcl, il faut déclarer la variable d'environnement ITCL_LIBRARY et charger la librairie dynamique itcl32.

Sous Windows, si l'on a installé ActiveTcl dans "c:\Program Files\ActiveTcl", cela donne le code suivant:

    if { [[info exists env(ITCL_LIBRARY)]] == 0 } {
        set env(ITCL_LIBRARY) "/program files/activetcl/lib/itcl3.2"
        load "/program files/activetcl/bin/itcl32.dll"
        namespace import itcl::*
    }

Pour déployer un développement réalisé avec itcl, il faudra donc fournir en plus de la distribution Tcl/Tk, les scripts qui sont dans ITCL_LIBRARY, à savoir itcl.tcl et pkgIndex.tcl, et la librairie dynamique (itcl32.dll pour windows).

Si l'on utilise freewrap comme outil de déploiement, cela reste vrai: les scripts "itcl.tcl" et "pkgIndex.tcl" sont wrappables par freewrap, mais la librairie dynamique devra être véhiculée séparément.


Comment utiliser Itcl: un exemple


Par facilité, le même exemple que ci-dessus est repris. Mais cette fois-ci, une classe "tiers", dont hérite véritablement la classe "client", est décrite.

    #  destruction des classes existantes
    #  (pour pouvoir sourcer plusieurs fois ce programme)
    foreach classid [[itcl_info classes]] {
        delete class $classid
    }
    #  classe tiers
    class tiers {
        public variable nom
        public variable prenom
        public variable ville
    }
    #  classe client
    class client {
        inherit tiers
    }
    proc test {} {
        client p1
        p1 configure -nom Dulac -prenom Jean -ville Marseille
        client p2
        p2 configure -nom Dumont -prenom Louise -ville Bourges

        puts "nom de p1: [[p1 cget -nom ]]"
        puts "ville de p2: [[p2 cget -ville]]"
    }

Ce que démontre cet exemple

On voit dans cet exemple que Itcl nous fait gagner en concision! En effet les mécanismes de construction / destruction / accès qui occupaient du code en écriture de type namespace, sont ici inclus dans les fonctions de base d'itcl.

De plus on dispose d'un vrai mécanisme d'héritage, qui fonctionne sans avoir à écrire du code.

On remarque également que les classes itcl fonctionnent comme des widgets Tk: le nom de classe est une commande qui crée des objets. Chaque nom d'objet créé devient ensuite une commande.

Bien sûr cet exemple est quasiment vide. Pour prendre connaissance d'itcl il faut lire la documentation (l'aide d'ActiveTcl, rubrique itcl), en commençant par le mot-clé "class". On y apprend les mots "variable", qui pour itcl change de sens en devenant une variable d'instance, "constructor", "method", "proc" (pour écrire ce qui ailleurs s'appelerait "méthode statique"), etc.


Comment utiliser Stooop: installation et déploiement


A l'inverse d'Itcl, stooop consiste en un unique script tcl.

Il suffit donc de le sourcer, et d'importer les commandes qu'il fournit: le code suivant fait très bien l'affaire.

    #  importe stooop
    set stoooppath /ou_est_stooop
    source $stoooppath/stooop.tcl
    namespace import stooop::*

Pour déployer un développement réalisé avec stooop, il suffit de lui adjoindre le programme stooop.tcl.

Si l'on utilise freewrap comme outil de déploiement, stooop.tcl pourra aisément être wrappé, au même titre que tout autre programme tcl.


Comment utiliser Stooop: un exemple


C'est toujours le même exemple.

    #    classe tiers
    class tiers {
        proc tiers {this args} {
            set arglist [[lrange $args 1 end]]
            foreach { property value } $args {
                set tiers::($this,$property) $value
            }
        }
        proc ~tiers {this} {
        }
        virtual proc get_property { this property } {
            return $tiers::($this,$property)
        }
    }
    #    classe client
    class client {
        proc client {this args} tiers { $args } {
        }
        proc ~client {this} {
        }
    }
    #    test
    proc test {} {
        set p1 [[new client -nom Dulac -prenom Jean -ville Marseille]]
        set p2 [[new client -nom Dumont -prenom Louise -ville Bourges]]
        puts "nom de p1: [[tiers::get_property $p1 -nom]]"
        puts "ville de p2: [[tiers::get_property $p2 -ville]]"
    }

Ce que démontre cet exemple

Stooop offre est plus proche de C++ que ne l'est itcl; les objets créés portent comme identifiant un numéro arbitraire, assimilable à un pointeur.

Les méthodes sont accédées classiquement comme des procédures (là où dans itcl, chaque instance d'objet devient une commande nouvelle).

Dans stooop, pour hériter d'une méthode sans la modifier, il faut appeler la méthode en la préfixant du nom de la classe de base (ici "tiers::get_property").

En fait, l'exemple choisi n'est pas très approprié, car c'est une adaptation de stooop a une fonctionnalité de itcl (configure, cget) qui n'existe pas dans stooop. Par contre, la classe switched livrée avec stooop permet une approche équivalente.

Enfin, il est bon de savoir qu'itcl consomme plus de mémoire que stooop, mais est plus rapide pour certains tests. Chercher "OO Tcl some benchmark results" dans une archive du groupe comp.lang.tcl pour des indications chiffrées.


Conclusion


J'espère que ce petit topo vous aura donné envie d'aller plus loin en tcl objet.

En effet, les gens qui aiment le "scripting" avec Tcl redoutent souvent la programmation objet à la Java parce qu'elle est synonyme de verbosité, avec des déclarations à n'en plus finir...

Mais en regardant de plus près, on s'aperçoit que la programmation objet avec Tcl permet d'écrire du code encore plus lisible et plus concis.