Swift est-il un langage de programmation orientée aspects ?
Pour répondre à cette question, nous devons d’abord comprendre la programmation orientée aspects, ou AOP. J’aime la voir comme une réponse à une certaine forme d’échec des conceptions orientées objet.

Pour répondre à cette question, nous devons d’abord comprendre ce qu’est la programmation orientée aspects, ou AOP en anglais. J’aime voir l’AOP comme une réponse à une certaine forme d’échec des conceptions orientées objet.
L’échec de la programmation orientée objet ?
La programmation orientée objet domine l’art de l’ingénierie logicielle depuis de nombreuses années. Cette approche modularise les systèmes vastes et complexes en unités techniques et fonctionnelles, selon un modèle intuitif et proche du monde réel. Ce paradigme tente d’architecturer les applications en modules, indépendants en principe, qui prennent en charge différents aspects du système. Une application orientée objet est construite autour d’unités porteuses de sens, les « objets », dont le cycle de vie est géré par des modules « métier ».
Des préoccupations transversales apparaissent cependant rapidement et systématiquement, compromettant peu à peu les principes mêmes de la programmation orientée objet. Elles n’ont aucun sens du point de vue de la modélisation : ce sont des aspects purement techniques au service de l’application développée. Elles prennent la forme de modules peu conceptualisés et souvent insuffisamment aboutis en matière d’abstraction.
Quelques exemples de préoccupations transversales :
- la persistance et les transactions ;
- la journalisation ;
- la gestion du cache.
Plus l’application devient vaste et complexe, plus la présence de tels modules pose problème. Le développeur fait alors face à deux phénomènes :
- l’enchevêtrement ;
- la dispersion.
Enchevêtrement
L’enchevêtrement se produit lorsqu’un module métier implémente plusieurs préoccupations transversales. Du code technique apparaît au milieu de modèles et d’une logique métier purs. Le module devient de moins en moins lisible et se concentre de moins en moins sur sa propre préoccupation.
Dispersion
La dispersion se produit lorsque les appels à ces modules techniques se répandent insidieusement dans toute l’application. Toutes les parties du code sont en quelque sorte « contaminées » par l’implémentation des préoccupations transversales.

Voici les phénomènes d’enchevêtrement et de dispersion.
La programmation orientée aspects naît de cet échec
Jusqu’ici, nous avons parlé des « aspects » techniques du code et des dérives qu’ils peuvent entraîner. De ce constat est née une solution élégante : extraire ces aspects, les rendre autonomes et les appliquer à n’importe quelle partie de l’application.
Les préoccupations transversales sont désormais gérées depuis l’extérieur des modules métier, au moyen de composants appelés « Aspects ».

Les Aspects isolent les préoccupations transversales.
Au-delà de sa définition purement conceptuelle, ce nouveau paradigme implique d’adapter les langages afin d’incorporer ces « Aspects » au code métier.
La programmation orientée aspects ne remplace pas la programmation orientée objet. Elle en est, au contraire, le complément idéal pour tendre vers une modélisation purement orientée objet de l’application.
Le rôle du développeur consiste désormais à identifier les « Aspects » à externaliser ainsi que les conditions de leur exécution au sein de la logique métier.
Introduisons quelques concepts formels
La programmation orientée aspects permet de préciser exactement où insérer le code transversal.
Join point : définit, de manière théorique, OÙ un aspect peut être inséré dans le flux d’exécution du code. Les join points les plus courants sont les appels de méthodes et les accès aux propriétés.
Pointcut : définit COMMENT insérer un Aspect au niveau des join points. Il s’accompagne d’un langage de motifs qui permet d’identifier tous les join points OÙ insérer le code transversal.
Advice : définit le code transversal à insérer aux endroits repérés par les pointcuts.
Weaving : définit le processus final au cours duquel le code métier et le code transversal sont tissés ensemble pour former le code complet de l’application, en répondant à toutes les préoccupations de la conception logicielle. L’AOP consiste à générer du code — à la compilation ou à l’exécution — en des points précis du flux afin de l’enrichir.

Le tissage des advices dans les join points définis par les pointcuts.
Quelques exemples avec AspectJ
AspectJ est une implémentation de référence du paradigme AOP dans l’univers Java. Java étant un langage bien connu et facile à comprendre, il illustrera efficacement les principes théoriques vus jusqu’ici.
Le but de cet article n’étant pas de maîtriser AspectJ, nous allons parcourir trois exemples simples d’utilisation de l’AOP pour retirer le code technique et transversal de la logique métier : la journalisation, le pattern Singleton et le pattern Observer.
Chaque exemple aura son équivalent en Swift afin de faire écho à la question posée dans le titre de l’article.
Journaliser avec AspectJ
Partons des exigences suivantes :
L’application doit permettre de créer un utilisateur, de lui donner un nom, puis de lui demander d’afficher ce nom. Elle doit également journaliser les manipulations des données de l’utilisateur.
Deux concepts émergent clairement : d’un côté, la modélisation de l’utilisateur et son cycle de vie ; de l’autre, la journalisation du déroulement de l’application. Nous pourrions inclure les logs dans le code de l’utilisateur et ainsi répondre aux exigences. Mais User perdrait alors son statut de Plain Old Java Object et ne respecterait plus le principe de responsabilité unique. De toute évidence, un aspect doit prendre en charge cette tâche tout en préservant l’intégrité de notre modélisation objet.

Le POJO User et son cycle de vie.
En l’état, ce code ne produit aucun log. User est un POJO conforme aux bonnes pratiques de la programmation orientée objet : sa seule responsabilité consiste à modéliser notre univers.
Voici l’Aspect que nous pouvons implémenter pour ajouter des logs sans altérer le POJO :

Cet Aspect journalise les appels à
getName()etsetName().
Sans entrer dans la syntaxe d’AspectJ, cet aspect ajoute les comportements suivants à l’application :
- chaque fois que
User.getName()est appelée — en réalité, autour de l’appel —, un advice est exécuté pour journaliser la valeur renvoyée ; - avant l’exécution de
User.setName(), un advice est exécuté pour journaliser les paramètres passés à la méthode.
Après le processus de tissage, l’exécution de l’application produit l’affichage suivant :

Il s’agit bien sûr d’un exemple élémentaire et AspectJ est capable de bien davantage. L’AOP a néanmoins atteint son but : nous avons ajouté à l’application une préoccupation purement technique sans même modifier notre logique ni notre modèle métier.
Journaliser avec Swift
Puisque notre défi consiste à implémenter un modèle objet pur, nous pouvons commencer par définir la structure User et son cycle de vie comme nous l’avons fait en Java :

Swift ne propose aucun mécanisme comparable au tissage. Depuis Swift 5.1, nous disposons toutefois des property wrappers — consultez mon article précédent sur le sujet —, qui agissent comme un délégué lors de l’accès à une propriété. Nous pouvons exploiter ce mécanisme et déléguer la journalisation à un property wrapper générique.

Chaque accès à la propriété
valueproduit des logs.
En annotant une propriété avec le property wrapper @Logged, ses accès en lecture et en écriture produisent des logs, MAIS cela nous oblige à modifier légèrement le code d’origine :

La propriété
nameest désormais surveillée par un property wrapper.
L’exécution de l’application affiche le résultat suivant :

Le résultat est très proche de ce que nous avons obtenu avec une véritable AOP. Si nous considérons l’annotation @Logged comme une forme de métadonnée qui complète la propriété name, nous pouvons dire qu’elle n’est pas intrusive du point de vue de la conception orientée objet.
Les property wrappers présentent même un avantage. Comme il s’agit d’un concept générique, nous pouvons annoter aussi bien la propriété name qu’une propriété user complète. La journalisation peut ainsi être facilement étendue aux nouveaux champs ajoutés à la structure User.
Le pattern Singleton avec AspectJ
Singleton est un design pattern controversé. Disposer d’une instance unique d’un objet peut être utile, mais cela ne devrait pas avoir d’incidence sur la manière dont le modèle est écrit.
L’AOP peut nous aider à isoler les détails d’implémentation au sein d’un Aspect.
L’un des rares cas dans lesquels Singleton est acceptable est l’implémentation d’un Logger.
Voici une implémentation naïve en Java :

Une implémentation naïve et non optimisée d’un Logger en Java.
Cette implémentation ne comporte aucun membre d’instance statique de type Logger permettant de réaliser le pattern Singleton. Il existera autant d’instances de Logger que d’appels à son constructeur dans notre programme.
AspectJ nous offre cependant la possibilité d’intercepter les appels au constructeur de Logger et de fournir une instance unique.

Avec AspectJ, la stratégie d’instanciation par défaut d’un Aspect consiste à créer un singleton.
L’instance du logger à créer est désormais fournie par l’Aspect. Comme la stratégie d’instanciation par défaut d’un Aspect avec AspectJ est un singleton, le logger créé sera par nature un singleton.

Bien que
loggerAetloggerBsemblent provenir de deux instanciations, il s’agit exactement du même objet.
Nous profitons ainsi des avantages d’un Singleton sans compromettre la conception du Logger.
Le pattern Singleton avec Swift
Nous allons une nouvelle fois exploiter les property wrappers pour déléguer la création de l’objet à un acteur tiers qui garantira l’unicité de l’instance.
Commençons par implémenter une classe qui impose le pattern Singleton en conservant les références uniques dans un dictionnaire statique.

Storage encapsule un dictionnaire statique qui garantit l’unicité des instances.
Nous pouvons maintenant implémenter un property wrapper qui utilisera ce stockage comme fournisseur de la valeur générique dont il est le délégué.

L’accès à value est délégué au stockage statique, ce qui impose le pattern Singleton.
Revenons à l’exemple du Logger :

Une fois encore, nous sommes très proches du résultat obtenu avec AspectJ, à condition d’accepter d’annoter les propriétés.
Nous pouvons pousser les possibilités du property wrapper encore plus loin grâce aux deux ajouts suivants.
1 : en accédant aux fonctions propres au property wrapper avec la notation $, nous pouvons, par exemple, réinitialiser la référence du Singleton chaque fois que nécessaire.

Le mécanisme des property wrappers nous donne accès à des fonctions utilitaires pour gérer le stockage.
Une fois la fonction reset() appelée, le stockage concerné est vidé. Une nouvelle référence du Singleton sera créée lors du prochain accès.
2 : comme nous pouvons passer des arguments à un property wrapper, nous pouvons introduire le concept de Scope. Un scope peut être vu comme une stratégie d’instanciation des objets gérés. Singleton ne serait alors que l’un des scopes possibles — celui par défaut. Lorsque vous définissez un scope personnalisé, l’instanciation n’est rattachée qu’à ce scope. Il devient logique de renommer le property wrapper Singleton en property wrapper Managed.

Le scope entre dans la composition des clés des objets dans le dictionnaire statique de Storage.
Par défaut, une propriété @Managed appartient au scope « Singleton ». Lorsque le scope est explicitement mentionné dans l’annotation, l’objet créé est isolé dans ce scope.

Si nous réduisons l’AOP à un moyen d’intercepter les accès aux propriétés et d’injecter du code autour d’eux, les property wrappers sont assez proches de ce qu’est un Aspect.
L’AOP ne se limite cependant pas à l’interception des propriétés. AspectJ propose, par exemple, une fonctionnalité appelée inter-type declaration.
L’inter-type declaration permet d’enrichir le code métier en modifiant la structure du modèle. Nous pouvons, par exemple, ajouter des attributs et des méthodes à une classe ou modifier ses relations d’héritage. Nous allons mettre ce principe en pratique en appliquant le pattern Observer à un modèle dont le code ne mentionne même pas ce pattern, puis voir comment Swift peut répondre au problème.
Le pattern Observer avec AspectJ
Le pattern Observer est utilisé en programmation orientée objet pour découpler la modélisation des objets des couches qui réagiront à leurs mutations. Le sujet observé maintient une liste d’observateurs, qu’il informe lorsqu’il subit une mutation.
Il est souvent utilisé dans des patterns d’architecture comme MVC afin de rafraîchir l’interface lorsque le modèle métier change.
Dans l’univers Java traditionnel, ce pattern implique des interfaces comme IObserver et ISubject pour abstraire les mécanismes d’enregistrement et de notification.

Le modèle doit implémenter l’interface
ISubject, tandis que les couches qui observent doivent implémenterIObserver.
Imaginons un modèle User qui implémente ISubject et une UserView qui implémente IObserver. Établir la connexion entre eux reviendrait à écrire :

L’appel à
user.setName()appelle en interne la méthodefireNotification().
Forcer User et UserView à implémenter ces interfaces est assez intrusif du point de vue de la modélisation. Cela implique, par exemple, que User dispose d’un mécanisme pour conserver la liste de ses observateurs afin de pouvoir les informer. Fondamentalement, un User n’est qu’un User : il ne devrait pas connaître le mécanisme d’observation.
Nous pouvons tirer parti de l’inter-type declaration pour retirer cette responsabilité du modèle User. Considérons désormais que le code de User et de UserView est entièrement dépourvu de préoccupations techniques.
Nous allons modifier dynamiquement leur structure afin d’enrichir leur code avec les implémentations des interfaces.

Ce code peut sembler un peu complexe, mais il accomplit essentiellement deux choses :
- il fait implémenter dynamiquement
ISubjectà User ; - il intercepte les appels aux setters de User puis déclenche les notifications.
Nous ferions quelque chose de similaire avec UserView pour lui faire implémenter IObserver.
Après le processus de tissage, User et UserView implémentent leurs interfaces respectives sans même le savoir. Le modèle reste pur tandis que le pattern Observer est bien implémenté.
Le pattern Observer avec Swift
Même si le défi semble de taille, Swift nous fournit deux outils pour atteindre notre objectif :
- une extension permettra de rendre UserView conforme à Observer ;
- un property wrapper permettra de transformer n’importe quelle propriété souhaitée en Subject observé.
Commençons par définir un protocole Observer. Nous utiliserons un associatedtype afin de profiter ensuite de la sûreté du typage du langage.

Observer étant un protocole avec un associatedtype, nous appliquons une technique d’effacement de type afin de pouvoir l’utiliser comme propriété d’instance ou comme paramètre de fonction lorsque nécessaire.
Nous pouvons maintenant définir un protocole Subject. Comme pour Observer, nous définissons un type associé qui représente la valeur observée. Nous pourrons ainsi contraindre les types associés d’Observer et de Subject à utiliser le même type de valeur.

Nous ne pouvons relier un Subject et des Observers que si leurs types associés respectifs sont cohérents.
Comme prévu, UserView est une classe simple dont la seule responsabilité consiste à afficher un User. Une extension peut la rendre conforme à Observer.

Les extensions peuvent être considérées comme l’équivalent Swift de l’inter-type declaration d’AspectJ, puisqu’elles modifient l’héritage ou les conformités d’un type.
Nous devons maintenant réagir aux mutations d’une propriété du Subject afin d’en informer ses observateurs. L’astuce consiste à déléguer les notifications à un property wrapper qui jouera le rôle du Subject.

Nous pouvons désormais annoter n’importe quelle propriété avec @Observed et utiliser la notation $ pour accéder à l’aspect Subject qui lui est associé.

Le résultat est plutôt satisfaisant 👍. User et UserView restent purs du point de vue de la conception orientée objet, mais peuvent participer à un pattern Observer grâce aux extensions et aux property wrappers.
Conclusion
Swift est-il un langage de programmation orientée aspects ?
Pas vraiment. Les langages de programmation orientée aspects ne se limitent pas à modifier l’héritage d’un type ou à exécuter du code lors de l’accès à une propriété. Ils permettent :
- de définir précisément les conditions d’insertion d’un Aspect — avant, après ou autour des appels de méthodes, des accès aux propriétés, des exceptions levées, etc. ;
- de les tisser statiquement ou dynamiquement ;
- d’effectuer une introspection du code intercepté et de pouvoir modifier complètement son comportement d’origine.
Mais Swift est assurément un langage multiparadigme. C’est ce qui le rend si polyvalent, si puissant et pourtant facile à apprendre. Il est à la fois :
- un langage de programmation orientée objet, avec des concepts comme les classes et les objets, l’abstraction, l’héritage et le polymorphisme ;
- un langage de programmation orientée protocoles, avec des outils comme les extensions de protocoles, la conformité conditionnelle et la composition de protocoles ;
- un langage de programmation fonctionnelle, avec des types de données immuables comme les structures et les énumérations, ou des concepts comme les fonctions d’ordre supérieur et les monades ;
- un langage de programmation orientée aspects, principalement grâce aux extensions et aux property wrappers.
Merci de m’avoir lu. N’hésitez pas à me dire si, selon vous, Swift est ou non un langage de programmation orientée aspects.
