L'objectif majeur du paradigme orienté objet est d'encapsuler les détails d'implémentation. C'est pourquoi, en
ce qui concerne la persistance, nous aimerions obtenir un objet persistant ayant exactement la même apparence
qu'un objet transitoire. Nous ne devrions pas avoir à prendre en compte le fait que l'objet soit persistant, ou à le
traiter de manière différente d'un autre objet. C'est du moins l'objectif.
En pratique, il peut arriver que l'application ait besoin de contrôler différents aspects de persistance :
-
lorsque des objets persistants sont lus et écrits
-
lorsque des objets persistants sont supprimés
-
manière dont les transactions sont gérées
-
manière dont le contrôle du verrouillage et des accès concurrents est accompli
Deux cas sont concernés : la première fois où l'objet est écrit dans la mémoire de l'objet persistant, et les fois
consécutives lorsque l'application veut mettre à jour la mémoire de l'objet persistant en prenant en compte un
changement effectué sur l'objet.
Dans chaque cas, le mécanisme spécifique dépend des opérations prises en charge par la structure de persistance.
Généralement, le mécanisme utilisé est d'envoyer un message à la structure de persistance pour créer l'objet
persistant. Une fois que l'objet est persistant, la structure de persistance est suffisamment intelligente pour
détecter les changements consécutifs affectant l'objet persistant et les écrit dans la mémoire de l'objet persistant,
si nécessaire (en règle générale, lorsqu'une transaction est validée).
Un exemple de création d'un objet persistant est illustré ci-dessous :
L'objet PersistenceMgr est une instance de VBOS, une structure de persistance. OrderCoordinator crée un ordre
persistant en l'envoyant en tant que paramètre effectif à un message 'createPersistentObject' vers PersistenceMgr.
Il n'est généralement pas nécessaire de modéliser ceci de manière explicite sauf s'il est important de savoir
que l'objet est explicitement stocké à un point spécifique dans une certaine séquence d'événements. Si des opérations
consécutives nécessitent d'émettre une requête sur l'objet, celui-ci doit exister dans la base de données, et c'est
pourquoi, il est important de savoir que l'objet existe à cet endroit.
La récupération d'objets à partir de la mémoire de l'objet persistant est nécessaire avant que l'application ne puisse
envoyer des messages à cet objet. Rappelez-vous que le travail dans un système orienté objet est réalisé par le biais
d'envoi de messages aux objets. Mais si l'objet auquel vous voulez envoyer un message est dans la base de données mais
pas encore en mémoire, un problème se posera : vous ne pouvez pas envoyer un message vers quelque chose qui n'existe
pas encore!
Bref, vous devez envoyer un message à un objet sachant émettre une requête sur la base de données, récupérer l'objet
correct et l'instancier. Ensuite, et seulement ensuite, vous pouvez envoyer le message prévu à l'origine. L'objet
instanciant un objet persistant est souvent appelé objet fabrique. Un objet fabrique a pour rôle de créer
des instances d'objets, y compris les objets persistants. En fonction d'une requête donnée, l'objet fabrique
peut être conçu pour renvoyer un ensemble constitué d'un ou plusieurs objets correspondant à la requête.
De manière générale, les objets sont connectés les uns aux autres par l'intermédiaire de leurs associations, il est
donc souvent uniquement nécessaire de récupérer l'objet racine dans un graphique d'objet ; le reste étant
essentiellement 'extrait' de manière transparente de la base de données par les associations avec l'objet racine. (Un
bon mécanisme de persistance est suffisamment intelligent pour ne récupérer que les objets dont on a besoin ; sinon,
cela pourrait aboutir à instancier un grand nombre d'objets sans utilité. La récupération d'objets avant d'en avoir
réellement besoin constitue l'un des problèmes de performance principaux qui résultent de mécanismes de persistance
simplistes.)
L'exemple suivant illustre la manière de modéliser la récupération d'objet à partir de la mémoire d'objet persistant.
Dans un diagramme de séquence réel, le SGBD ne serait pas affiché dans la mesure où il devrait être encapsulé dans
l'objet fabrique.
Le problème des objets persistants, c'est leur persistance ! Contrairement aux objets transitoires qui disparaissent
simplement lorsque le processus qui les a créés se termine, les objets persistants ne disparaissent que lorsqu'ils sont
explicitement supprimés. Il est donc important de supprimer l'objet lorsqu'il n'est plus utilisé.
Le problème est que cela est difficile à déterminer. Ce n'est pas parce qu'une application est effectuée avec un objet
que cela signifie que toutes les applications, présentes et futures, le seront. Et dans la mesure où les objets ont des
associations qu'ils ne connaissent même pas, il n'est pas toujours facile de déterminer s'il est possible de supprimer
un objet.
En conception, on peut représenter cela de manière sémantique en utilisant les diagrammes état-transition :
lorsque l'objet se trouve à l'état final, il peut être considéré comme étant libéré. Les développeurs
responsables de l'implémentation des classes persistantes peuvent ensuite utiliser les informations du diagramme
état-transition pour appeler le comportement du mécanisme de persistance approprié afin de libérer l'objet. La
responsabilité du concepteur de la réalisation de cas d'utilisation est d'appeler les opérations appropriées afin que
l'objet puisse atteindre son état final lorsqu'il est approprié de supprimer l'objet.
Si un objet possède de nombreuses connexions vers d'autres objets, il peut s'avérer difficile de déterminer si l'objet
peut être supprimé. Dans la mesure où un objet fabrique connaît la structure de l'objet ainsi que les objets
auxquels il est connecté, il est souvent utile de donner la responsabilité à l'objet fabrique d'une classe de
déterminer si une instance particulière peut être supprimée. La structure de persistance peut également fournir un
support pour cette fonctionnalité.
Les transactions définissent un ensemble d'appels d'opérations qui sont atomiques; soit elles sont toutes
effectuées, soit aucune d'entre elles n'est effectuée. Dans le contexte de la persistance, une transaction définit un
ensemble de changements effectué sur un ensemble d'objets qui sont tous exécutés ou pas du tout exécutés. Les
transactions permettent une cohérence, en garantissant que des ensembles d'objets se déplacent d'un état cohérent à
l'autre.
Il existe différentes options permettant d'afficher les transactions dans les réalisations de cas d'utilisation :
-
Textuellement. A l'aide de scripts dans la marge du diagramme de séquence, les frontières de la transaction
peuvent être documentées comme indiqué ci-dessous. Cette méthode est simple et permet d'utiliser un nombre indéfini
de mécanismes pour implémenter la transaction.
Représenter les frontières de transaction à l'aide d'annotations textuelles.
-
Utilisation de messages explicites. Si le mécanisme de gestion de la transaction utilisé fait appel à des
messages explicites pour commencer et finir les transactions, ces messages peuvent être affichés de manière
explicite dans le diagramme de séquence, comme indiqué ci-dessous :
Diagramme de séquence affichant des messages explicites pour démarrer et arrêter les transactions.
Traiter les cas d'erreur
Si toutes les opérations spécifiées dans une transaction ne peuvent pas être effectuées (en règle générale en raison
d'une erreur), la transaction est avortée et tous les changements effectués lors de la transaction sont
inversés. Les cas d'erreur anticipés représentent souvent des flux d'événements exceptionnels dans les cas
d'utilisation. Dans d'autres cas, les cas d'erreur se produisent en raison de certaines défaillances du système. L es
cas d'erreur doivent également être documentés dans les interactions. Les erreurs et les exceptions simples peuvent
être affichées dans l'interaction où elles se produisent ; les erreurs et les exceptions complexes peuvent nécessiter
leurs propres interactions.
Il est possible d'afficher les modes de défaillance d'objets spécifiques sur les diagrammes état-transition. Le flux
conditionnel de la gestion du contrôle de ces modes de défaillance peut être affiché dans l'interaction dans laquelle
l'erreur ou l'exception se produit.
L'accès concurrent décrit le contrôle d'accès vers les ressources système critiques au cours de la transaction. Afin de
maintenir la cohérence du système, il est possible qu'une transaction nécessite d'avoir un accès exclusif à certaines
ressources clés du système. Cette exclusivité peut inclure la possibilité de lire un ensemble d'objets, d'écrire un
ensemble d'objets ou les deux.
Considérons un exemple simple illustrant la nécessité de restreindre l'accès à un ensemble d'objets. Supposons que nous
exécutons un système d'entrée de commande simple. Les personnes appellent pour passer des commandes qui sont traitées
puis expédiées. La commande peut être considéré comme une sorte de transaction.
Afin d'illustrer la nécessité d'un contrôle des accès concurrents, supposons le cas d'une commande d'une nouvelle paire
de chaussures de randonnée. Lorsque la commande est entrée dans le système, celui-ci vérifie que les chaussures, à la
bonne pointure, sont en stock. Si c'est le cas, nous souhaitons réserver cette paire, de telle manière que
personne ne puisse les acheter avant l'expédition de la commande. Une fois la commande envoyée, les chaussures sont
retirées du stock.
Entre le moment de la commande et celui de l'expédition, les chaussures se trouvent dans un état spécial(),
elles sont en stock, mais elles sont "réservées" par ma commande. Si ma commande est annulée pour quelque raison que ce
soit (je change d'avis ou ma carte de crédit a expiré), les chaussures sont renvoyées vers le stock. Une fois la
commande expédiée, nous supposons que notre petite société ne souhaite pas conserver l'enregistrement qu'elle possédait
les chaussures.
L'objectif de l'accès concurrent, comme les transactions, est de s'assurer que le système passe d'un état cohérent à un
autre. En outre, l'accès concurrent tend à assurer qu'une transaction dispose de toutes les ressources dont elle a
besoin pour terminer son travail. Le contrôle des accès concurrents peut être implémenté de diverses manières, y
compris le verrouillage des ressources, les sémaphores, les verrous de mémoire partagée et les espaces de travail
privés.
Dans un système orienté objet, il est difficile de dire uniquement à partir des patterns d'un message si un message
particulier peut provoquer un changement d'état d'un objet. De même, différentes implémentations peuvent parer à la
nécessité de restreindre l'accès à un certain type de ressources ; par exemple, certaines implémentations fournissent
chaque transaction avec sa propre vue de l'état du système au début de la transaction. Dans ce cas, d'autres processus
peuvent changer l'état et l'objet sans affecter la 'vue' de toute autre transaction d'exécution.
Afin d'éviter d'imposer des contraintes à l'implémentation, en conception, nous indiquons simplement les ressources
auxquelles la transaction doit avoir un accès exclusif. En reprenant l'exemple cité auparavant, nous indiquons que nous
avons besoin d'un accès exclusif à la paire de chaussures commandée. Une alternative simple consiste à annoter la
description du message envoyé, en indiquant que l'application a besoin d'un accès exclusif vers l'objet. L'implémenteur
peut ensuite utiliser ces informations afin de déterminer le meilleur moyen d'implémenter l'exigence d'accès
concurrent. L'exemple ci-dessous illustre l'annotation d'un message nécessitant un accès exclusif. On part du principe
que tous les verrouillages sont relâchés et que la transaction est terminée.
Exemple illustrant le contrôle d'accès annoté dans un diagramme de séquence.
La raison qui incite à de ne pas restreindre l'accès à tous les objets nécessaires dans une transaction résulte souvent
du fait que seuls quelques objets ont des restrictions d'accès ; restreindre l'accès à tous les objets participant à
une transaction gaspille de précieuses ressources et peut créer des goulots d'étranglement en matière de performance
plutôt que de les empêcher.
|