\section{Déclaration des classes}
Dans la majorité des langages objets, la définition de l'objet contient les champs et les méthodes.
En R, la définition ne contient que les champs. Les méthodes sont précisées ensuite.
\subsection{Les champs}
La premiere étape est donc de définir les champs de l'objet proprement dit.
Cela se fait à l'aide de l'instruction 'setClass'. setClass est une fonction qui
prend comme argument le nom de la classe, la liste des champs et
quelques autres broutilles, que nous verrons plus tard. Le nom de l'objet est
donnée par 'Class' et les champs sont données sous forme de liste par
'representation'.
Comme nous l'avons vu dans l'exemple introductif, la programmation objet fait du contrôle de type, c'est à dire qu'elle ne permettra pas
à une chaine de caracters d'être ranger la ou devrait se trouver en entier.
Chaque champ doit être déclaré avec son type.
Code : Tout sélectionner
setClass(
Class="trajectoires",
representation=list(
temps = 'numeric',
traj = 'matrix'
)
)
[[[2. Question aux connaisseurs S4 : on peut soit utiliser "representation=list(temps = 'numeric',traj = 'matrix')" soit "representation(temps = 'numeric',traj = 'matrix')". Cela fait-il une différence ?]]]
On peut ensuite créer un objet 'trajectoire' grace à new :
Comme vous pouvez le constater, l'affichage n'est pas terrible. Il
sera important de définir une méthode pour arranger ca.
Naturellement, un objet se stocke dans une variable comme n'importe
quelle autre valeur de R.
On peut ensuite acceder aux champs de trajCochin grace à l'opérateur @ :
Code : Tout sélectionner
trajCochin@temps
[1] numeric(0)
trajCochin@temps <- 3
trajCochin
An object of class "trajectoires"
Slot "temps":
[1] 3
Slot "traj":
<0 x 0 matrix>
\paragraph{Attention} : comme nous le verrons par la suite, l'opérateur '@' est à utiliser avec parcimonie. En fait, il ne devrait jamais être utiliser sauf dans les méthodes.
L'utilisation que nous présentons ici (affichage d'un champ, et pire encore affectation d'une valeur à un champ) est à proscrire.
Par contre, on peut créer un objet en lui donnant directement les valeurs qu'il doit affecter à ses champs :
Code : Tout sélectionner
traj1 <- matrix(c(1,2,3,2,1,4),nrow=2)
trajStAnne <- new("trajectoires",temps=c(1,23),traj=traj1)
Enfin, on aurait pu déclarer un objet en lui donnant des valeurs par défaut. A chaque création, si l'utilisateur ne spécifie pas les valeurs des champs, ceux-ci en auront quand même une.
On fait cela grace à 'prototype'
Code : Tout sélectionner
setClass(
Class="trajectoiresBis",
representation=list(
temps = 'numeric',
traj = 'matrix'
),
prototype=list(
temps = 1,
traj = matrix(0)
)
)
Dans le cas précis de l'objet 'trajectoires', il n'y a pas vraiment de valeur par défaut qui s'impose.
Il est donc préférable de conserver la classe comme elle a été initialement définie.
Voila, nous avons défini notre premier objet. Par contre, j'espère que vous êtes plus adroit que moi :
dans la définition de trajStAnne, j'ai fait une petite erreure de typo, j'ai tapé '1,23' au lieu de '1,2,3'.
Du coup, il y a moins de 'temps' qu'il n'y a de colonne dans 'traj' et notre objet est incohérant.
Naturellement, R n'a rien vu. Mais il est possible de l'éduquer...
\subsection{Généralitées sur les méthodes}
Les méthodes sont définies grâce à la fonction 'setMethod'. Cette fonction prend pour premier argument le nom de la méthode qui est en train d'être définie, comme second le nom
de l'objet et comme troisième la fonction à exécuter quand cette méthode est appelée. Par exemple
Code : Tout sélectionner
setMethod("print",
"trajectoires",
function(x,...){cat("*** Voila des trajectoires mesurées aux temps ",x@temps," ***\n")}
)
printPourTrajectoire(trajStAnne)
On distingue trois types méthodes : celles directement liées à la programmation objet (comme par exemple les constructeurs, que nous verons bientot),
celles qui viennent imiter des fonctions déja existantes (comme 'print' pour l'objet 'trajectoire') et celles totalement nouvelles inventées par l'utilisateur.
\subsection{Méthodes liées à la programmation objet}
Ces méthodes permettent de faire certainnes vérifications sur les objets, de les contrôler, de manipuler leur structure.
\subsubsection{Vérificateur}
Le vérificateur est là pour contrôler qu'il n'y a pas d'incohérance interne dans l'objet. On lui donne des règles, et à chaque création d'objet, il vérifiera que l'objet suit les règles.
Dans le cas de l'objet trajectoire,
il doit y avoir autant de colonnes dans la matrice 'traj' que dans 'temps'. Nous allons donc demander au vérificateur de s'en assurer. Le vérificateur est une méthode un peu part.
Il n'est pas défini avec 'setMethod' mais avec 'setValidity' a qui nous donnons le nom de l'objet,
puis une fonction de vérification. La fonction de vérification prend comme argument l'objet, et retourne soit TRUE, soit une erreure :
Code : Tout sélectionner
setValidity("trajectoire",
method=function(object){
if(length(object@temps)!=ncol(object@traj)){
stop("[trajectoire:validation] Le nombre de mesures temporelles ne correspond pas au nombre de colonnes de la matrice")
}else{
return(TRUE)
}
}
)
Si nous essayions maintenant de definir trajStAnne comme nous l'avions fait précédement, le vérificateur nous en empècherait :
Code : Tout sélectionner
trajStAnne <- new("trajectoire",temps=c(1,23),traj=traj1)
Erreur dans validityMethod(object) :
aucun slot de nom "temps" pour cet objet de la classe "maclasse"
trajStAnne <- new("trajectoire",temps=c(1,2,3),traj=traj1)
Au passage, une remarque générale : quand vous faites un programme long avec de nombreux objets et de nombreuses fonctions, il est toujours difficile, en cas d'erreur, de savoir
quelle fonction vient de stoper.
[[[4. Peut-être parce que je ne maitrise pas les outils de débugage ?]]] Donner un message d'erreur précis qui indique le nom de l'objet
et la méthode incriminée aide grandement.
\paragraph{Attention :} le vérificateur n'est appellé QUE lors de la création initiale de l'objet. Si ensuite il est modifié, rien ne va plus, il n'y a plus de contrôle :
Code : Tout sélectionner
trajStLouis <- new("trajectoire",temps=c(1),traj=matrix(1))
trajStLouis@temps <- c(1,23)
Voila une des raisons qui poussent à proscrire l'utilisation de '@' pour modifier les valeurs des champs.
[[[5. Question aux connaisseurs S4 : est-il possible de créer un vérificateur qui contrôle également les modifications a pas seulement la création ?]]]\subsubsection{Constructeur}
Le vérificateur est une version simplifié d'un outil bien plus puissant appelé le constructeur. Le constructeur est une méthode permettant de fabriquer un objet. Il est appelé à chaque utilisation
de la fonction 'new'.
Reprenons nos trajectoires. Il serait assez plaisant que les colonnes de la matrice des trajectoires aient des noms, les noms des temps où les mesures ont été prises. De même, les lignes
pourraient être indicées par un numero d'individu :
Code : Tout sélectionner
T0 T1 T4 T5
I1 15 15.1 15.2 15.2
I2 16 15.9 16 16.4
I3 15.2 15.2 15.3 15.3
I4 15.5 15.6 15.8 16
Le constructeur va nous permettre de faire tout ca. Le constructeur est une methode qui, lors de l'appel de 'new', fabrique l'objet tel que nous le voulons.
Le nom de la méthode constructeur dans R est 'initialize'. 'initialize' fait appel à une fonction qui prend pour argument '.Objet',
l'objet qui est train d'être construit et les différentes valeur a affecter aux champs de l'objet.
Cette fonction doit se terminer par l'affectation des valeurs aux champs de .Object puis par un 'return(.Object)' :
Code : Tout sélectionner
setMethod("initialize",
"trajectoire",
function(.Object,temps,traj){
colnames(traj) <- paste("T",temps,sep="")
rownames(traj) <- paste("I",1:nrow(traj),sep="")
.Object@temps <- temps
.Object@traj <- traj
return(.Object)
}
)
new("trajectoire",temps=c(1,2,4,8),traj=matrix(1:8,nrow=2))
A noter que si un constructeur est défini, le vérificateur est désactivé. Dans notre cas, si 'temps' comporte moins de valeurs que de colonne dans traj,
le constructeur buggue. Si 'temps' comporte moins de valeurs que de colonne dans traj et que notre constructeur ne buggait pas
(par exemple si on commentait la ligne 'colnames(traj) <- paste("T",temps,sep="")', le vérificateur ne serait tout de même pas appelé.
[[[6. Question aux connaisseurs S4 : Du coup, le vérificateur présente-t-il un intéret ?]]]Si on décide d'utiliser un constructeur, il faut donc y inclure les vérifications. Notre constructeur, fusionné avec le vérificateur, devient :
Code : Tout sélectionner
setMethod("initialize",
"trajectoire",
function(.Object,temps,traj){
if(length(object@temps)!=ncol(object@traj)){
stop("[trajectoire:initialisation] Le nombre de mesures temporelles ne correspond pas au nombre de colonnes de la matrice")
}else{
colnames(traj) <- paste("T",temps,sep="")
rownames(traj) <- paste("I",1:nrow(traj),sep="")
.Object@temps <- temps
.Object@traj <- traj
return(.Object)
}
}
)
new("trajectoire",temps=c(1,2,4,8),traj=matrix(1:8,nrow=2))
A noter qu'un constructeur ne prend pas nécessaire pour argument les champs de l'objet. Par exemple, si on sait (ca n'est pas le cas dans la réalité, mais imaginons) que l'IMC
augmente de 0.1 toutes les semaines, on pourrait construire des trajectoires en fournissant le nombre de semaine et les poids initiaux :
Code : Tout sélectionner
setMethod("initialize",
"trajectoire",
function(.Object,nbSemaine,IMCinit){
traj <- outer(IMCinit,1:nbSemaine,function(init,nbSem){return(init+0.1*nbSem)})
colnames(traj) <- paste("T",1:nbSemaine,sep="")
rownames(traj) <- paste("I",1:nrow(traj),sep="")
.Object@temps <- 1:nbSemaine
.Object@traj <- traj
return(.Object)
}
)
new("trajectoire",nbSemaine=4,IMCinit=c(16,17,15.6))
A noter que cette nouvelle définition a supprimer l'ancienne.
[[[7. Question aux connaisseurs S4 : est-il possible de définir plusieurs constructeur, en fonction des arguments qu'on leur donne ?]]]