Programmation Orientée Aspect

Définition : AOP

La Programmation Orientée Aspect, ou AOP pour Aspect Oriented Programming, est une technique de programmation qui consiste à découper un traitement en modules indépendants les uns des autres. L'objectif est d'obtenir une tâche spécifique par module. Cela revient à dire qu'un module n'est spécialisé qu'à une seule et unique tâche, ou aspect.
L'AOP ne cherche pas à remplacer la Programmation Orientée Objet (POO), et n'est pas liée à un langage non plus, car ce concept se pratique aussi bien avec un langage orienté objet qu'avec un langage procédural.

L'idée première de ce paradigme de programmation est de séparer les préocupations (Separation of Concerns ou SoC) lors du développement. Ces préocupations sont aussi appellées aspects, où chaque aspect aurait sa propre problématique.

Dans une application, 3 types d'aspect se distinguent sous forme de service :

  1. l'aspect technique, tel que le DAO (persistence des données), l'historisation des événements (trace), l'internationalisation (traduction), la programmation concurrente (multi-threading) ;
  2. l'aspect applicatif, comme l'authentification utilisateur, gestion de la session utilisateur ;
  3. l'aspect métier, désignant par exemple le BO (règle métier).

La Programmation Orientée Aspect est une technique qui complète parfaitement la Programmation Orientée Objet. Elle introduit naturellement la notion de module indépendant. Quelque part, c'est ce que font déjà les fonctions et les méthodes, mais l'AOP propose une technique de développement plus élégante et améliorée en essayant de résoudre certaines problèmatiques : application divisée en modules spécialisés (technique, applicatif et métier), réutilisation de ces modules et extensibilité avec le concept de l'héritage. Et cela en pratiquant l'injection de dépendance en toute transparence.

Exemple de programmation traditionnelle d'une méthode d'action d'un contrôleur

Voici un exemple de méthode d'action sans AOP. Ici, le code a été enlevé volontairement car inutile dans la compréhension de cette exemple. Les commentaires suffisent :

/**
 * Add a new user in database. User data are sent from a form by POST method.
 * This method can be used by an administror only. So administrator session is required.
 * Input:
 *		- userId (required): user identifier with a specific format (ex: c369vs).
 *		- firstname (required): user first name.
 *		- lastname (required): user last name.
 *		- email (required): user email.
 * @return void
 * @exception SessionException: case session administrator is expired.
 * @exception SecurityException: case administrator has no right to execute action.
 * @exception FormDataException: case a request parameter value is not expected.
 * @exception BusinessException: case business logic is not expected.
 * @exception SqlException: case SQL error occured.
 * @exception TechnicalException: case technical error occured.
 */
public function addUserAction() {
	//Check administrator session: throw SessionException if administrator is not logged
	...

	//Check administrator access rights: throw SecurityException if administrator has no access right
	...
	
	//Retrieve request parameters
	...
	
	//Check userId:
	//	- userId is required: throw FormDataException if this parameter is missing or empty
	//	- minimum length: throw FormDataException if the minimum length is not expected
	//	- maximum length: throw FormDataException if the maximum length is exceeded
	//	- format: throw FormDataException if the format is not expected
	...

	//Check userId in database: throw BusinessException if user identidfier already exists
	//User identifier must be unique
	...

	//Check firstname:
	//	- firstname is required: throw FormDataException if this parameter is missing or empty
	//	- minimum length: throw FormDataException if the minimum length is not expected
	//	- maximum length: throw FormDataException if the maximum length is exceeded
	//	- format: throw FormDataException if the format is not expected
	...

	//Check lastname:
	//	- lastname is required: throw FormDataException if this parameter is missing or empty
	//	- minimum length: throw FormDataException if the minimum length is not expected
	//	- maximum length: throw FormDataException if the maximum length is exceeded
	//	- format: throw FormDataException if the format is not expected
	...

	//Check email:
	//	- email is required: throw FormDataException if this parameter is missing or empty
	//	- minimum length: throw FormDataException if the minimum length is not expected
	//	- maximum length: throw FormDataException if the maximum length is exceeded
	//	- format: throw FormDataException if the format is not expected
	...

	//Check email in database: throw BusinessException if email already exists
	//Email must be unique
	...

	//Insert user data in database
	...

	//Trace administrator action in log file
	...
}

L'objectif de la méthode d'action addUserAction est d'enregistrer les données d'un nouvel utilisateur dans la base de données. Cette action se décompose en plusieurs tâches :

  1. Contrôle la session de l'administrateur : l'action nécessite que l'administrateur soit identifié.
  2. Contrôle les droits d'accès à l'action : l'administrateur doit avoir les droits pour ajouter un nouvel utilisateur dans le système.
  3. Contrôle la validité des paramètres d'entrée : optionalité, longueur minimale, longueur maximale, et format du paramètre.
  4. Contrôle la règle de gestion métier : ici l'identifiant utilisateur et l'email doivent être unique en base de données.
  5. L'action principale : ici, l'ajout des données du nouvel utilisateur dans la base de données.
  6. Archive l'action de l'administrateur dans un fichier de log (ou en base de données).

Passage de la programmation traditionnelle en Programmation Orientée Aspect

La structure du code précédent permet de bien distinguer les tâches qui s'exécutent de façon séquentielle. Chaque tâche est spécialisée et gère sa propre problématique. De ce fait, elle peut être mise en module, où plutôt en méthode comme suit :

/**
 * Add a new user in database. User data are sent from a form by POST method.
 * This method can be used by an administror only. So administrator session is required.
 * Input:
 *		- userId (required): user identifier with a specific format (ex: c369vs).
 *		- firstname (required): user first name.
 *		- lastname (required): user last name.
 *		- email (required): user email.
 * @return void
 * @exception SqlException: case SQL error occured.
 */
public function addUserAction() {
	//Retrieve request parameters
	...
	
	//Insert user data in database
	...
}

/**
 * Check administrator session.
 * Input:
 *		- $_SESSION.
 * @return void
 * @exception SessionException: case session administrator is expired.
 */
public function checkAdministratorSession() {
	//Check administrator session: throw SessionException if administrator is not logged
	...
}

/**
 * Check administrator access rights on the current action.
 * Input:
 *		- $_SERVER
 *		- $_SESSION
 * @return void
 * @exception SecurityException: case administrator has no right to execute action.
 */
public function checkAdministratorAccess() {
	//Check administrator access rights: throw SecurityException if administrator has no access right
	...
}

/**
 * Check request parameters. It is a technical check only on data optionality, length and format.
 * Input:
 *		- userId (required): user identifier with a specific format (ex: c369vs).
 *		- firstname (required): user first name.
 *		- lastname (required): user last name.
 *		- email (required): user email.
 * @return void
 * @exception FormDataException: case a request parameter value is not expected.
 */
public function checkParameters() {
	//Retrieve request parameters
	...
	
	//Check userId:
	//	- userId is required: throw FormDataException if this parameter is missing or empty
	//	- minimum length: throw FormDataException if the minimum length is not expected
	//	- maximum length: throw FormDataException if the maximum length is exceeded
	//	- format: throw FormDataException if the format is not expected
	...

	//Check firstname:
	//	- firstname is required: throw FormDataException if this parameter is missing or empty
	//	- minimum length: throw FormDataException if the minimum length is not expected
	//	- maximum length: throw FormDataException if the maximum length is exceeded
	//	- format: throw FormDataException if the format is not expected
	...

	//Check lastname:
	//	- lastname is required: throw FormDataException if this parameter is missing or empty
	//	- minimum length: throw FormDataException if the minimum length is not expected
	//	- maximum length: throw FormDataException if the maximum length is exceeded
	//	- format: throw FormDataException if the format is not expected
	...

	//Check email:
	//	- email is required: throw FormDataException if this parameter is missing or empty
	//	- minimum length: throw FormDataException if the minimum length is not expected
	//	- maximum length: throw FormDataException if the maximum length is exceeded
	//	- format: throw FormDataException if the format is not expected
	...
}

/**
 * Check business logic for addUserAction.
 * Input:
 *		- userId (required): user identifier with a specific format (ex: c369vs).
 *		- email (required): user email.
 * @return void
 * @exception BusinessException: case business logic is not expected.
 * @exception SqlException: case SQL error occured.
 */
public function addUserCheckBusiness() {
	//Retrieve request parameters
	...
	
	//Check userId in database: throw BusinessException if user identidfier already exists
	//User identifier must be unique
	...

	//Check email in database: throw BusinessException if email already exists
	//Email must be unique
	...
}

/**
 * Trace action.
 * Input:
 *		- all data which can be usefull : $_SERVER, $_SESSION, ...
 * @return void
 * @exception TechnicalException: case technical error occured.
 */
public function trace() {
	//Trace administrator action in log file
	...
}

La méthode d'action addUserAction qui était bien founie en programmation traditionnelle, s'est vue totalement dépouillée en AOP pour ne traiter que sa tâche principale, qui n'est au final que l'enregistrement des données du nouvel utilisateur. Le contrôle de ces données est attribué à une autre méthode, et il en est de même pour les autres tâches (ex : contrôle de session et des droits de l'administrateur, ...).

Chacune des méthodes issues de addUserAction gère donc sa propre problématique, et il en ressort les aspects suivants :

  1. aspect technique : checkParameters, trace. L'aspect technique est fourni par le framework.
  2. aspect applicatif : checkAdministratorSession, checkAdministratorAccess. L'aspect applicatif peut être founi par le framework, mais il est généralement personnalisé selon la logique métier.
  3. aspect métier : addUserCheckBusiness, addUserAction. L'aspect métier correspond à un développement personnalisé, et donc est codé par le développeur.

Les méthodes sont indépendantes les unes des autres. Elles partagent entre elles seulement les variables membres de la classe (ex : les paramètres de la requête). Après la restructuration du code en modules indépendants, il reste un problème à régler : comment appeler ces modules sans que l'on se retrouve en situation de dépendance ?
Créer une nouvelle méthode pour appeler ces modules reviendrait à nous retrouver au point de départ avec la méthode d'action. La nouvelle méthode deviendrait alors dépendante des modules, et c'est justement ce que nous souhaitons éviter. Et c'est là qu'intervient le tisseur d'aspects.

Le tisseur d'aspects

Le tisseur d'aspects est tout simplement un fichier texte de déclaration, comme un fichier de configuration, qui y définit les modules (méthodes) à exécuter. Il effectue la tâche que ferait une méthode d'action (ici addUserAction) en programmaton traditionnelle. Son format peut être du XML, JSON ou autre, selon l'inspiration du framework auquel le tisseur d'aspects est rattaché.

Dans le cadre du framework Adventy, chaque méthode d'action est lié à un tisseur d'aspects. Ce dernier est un fichier PHP dans lequel un tableau associatif est utilisé pour la déclaration des modules à lancer. Il est de la forme :

<?php 
//List of methods to run sequentially
$_aopMethods = array(
	/**
	 * Page aspect weaver.
	 */
	'page' => null,
	
	/**
	 * Web service aspect weaver.
	 */
	'ws' => null,
	
	/**
	 * Event aspect weaver.
	 */
	'event' => array(
		//method1 is the first method to run, without parameter
		array(
			'name' => 'method1',
			'arguments' => null
		),

		//method2 is the second method to run, with parameters $value1 to $valueN
		array(
			'name' => 'method2',
			'arguments' => array(
				$value1,
				...
				$valueN
			)
		),

		//Others methods to run
		...
	)
);
?>

En reprenant notre exemple avec la méthode d'action addUserAction, le tisseur d'aspects associé serait :

<?php 
//List of methods to run sequentially
$_aopMethods = array(
	/**
	 * Page aspect weaver.
	 */
	'page' => null,
	
	/**
	 * Web service aspect weaver.
	 */
	'ws' => null,
	
	/**
	 * Event aspect weaver.
	 */
	'event' => array(
		//Check administrator session
		array(
			'name' => 'checkAdministratorSession',
			'arguments' => null
		),

		//Check administrator access rights on the current action
		array(
			'name' => 'checkAdministratorAccess',
			'arguments' => null
		),

		//Check request parameters. It is a technical check only on data optionality, length and format
		array(
			'name' => 'checkParameters',
			'arguments' => null
		),

		//Check business logic for addUserAction
		array(
			'name' => 'addUserCheckBusiness',
			'arguments' => null
		),

		//Add a new user in database. User data are sent from a form by POST method
		array(
			'name' => 'addUserAction',
			'arguments' => null
		),

		//Trace action
		array(
			'name' => 'trace',
			'arguments' => null
		)
	)
);
?>

Ce fichier est à placer dans le dossier racine des tisseurs d'aspects /application/aop/ en suivant la logique de création de sous-dossiers si besoin, comme pour les CSS/JS de page. Par exemple, pour la requête /manager/user/add-user, le fichier du tisseur d'aspects a créer serait /application/aop/manager/user/add-user.aop.class.

Controller vs Aspect

En programmation traditionnelle, les dépendences sont inévitables. Bien que le code soit bien structuré dans notre exemple, le risque de voir du code éparpillé n'est pas nul surtout lorsque plusieurs développeurs interviennent sur ce même code source. Cela peut engendrer de la duplication de code, du code mort ou inutile, voire même trop de code, ce qui rend difficile son évolution et sa maintenance.
Quant au test unitaire, il ne peut que s'appliquer sur les méthodes (avec une seule problématique) dont la méthode d'action en est dépendante. Car en effet, faire un test unitaire sur la méthode d'action n'a aucun sens, et d'ailleurs ne peut être considéré comme unitaire, puisqu'elle gère plusieurs responsabilités.

En AOP, chaque méthode gère sa propre problématique. Le test unitaire peut donc se pratiquer facilement sur chaque méthode. Une méthode, c'est une tâche spécifique et donc une responsabilité unique. Le code est spécialisé a destination d'un objectif précis. De ce fait, il y a moins de risque de voir du code ésotérique, du code dupliqué ou du code mort, et il est bien plus aisé de le maintenir et de le faire évoluer dans ces conditions.

Ces 2 styles de programmation impactent également la façon dont la requête va s'exécuter. Par exemple pour la requête /manager/user/add-user :

  1. en programmation traditionnelle, la méthode d'action addUserAction est appelée ;
  2. en AOP, c'est le tisseur d'aspects qui sert de point d'entrée. Les méthodes qui y sont définies sont exécutées séquentiellement jusqu'à la dernière méthode ou jusqu'à rencontrer une exception.

Le framework Adventy intègre l'AOP qui reste optionnelle lors du développement. En l'absence du tisseur d'aspect, c'est la programmation traditionnelle qui est appliquée par défaut. Mais lorsque le tisseur d'aspects est défini, la Programmation Orientée Aspect est activée automatiquement.

Les avantages et inconvénients de l'AOP

Même si l'utilisation d'un tisseur d'aspects est inhabituelle, la pratique de l'AOP est fortement recommandée, et cela pour diverses raisons :

  1. la séparation des préoccupations : cela encourage le développement de méthodes à responsabilité unique, le code est moins dense et donc plus aisé à comprendre ;
  2. les aspects : l'aspect amène à une meilleure modularité du code, le développement est spécialisé sur une problématique, ce qui augmente la réutilisation du code. Le niveau d'intervention ou d'exécution est plus clair selon le type d'aspect : technique, applicatif ou métier ;
  3. l'industrialisation : l'implémentation modulaire fait ressortir les fonctionnalités transversales et donc réutilisables. Ces dernières correspondent essentiellement aux aspects techniques (ex : trace) et applicatifs (ex : session utilisateur) ;
  4. les tests unitaires : la modularité permet de tester le code de façon vraiment unitaire. En effet, chaque module correspond à une problématique et gère donc sa propre préoccupation. Les tests unitaires ne s'en retrouvent que plus facile à réaliser ;
  5. la productivité : le développeur ne se soucie que de l'aspect métier et une partie de l'aspect applicatif s'il souhaite le personnaliser. Le reste (aspects transversaux techniques et applicatifs) est déjà proposé par le framework.

Par rapport à la programmation traditionnelle, la nouveauté du tisseur d'aspects qu'apporte l'AOP peut pertuber le développeur. De plus, ce dernier gère beaucoup plus de fichiers sources (CSS/JS de page, propriétés de page, tisseur d'aspect...) qu'avec un framework MVC standard. Cependant, même si ces fichiers sont nombreux, chacun d'entre eux a un rôle ou une responsabilité bien précise. Le code est ainsi trié et regroupé par préoccupation, ce qui évite d'avoir du code dit spaghetti et une maintenance difficile.

Depuis les débuts de la programmation, les meilleures pratiques pour avoir du code clair et maintenable ont toujours été de diviser le code en fonction ou méthode. L'AOP n'est qu'une évolution logique dans la finition de ces pratiques. Cette technique ouvre également de nouvelles portes dans le domaine de l'industrialisation du code, les tests unitaires et la productivité. En plus de cela, l'AOP vient compléter à merveille la Programmation Orientée Objet en y apportant du style et de l'élégance dans le code.