IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Architecture en couches, découplage et injection de dépendances avec Unity


précédentsommairesuivant

VI. Plus loin avec Unity  : l'interception

Pour cette partie, je suggère l'utilisation de l'application console.

VI-A. L'interception

La notion d'interception est un peu plus complexe, au lieu de nous fournir une implémentation d'une interface, Unity nous retourne un objet proxy qui enrobe notre objet réel. Cet objet possède exactement les méthodes de l'interface, mais on peut ajouter des comportements à celles-ci. Exemple : On me remonte que ma couche d'accès aux données est lente, comment puis je mesurer les performances de celle-ci ? Soit je la modifie en conséquence et ajoute des traces d'audit un peu partout ce qui est assez coûteux, soit j'intercepte toutes les méthodes via un proxy : lors de l'interception je peux enregistrer le temps d'exécution. Ça a l'avantage de laisser intact mon code de la couche d'accès aux données et de s'activer via quelques lignes dans le fichier de configuration.
Nous allons créer une classe StopwatchInterceptionBehavior pour auditer nos performances, mais tout d'abord, il faut configurer Unity! Pour activer l'interception de manière générale, il faut déclarer l'extension via une balise sectionExtension dans la section unity puis enregistrer l'extension dans notre container via une balise extension. On peut enfin activer notre interception sur des objets désirés.

 
Sélectionnez
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <configSections>
        <section name="unity" type="Microsoft.Practices.Unity.Configuration.UnityConfigurationSection, Microsoft.Practices.Unity.Configuration"/>
    </configSections>

    <unity xmlns="http://schemas.microsoft.com/practices/2010/unity">
        <sectionExtension type="Microsoft.Practices.Unity.InterceptionExtension.Configuration.InterceptionConfigurationExtension, Microsoft.Practices.Unity.Interception.Configuration" />

        <assembly name="MovieBase.Business.Interfaces" />
        <assembly name="MovieBase.Business" />
        <assembly name="MovieBase.DataAccess.Interfaces" />
        <assembly name="MovieBase.XmlDataAccess" />
        <assembly name="MovieBase.Tools"/>

        <namespace name="MovieBase.Business.Interfaces" />
        <namespace name="MovieBase.Business" />
        <namespace name="MovieBase.DataAccess.Interfaces" />
        <namespace name="MovieBase.XmlDataAccess" />
        <namespace name="MovieBase.Tools"/>

        <container>
            <extension type="Interception"/>

            <register type="IDataAccess" mapTo="DataAccess">
                <lifetime type="singleton"/>
                <constructor />
                <property name="StoreFile" value="Store.xml" />
                <interceptor type="InterfaceInterceptor"/>
                <interceptionBehavior type="StopwatchInterceptionBehavior"/>
            </register>
            <register type="IBusinessLogic" mapTo="BusinessLogic" />
        </container>
    </unity>
</configuration>

Ici, on a activé l'interception via le Behavior StopwatchInterceptionBehavior. Pour ça, Unity va créer un proxy de notre couche d'accès, j'ai choisi un proxy de type InterfaceInterceptor. Voici les types de proxy différents :

  • Transparent Proxy Interceptor : permet d'intercepter toutes les méthodes et plusieurs interfaces de l'objet. Revers de la médaille : il est très lent ! ;
  • Interface Interceptor : il permet d'intercepter toutes les méthodes de l'interface. Revers de la médaille : ne fonctionne que pour ladite interface et pas les autres méthodes ;
  • Virtual Method Interceptor : permet d'intercepter tous les membres déclarés virtual. Revers de la médaille : impose de déclarer ses membres virtual. Quid des assemblies tierces ?
Image non disponible
Fonctionnement de l'interception

Un excellent tableau dans la documentation de Unity détaille un peu plus les différences, cas d'utilisation et avantages/inconvénients.
Notre classe d'interception est assez simple, elle doit implémenter l'interface IInterceptionBehavior de Unity et donc implémenter trois membres : GetRequiredInterfaces qui renvoie les interfaces que l'objet intercepté doit implémenter, WillExecute qui renvoie si le behavior fait vraiment quelque chose (si non, il n'est pas inclus dans la chaine d'interception) et Invoke qui est appelé lors de l'interception. Cette méthode prend deux paramètres : un IMethodInvocation qui contient les paramètres de l'interception (méthode interceptée, paramètres, objet doté d'un proxy, etc.) et un délégué vers le prochain maillon de la chaine d'interception.
Voici donc l'exemple :

 
Sélectionnez
public class StopwatchInterceptionBehavior : IInterceptionBehavior
{
    public IEnumerable<Type> GetRequiredInterfaces()
    {
        return new HashSet<Type>();
    }

    public bool WillExecute
    {
        get { return true; }
    }

    public IMethodReturn Invoke(IMethodInvocation input, GetNextInterceptionBehaviorDelegate getNext)
    {
        var stopwatch = Stopwatch.StartNew();
        try
        {
            return getNext()(input, getNext);
        }
        finally
        {
            Debug.WriteLine(String.Format("Executing {0} took {1} ms", input.MethodBase.Name, stopwatch.ElapsedMilliseconds));
        }
    }
}

Lors de l'exécution en Debug, on aperçoit dans la fenêtre Output de Visual Studio le message avec la durée d'exécution. Il est évidemment possible d'écrire dans un fichier journal plutôt que dans cette fenêtre. Ainsi, il est possible d'instrumenter un environnement de production.

VI-B. L'injection de Policy

Créer un Interception Behavior est pratique lorsque l'on veut ajouter un comportement à toutes les méthodes. Ça impose de configurer pour chaque élément les comportements à ajouter et l'interception se fait sur toutes les méthodes possibles (selon le type du proxy).
Il existe un moyen d'être un peu plus fin : il est possible de définir des règles par exemple « Si l'objet doté d'un proxy fait partie de telle assembly, applique ces comportements. S’il possède telle méthode, applique également ce comportement sur cette méthode uniquement ». C'est l'injection de Policy.
Pour l'exemple, nous allons retravailler notre couche Business : actuellement elle s'occupe de valider les paramètres d'entrée et de remonter une erreur si l'année n'est pas dans la plage attendue, etc. Nous allons mettre au point un système de validation des paramètres par attribut.
Il faut tout d'abord changer légèrement changer l'interface IBusinessLogic :

 
Sélectionnez
public interface IBusinessLogic
{
    List<Movie> GetMovies();

    List<Movie> GetMoviesByCountry(
        [RequiredValidator]
        [InValidator("UK", "USA", "France")]
        String country);

    List<Movie> GetMoviesByYear(
        [RangeValidator(Min = 1950, Max = 2010)]
        ushort year);

    List<Movie> GetMoviesByYearAndCountry(
        [RangeValidator(Min = 1950, Max = 2010)]
        ushort year,
        [RequiredValidator]
        [InValidator("UK", "USA", "France")]
        String country);

    Movie GetMovie(
        [RangeValidator(Min = 0, Max = Int32.MaxValue)]
        int movieId);

    void InsertMovie(
        [RequiredValidator]
        String title,
        [RequiredValidator]
        [InValidator("UK", "USA", "France")]
        String country,
        [RangeValidator(Min = 1950, Max = 2010)]
        ushort year);
}

On remarque l'apparition de trois validateurs (RequiredValidatorAttribute, InValidatorAttribute et RangeValidatorAttribute) qui héritent tous les trois de BaseValidatorAttribute.
Il faut donc maintenant écrire un bout de code qui déclenche effectivement la validation des paramètres à chaque appel. Pour ceci, il faut créer une classe ValidationCallHandler qui se charge de faire le travail. Cette classe implémente ICallHandler qui ressemble à IInterceptionBehavior, car elle possède également la méthode Invoke comme ci-dessus. Le principe est simple : à chaque interception, on récupère les paramètres et pour chaque paramètre on récupère la valeur et ses attributs héritant de BaseValidatorAttribute. Pour chaque attribut, on vérifie la valeur et si une exception de validation est rencontrée, on le renvoie. Cette exception arrête la chaine des comportements d'interception (on n'appelle pas getNext).

 
Sélectionnez
public class ValidationCallHandler : ICallHandler
{
    public IMethodReturn Invoke(IMethodInvocation input, GetNextHandlerDelegate getNext)
    {
        try
        {
            for (int i = 0; i < input.Arguments.Count; i++)
            {
                var parameterValue = input.Arguments[i];
                var parameterInfo = input.Arguments.GetParameterInfo(i);

                var attributes = parameterInfo.GetCustomAttributes(typeof(BaseValidatorAttribute), false);

                foreach (var attribute in attributes.OfType<BaseValidatorAttribute>())
                {
                    attribute.Validate(parameterInfo.Name, parameterValue);
                }
            }

            return getNext()(input, getNext);
        }
        catch (Exception exc)
        {
            return input.CreateExceptionMethodReturn(exc);
        }
    }

    public int Order { get; set; }
}

Il faut également configurer les objets à intercepter dans le fichier de configuration. Il faut définir le comportement de proxification et ajouter la balise policyInjection. On utilise également l'implémentation AttributeValidatedBusinessLogic qui, contrairement à la version originale, ne procède à aucune vérification des paramètres.

 
Sélectionnez
<register type="IBusinessLogic" mapTo="AttributeValidatedBusinessLogic">
    <interceptor type="InterfaceInterceptor"/>
    <policyInjection/>
</register>

Désormais, pour utiliser notre CallHandler, il y a deux possibilités : continuer à procéder par attributs ou procéder par configuration.

VI-B-1. Injection de Policy par attribut

Plus simple à mettre en œuvre, elle nécessite cependant une recompilation du code pour l'activation/désactivation. Il suffit de créer un attribut ValidationCallHandlerAttribute héritant de HandlerAttribute.

 
Sélectionnez
[AttributeUsage(AttributeTargets.Method)]
public class ValidationCallHandlerAttribute : HandlerAttribute
{
    public override ICallHandler CreateHandler(IUnityContainer container)
    {
        return new ValidationCallHandler();
    }
}

Il suffit désormais de décorer les méthodes à qui il faut appliquer la vérification avec l'attribut.

 
Sélectionnez
[ValidationCallHandler]
List<Movie> GetMoviesByYear(
        [RangeValidator(Min = 1950, Max = 2010)]
        ushort year);

Désormais, si on essaie de saisir une date comme 1920, on obtient une exception.
La technique par attributs peut être intéressante pour, par exemple, créer une méthode transactionnelle. On peut créer un attribut Transaction qui enrôle toute la méthode dans une transaction. Il est ici utile de bénéficier de l'information que la méthode est transactionnelle directement dans le C#, ce que la technique par configuration ne permet pas.

VI-B-2. Injection de Policy par configuration

Légèrement plus complexe à mettre en place, mais plus flexible, elle permet de définir des règles pour appliquer plusieurs CallHandlers. Bien sûr, il faut auparavant enlever les attributs ValidationCallHandler sous peine de conflits.
Voici la configuration que nous allons utiliser :

 
Sélectionnez
<unity xmlns="http://schemas.microsoft.com/practices/2010/unity">
    <sectionExtension type="Microsoft.Practices.Unity.InterceptionExtension.Configuration.InterceptionConfigurationExtension, Microsoft.Practices.Unity.Interception.Configuration" />

    <assembly name="MovieBase.Business.Interfaces" />
    <assembly name="MovieBase.Business" />
    <assembly name="MovieBase.DataAccess.Interfaces" />
    <assembly name="MovieBase.XmlDataAccess" />
    <assembly name="MovieBase.Tools"/>

    <namespace name="MovieBase.Business.Interfaces" />
    <namespace name="MovieBase.Business" />
    <namespace name="MovieBase.DataAccess.Interfaces" />
    <namespace name="MovieBase.XmlDataAccess" />
    <namespace name="MovieBase.Tools"/>
    <namespace name="MovieBase.Tools.Validation"/>

    <container>
        <extension type="Interception"/>
    
        <interception>
            <policy name="ParametersValidationPolicy">
                <callHandler name="ValidationCallHandler" type="ValidationCallHandler" />
                <matchingRule name="AssemblyMatchingRule" type="AssemblyMatchingRule">
                    <constructor>
                        <param name="assemblyName" value="MovieBase.Business"/>
                    </constructor>
                </matchingRule>
            </policy>
        </interception>
                
        <register type="IDataAccess" mapTo="DataAccess">
            <lifetime type="singleton"/>
            <constructor />
            <property name="StoreFile" value="Store.xml" />
            <interceptor type="InterfaceInterceptor"/>
            <interceptionBehavior type="StopwatchInterceptionBehavior"/>
        </register>
        <register type="IBusinessLogic" mapTo="AttributeValidatedBusinessLogic">
            <interceptor type="InterfaceInterceptor"/>
            <policyInjection/>
        </register>
    </container>
</unity>

On remarque plusieurs choses : il faut créer une Policy appelée ici ParametersValidationCallHandler, indiquer le ou les CallHandler à appliquer et enfin une règle d'application. Ici on veut que cette règle ne soit appliquée que pour les éléments de ma couche Business. Il existe plusieurs MatchingRules par défaut : matcher sur la signature de méthode, un tag, le namespace, une propriété, etc.
De manière identique, avec 1920, une exception est levée.

VI-C. Conclusions sur l'interception

Unity se branche très bien sur les Enterprise Library de Microsoft. Dans cette bibliothèque, il y a plusieurs CallHandler disponibles : pour la sécurité un AuthorizationCallHandler chargé de vérifier les autorisations, pour l'instrumentation un LogCallHandler et un PerformanceCounterCallHandler, pour la gestion d'exception un ExceptionCallHandler pour la gestion d'exception et enfin un ValidationCallHandler un peu plus complexe que l'exemple ci-dessus.
Ces méthodes sont un bon moyen de rajouter des comportements communs plutôt techniques à des briques métier. Cela permet de nettoyer par exemple la couche Business et n'y laisser que du code métier.


précédentsommairesuivant

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2011 Nathanael Marchand. Aucune reproduction, même partielle, ne peut être faite de ce site ni de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts. Droits de diffusion permanents accordés à Developpez LLC.