Design pattern: Visiteur

Objectif:

Permettre d’appliquer des comportements spécifiques à un ou plusieurs objets et être sûr que tous les types d’objets sont pris en compte

Justification

Problèmes

On possède une liste hétérogère d’objets, par exemple une liste de véhicules: voiture, moto, etc…
On souhaite appliquer des comportements sur ces objets comme:
– "ajouter des passagers",
– "ajouter des bagages".

Certains problèmes se posent:
Comportement spécifique à un type de véhicule: chaque comportement est applicable à tous les types de véhicule mais chaque comportement est spécifique au type de véhicule: par exemple on ne peut pas ajouter plus d’un passager à une moto ou plus de 4 passagers à une voiture.
Ajout d’un nouveau comportement: si on considère un nouveau comportement comme "ajouter un bagage" comment être sûr que ce nouveau comportement sera implémenté pour tous les types de véhicules.
Ajout d’un nouveau type d’objet: si on ajoute un nouveau véhicule, comment être sûr qu’il sera pris en compte pour tous les comportements.
Implémentation du comportement: pour éviter de casser le principe de la responsabilité unique, on ne veut pas modifier objets à chaque ajout d’un nouveau comportement. Le comportement doit donc être implémenté dans une classe séparée.

Solution

Le pattern "visiteur" répond à ce problème en considérant:
des objets visités: dans notre exemple, ce sont les véhicules.
des visiteurs: dans notre exemple, ce sont les comportements.
Pour permettre l’application des comportements, les objets visités comportent une fonction permettant "d’accepter" la visite des visiteurs: AcceptNewLoad().

Véhicules visités

Voici un exemple d’implémentation des objets visités:

public abstract class Vehicle
{
    public Vehicle()
    {
        this.PassagerCount = 0;
        this.TotalLuggageWeight = 0;
    }

    public abstract void AcceptNewLoad(ILoadHandler loadHandler);

    public int PassagerCount { get; set; }
    public int TotalLuggageWeight { get; set; }
}

public class Car : Vehicle
{
    public override void AcceptNewLoad(ILoadVisitor loadHandler)
    {
        loadHandler.VisitForAddingLoad(this); 
    } 
}

public class Motocycle : Vehicle
{
    public override void AcceptNewLoad(ILoadVisitor loadHandler)
    {
        loadHandler.VisitForAddingLoad(this); 
    }
}

Visiteurs

Un exemple des visiteurs:

public interface ILoadHandler
{
    void VisitForAddingLoad(Car car);
    void VisitForAddingLoad(Motocycle moto);
}

public class NewPassagerLoadHandler : ILoadHandler
{
    public void VisitForAddingLoad(Car car)
    {
        if (car.PassagerCount < 4)
            car.PassagerCount++; 
    }

    public void VisitForAddingLoad(Motocycle moto)
    {
        if (moto.PassagerCount < 2)
            moto.PassagerCount++; 
    }
}

public class NewLuggageLoadHandler : ILoadHandler
{
    private int _luggageWeight;

    public NewLuggageLoadHandler(int luggageWeight)
    {
        this._luggageWeight = luggageWeight;
    }

    public void VisitForAddingLoad(Car car)
    {
        if (this.luggageWeight < 50 && car.TotalLuggageWeight < 200)
            car.TotalLuggageWeight += this._luggageWeight; 
    }

    public void VisitForAddingLoad(Motocycle moto)
    {
        if (this.luggageWeight < 5 && moto.TotalLuggageWeight < 15)
            moto.TotalLuggageWeight += this._luggageWeight; 
    }
}

Utilisation des objets

var addPassagerVisitor = new NewPassagerLoadHandler();
var addLuggageVisitor = new NewLuggageLoadHandler(15);

foreach (var vehicle in Vehicles)
{
    vehicle.AcceptNewLoad(addPassagerVisitor);
    vehicle.AcceptNewLoad(addLuggageVisitor);
}

Pour aller plus loin…

Dépendances entre les objets

– "Visiteur" évite de lier les objets visités aux classes spécialisées de visiteurs puisque la seule dépendance est sur l’interface des visiteurs ILoadHandler.
– L’implémentation des objets visités (Vehicle) n’est pas modifiée en cas d’ajout ou de modification d’un visiteur.
En cas d’ajout d’un visiteur, l’objet visité doit hériter de Vehicle ce qui garantit l’implémentation de AcceptNewLoad() (point d’entrée des visiteurs). De même le visiteur doit satisfaire l’interface ILoadHandler donc tous les types d’objets visités seront pris en compte par les implémentations différentes de VisitForAddingLoad.
En cas d’ajout d’un objet visité, l’interface des visiteurs ILoadHandler devra être perfectionnée en rajoutant la signature:

void VisitForAddingLoad(NewVehicle newVehicle);

De fait, tous les visiteurs devront être modifiés et prendront en compte le nouvel objet. Du coté de l’objet visité, pas de changement.

Diagramme théorique

Limite

L’implémentation de ce pattern nécessite une organisation des classes sur lequel il est difficile de revenir par la suite:
– Les objets visités doivent tous hérités la classe abstraite VisitedObject,
– Tous les visiteurs doivent implémenter toutes les surcharges de la fonction Visit() correspondant aux différents types d’objets visités.
Il sera difficile et couteux d’envisager un autre pattern au cas où celui-ci ne convient plus. Il est donc préférable de limiter son utlisation à un contexte précis et un faible nombre de classes.

Design pattern: Décorateur

Objectif:

Rajouter dynamiquement une ou plusieurs compétences à une classe sans en modifier l’implémentation.

Justification

Problèmes

On souhaite ajouter un ou plusieurs comportements à une classe déjà implémentée. La méthode la plus simple est d’intervenir dans cette classe et de rajouter les comportements voulus directement. Cependant plusieurs raisons peuvent motiver le choix de ne pas toucher à l’implémentation de la classe:
Responsabilité unique: on ne veut pas compléxifier la classe en lui rajoutant trop de comportements. On évite ainsi de casser le principe de la responsabilité unique.
Comportements communs à plusieurs classes: le comportement à rajouter n’est pas forcément spécifique à la classe à modifier. Si ce comportement peut éventuellement être ajoutée à d’autres classes, on peut vouloir l’implémenter dans un objet et faire en sorte d’apporter la compétence aux autres classes sans dupliquer les implémentations.
Comportements configurables: pour éviter de rendre l’ajout de la compétence irréversible, on peut vouloir le rendre configurable ou dépendant d’un contexte.
Tests: des tests existent déjà pour cette classe, modifier le comportement de la classe peut conduire à devoir modifier les tests.

Solution

"Décorateur" rajoute le comportement à la classe à faire évoluer en l’encapsulant et en proposant les mêmes fonctions.

Par exemple, on considère la classe PersonStore permettant d’ajouter, de supprimer ou de mettre à jour des personnes. Cette classe satisfait l’interface IPersonStore:

public interface IPersonStore
{
   int AddNewPerson(string firstName, string lastName);
   void RemovePerson(string personId);
   void UpdatePerson(int personId, string firstName, string lastName);
}

public class PersonStore : IPersonStore
{
   public int AddNewPerson(string firstName, string lastName)
   { ... }

   public void RemovePerson(string personId)
   { ... }

   public void UpdatePerson(int personId, string firstName, string lastName)
   { ... }
}

On souhaite rajouter la capacité de "logging" à la classe. Le pattern "Décorateur" rajoute ce comportement en:
– implémentant le nouveau comportement dans une classe séparée appelée "Décorateur".
– le "décorateur" satisfait aussi IPersonStore pour que les classes consommatrices ne soient pas modifiées.
– le "décorateur" encapsule la classe PersonStore.

public class PersonStoreLoggingDecorator : IPersonStore
{
   private PersonStore _personStore;
   private ILog _logger;

   public PersonStoreLoggingDecorator(IPersonStore personStore, ILog logger)
   {
      this._personStore = personStore;
      this._logger = logger;
   }	

   public int AddNewPerson(string firstName, string lastName)
   {
      this._logger.Info("Adding new person");
      this._personStore.AddNewPerson(firstName, lastName);
   }

   public void RemovePerson(string personId)
   {
      this._logger.Info("Removing person");
      this._personStore.RemovePerson(personId);
   }

   public void UpdatePerson(int personId, string firstName, string lastName)
   {
      this._logger.Info("Updating person");
      this._personStore.UpdatePerson(personId, firstName, lastName);
   }
}

Le "décorateur" PersonStoreLoggingDecorator est utilisée de la même que PersonStore puisqu’elle possède la même interface:

IPersonStore personStoreWithLogger = new PersonStoreLoggingDecorator(new PersonStore());
personStoreWithLogger.AddNewPerson("Robert", "Mitchoum");

Ajouter plusieurs compétences
L’intérêt du pattern est de pouvoir ajouter plusieurs compétences en multipliant les "décorateurs". Par exemple, si on veut rajouter la sauvegarde des modifications sur un fichier, on peut implémenter un "décorateur" PersonStoreBackUpDecorator:

public class PersonStoreBackUpDecorator : IPersonStore
{
   private PersonStore _personStore;
   private PersonStoreFileWriter _fileWriter;
   
   public PersonStoreLoggingDecorator(IPersonStore personStore, 
      PersonStoreFileWriter fileWriter)
   {
      this._personStore = personStore;
      this._fileWriter = fileWriter;
   }	
   
   public int AddNewPerson(string firstName, string lastName)
   {
      this._personStore.AddNewPerson(firstName, lastName);
      this._fileWriter.Write(this._personStore);	
   }
   
   public void RemovePerson(string personId)
   {
      this._personStore.RemovePerson(personId);
      this._fileWriter.Write(this._personStore);
   }
   
   public void UpdatePerson(int personId, string firstName, string lastName)
   {
       this._personStore.UpdatePerson(personId, firstName, lastName);
       this._fileWriter.Write(this._personStore);
   }
}

On peut alors utiliser les 2 "décorateurs":

var personStore = new PersonStore();
IPersonStore personStoreWithLogger = new PersonStoreLoggingDecorator(personStore);
IPersonStore decoratedPersonStore = new PersonStoreBackUpDecorator(personStoreWithLogger);
decoratedPersonStore.AddNewPerson("Robert", "Mitchoum");

Pour aller plus loin

Version plus générique

L’exemple précédent utilisait des interfaces pour définir les décorateurs et la classe à faire évoluer. Une version plus générique de l’implémentation du pattern peut être privilégiée en utilisant:
– Une classe abstraite pour définir une version générique de la classe à faire évoluer: "Component".
– Plusieurs classes dérivant de "component" correspondant aux classes à décorer: "ConcreteComponent".
– Le "décorateur" abstrait: "Decorator".
– Tous les décorateurs spécialisés dérivant du "décorateur" abstrait: "ConcreteDecorator".

ATTENTION aux utilisations trop rigoureuses des patterns

Il est complètement inutile de vouloir implémenter un design pattern rigoureusement suivant sa définition. Chaque contexte est plus ou moins unique, il est pratiquement toujours d’adapter un pattern à se contexte.
Le pattern définit une solution qu’il convient d’adapter.

Interception

Une implémentation plus industrielle de ce pattern serait de privilégier l’interception pour découpler davantage les classes. Cette approche est plus configurable et des outils d’injection de dépendances aideraient à rendre l’implémentation plus évolutive.

Injection de dépendances en utilisant Unity en 10 min

L’intérêt de l’injection de dépendances est de permettre:
– une meilleure maintenabilité,
– de mettre en place plus facilement une méthode TDD (Test Driven Development),
– d’être plus flexible (plus facile de s’adapter à une nouvelle implémentation),
– d’être plus extensible (ajout plus facile de nouvelles fonctionnalités),
– supporter le "late binding" (inclure des modules sans recompiler),
– le développement paralllèle,
– faible couplage en réduisant le nombre de dépendances.

Il existe plusieurs bibliothèques disponibles en .NET permettant l’injection de dépendances:
Castle Windsor disponible sur GitHub. Une documentation est aussi disponible sur GitHub.
Spring.NET.
StructureMap présent sur GitHub très bien documenté.
Autofac
Unity: le code est disponible sur GitHub. La documentation se trouve sur MSDN.
Ninject
S2Container.NET
PicoContainer.NET disponible sur GitHub.

Quelques approches pour déléguer la gestion d’objets

Plusieurs approches permettent de déléguer l’instanciation et la gestion de la durée de vie d’objets à d’autres classes différentes de celles qui les consomment. Les patterns "Factory" et le pattern "Service Locator" sont quelques unes de ces approches.

Patterns "Factory"

Il existe 3 implémentations du pattern factory: "factory method", fabrique abstraite et factory simple.

Method factory

Dans une classe consommatrice, on implémente une méthode virtuelle qui sera chargée de créer l’objet. Cette méthode peut, par exemple, être appelée dans le constructeur de la classe consommatrice. De plus elle peut être surchargée dans une classe qui hérite de la classe consommatrice.

Inconvénients:
– Si plusieurs classes consommatrices doivent utiliser le même objet, elles devront toute implémenter une fonction pour créer l’objet. De plus, cette fonction peut nécessiter d’autres objets pour créer l’objet, ce qui complexifie le modèle.
– Les tests à effectuer sur la classe consommatrice, ne sont pas évident à implémenter puisqu’il faut prévoir un mécanisme pour simuler la création de l’objet.

Factory simple

Ce pattern introduit une "factory" pour déléguer la création de l’objet. La classe consommatrice crée donc la "factory" et crée l’objet consommé au moyen de cette "factory".

Inconvénients
– La complexité de ce modèle peut augmenter rapidement si la classe consommatrice doit créer beaucoup d’objets.
– Si on doit créer des objets de type différent, il faudra créer les "factory" correspondantes. La logique de choix des "factory" reste dans la classe consommatrice. L’utilisation de plusieurs "factory" correspond au pattern "fabrique abstraite".

Exemple:

public class ManagementController : Controller
{
  private readonly ITenantStore tenantStore;

  public ManagementController()
  {
    var tenantStoreFactory = new TenantStoreFactory();
    this.tenantStore = tenantStoreFactory.CreateTenantStore();
  }

  public ActionResult Index()
  {
    var model = new TenantPageViewData<IEnumerable>(this.tenantStore.GetTenantNames())
    {
      Title = "Subscribers"
    };
    return this.View(model);
  }

  ...
}

Pattern "Service Locator"

Le "Service Locator" se comporte comme un registre à qui on demande des objets. C’est le "Service Locator" qui va créer et enregistrer l’objet. Pour obtenir l’instance, la classe consommatrice doit fournir le nom de l’objet et le type voulu en retour. En utilisant ce pattern, le "Service Locator" devient le responsable de la durée de vie de l’objet.

Inconvénients:
La seule contrainte de ce pattern est de maintenir une référence vers le "Service Locator" dans la classe consommatrice de façon y faire appel pour créer les objets.

Injection de dépendances

Dans la classe consommatrice, l’injection de l’objet se fait par le constructeur. Ainsi on ne garde pas une référence vers un "service locator" ou vers une "factory". De plus la gestion de la durée de vie de l’objet n’est pas effectuée par la classe consommatrice. On a une inversion de contrôle puisque l’objet est poussé vers la classe consommatrice sans demande explicite.

Un autre intérêt est que la classe consommatrice n’a pas de connaissance sur l’objet à créer puisque l’objet n’est manupulé qu’au moyen de son interface.

Exemple:

public class ManagementController : Controller
{
  private readonly ITenantStore tenantStore;

  public ManagementController(ITenantStore tenantStore)
  {
    this.tenantStore = tenantStore;
  }

  public ActionResult Index()
  {
    var model = new TenantPageViewData<IEnumerable>(this.tenantStore.GetTenantNames())
    {
      Title = "Subscribers"
    };
    return this.View(model);
  }

  ...
}

L’injection de dépendances permet de minimiser les dépendances puisque dans le cas d’une "factory", la classe consommatrice possède une dépendance vers la "factory" et vers l’interface de l’objet à utiliser.

Dans le cas de l’injection de dépendance, sachant que l’objet à utiliser est injecté directement dans la classe consommatrice, il n’y a plus de dépendances vers une "factory" ou une autre classe qui fournit l’objet.

Instanciation des objets

L’injection de dépendances nécessite de devoir instancier tous les objets dans l’application. Cette instanciation doit se faire très tôt dans le cycle de vie de l’application pour être correctement utliisé par la suite (i.e. dans le Main() d’une application "Console", dans le "Global.asax" d’une application web etc…).

Durée de vie des objets

Certaines problématiques concernent la durée de vie des objets:
– Un objet doit-il être partagé entre plusieurs classes ou chaque classe doit-elle utiliser une seule instance.
– Quelle est la durée de vie de l’objet ? Sa durée de vie doit être lié à celle de la classe consommatrice et à celle de l’application.

Type d’injection

Plusieurs types d’injection:
Injection par le constructeur: comme dans l’exemple précédent.
Injection en utilisant un accesseur: à utiliser dans le cas où la dépendances est optionnelle sinon privilégier l’injection par le constructeur.
Injection par appel à une méthode: cete méthode peut êter utile quand il est nécessaire d’avoir un paramètre supplémentaire qui ne peut être passé dans le constructeur.

Exemple:

public class MessageQueue : ...
{
  public MessageQueue(StorageAccount account)
  : this(account, typeof(T).Name.ToLowerInvariant())
  {
  }

  public MessageQueue(StorageAccount account,
                     string queueName)
  {
    ...
  }

  public void Initialize(TimeSpan visibilityTimeout, IRetryPolicyFactory retryPolicyFactory)
  {
    ...
  }
  
  ...
}

Quand ne pas utiliser l’injection de dépendances

– Petites applications,
– Dans une application existante qui n’est pas conçue à l’origine pour l’inversion de contrôle.
– L’enregistrement des types et la recherche d’instances introduisent un temps d’exécution plus long. Ce temps est très négligeable pour la recherche d’instances mais est plus pénalisant pour l’enregistrement (même s’il n’est effectué qu’une seule fois).

Injection de dépendances avec Unity

Dans le cycle de vie d’un objet manipulé par l’injection de dépendances, il y a 3 étapes:
L’enregistrement: elle se fait auprès d’un conteneur d’objets appelé "container":

var container = new UnityContainer();
container.RegisterType();

La résolution: qui permet d’instancier à la fois la classe consommatrice et les objets à injecter dans la classe consommatrice:

var controller = container.Resolve();

La suppression par le garbage collector.

Exemples d’enregistrement

Enregistrement d’instances

L’instance est enregistrée directement. L’instance peut ensuite être retrouvée par son type:

StorageAccount account = ApplicationConfiguration.GetStorageAccount("DataConnectionString");
container.RegisterInstance(account);

Enregistrement d’un type simple

On enregistre un type qui sera reconnue par son interface:

container.RegisterType();

La résolution permet d’instancier l’objet à partir de l’interface:

var surveyStore = container.Resolve();

Enregistrement d’un type par nommage

En nommant l’enregistrement, il est possible d’en utliser plusieurs instances:

container
  .RegisterType<IMessageQueue, MessageQueue>(
    "Standard", new InjectionConstructor(storageAccountType, retryPolicyFactoryType,
      Constants.StandardAnswerQueueName))
  .RegisterType<IMessageQueue, MessageQueue>(
    "Premium", new InjectionConstructor(storageAccountType, retryPolicyFactoryType,
      Constants.PremiumAnswerQueueName));

La résolution peut se faire aussi en utilisant les noms:

container.Resolve<IMessageQueue>("Standard"),
container.Resolve<IMessageQueue>("Premium"),

Injection avec le constructeur

On peut injecter des classes enregistrées avec des paramètres:

public DataTable(StorageAccount account, IRetryPolicyFactory retryPolicyFactory, 
  string tableName)
{
  ...
}

Au préalable, les types auront été enregistrés avec:

container
  .RegisterType<IDataTable, DataTable>(
    new InjectionConstructor(storageAccountType,
      retryPolicyFactoryType, Constants.SurveysTableName))
  .RegisterType<IDataTable, DataTable>(
    new InjectionConstructor(storageAccountType,
      retryPolicyFactoryType, Constants.QuestionsTableName));

Enregistrer des génériques
Pour enregistrer une classe qui utlise des génériques, la syntaxe est un peu différente:

container.RegisterType(typeof(IMessageQueue), typeof(MessageQueue),
  new InjectionConstructor(storageAccountType, retryPolicyFactoryType, typeof(string)));

La résolution du type se fait en faisant:

container.Resolve<IMessageQueue>(...)

Surcharge des paramètres
Si on doit préciser un paramètre dans le constructeur et que la valeur n’est pas connu au moment de l’enregistrement, on peut préciser le type de ce paramètre. Ainsi au moment de la résolution, il sera possible d’indiquer la valeur du paramètre et exécuter le bonne surcharge du constructeur.

Par exemple:

container.RegisterType(typeof(IMessageQueue), typeof(MessageQueue),
  new InjectionConstructor(storageAccountType, retryPolicyFactoryType, typeof(string)));
...

container.RegisterType<IBlobContainer,  EntitiesBlobContainer>(
  new InjectionConstructor(storageAccountType, retryPolicyFactoryType, typeof(string)));

Exemple de résolution de types

Résolution simple

container.Resolve().Initialize();

Gestion de la durée de vie

Lifetime manager Application Dépendances de référence Exemple
ContainerControlledLifetimeManager Singleton Container container.RegisterType(
new ContainerControlledLifetimeManager());
HierarchicalLifetimeManager Singleton ChildContainer IUnityContainer container = new UnityContainer();
container.RegisterType(
new HierarchicalLifetimeManager());
IUnityContainer child1 = container.CreateChildContainer();
IUnityContainer child2 = container.CreateChildContainer();
var tenant1 = child1.Resolve();
var tenant2 = child2.Resolve();
var tenant3 = container.Resolve();
TransientLifetimeManager Instance Une instance différente par appel à Resolve().
PerResolveLifetimeManager Instance Une seule instance par appel à Resolve().
ExternallyControlledLifetimeManager Singleton Aucune ("weak reference")
PerRequestLifetimeManager Instance Une instance par appel à Resolve() et par requête HTTP.

Il est possible de définir une "LifetimeManager" spécialisé.

Interception

Ce parttern permet d’incorporer des fonctionnalités à une classe: logging, gestion d’exception, authentification, gestion de cache etc…

L’ajout de fonctionnalités doit toutefois satisfaire certains critères de qualité:
– Cohérence,
– Maintenabilité du code,
– Eviter la duplication du code.

Pattern "décorateur"

Ce pattern permet de répondre à la problématique de l’interception en rajoutant dynamiquement des compétences à une classe:
Si on considère l’interface:

public interface ITenantStore
{
  void Initialize();
  Tenant GetTenant(string tenant);
  IEnumerable GetTenantNames();
  void SaveTenant(Tenant tenant);
  void UploadLogo(string tenant, byte[] logo);
}

public class TenantStore : ITenantStore
{
  public TenantStore(IBlobContainer tenantBlobContainer,
    IBlobContainer logosBlobContainer)
  {
    ...
  }
}

On ajoute une compétence à la classe en injectant une classe du même type. Dans cet exemple, la compétence rajoutée est le logging:

class LoggingTenantStore : ITenantStore
{
  private readonly ITenantStore tenantStore;
  private readonly ILogger logger;
  public LoggingTenantStore(ITenantStore tenantstore, ILogger logger)
  {
    this.tenantStore = tenantstore;
    this.logger = logger;
  }
  public void Initialize()
  {
    tenantStore.Initialize();
  }

  public Tenant GetTenant(string tenant)
  {
    return tenantStore.GetTenant(tenant);
  }

  public IEnumerable GetTenantNames()
  {
    return tenantStore.GetTenantNames();
  }

  public void SaveTenant(Tenant tenant)
  {
    tenantStore.SaveTenant(tenant);
    logger.WriteLogMessage("Saved tenant" + tenant.Name);
  }

  public void UploadLogo(string tenant, byte[] logo)
  {
    tenantStore.UploadLogo(tenant, logo);
    logger.WriteLogMessage("Uploaded logo for " + tenant);
  }
}

On peut ensuite rajouter plusieurs compétences en faisant des cascades "d’injection".

Pattern "interception"

"Interception" permet d’introduire dynamiquement entre la classe appelante et la classe cible, du code responsable de l’ajout de compétences. La différence entre "décorateur" et "interception" est que interception crée lui-même dynamiquement les classes dans lesquelles on veut rajouter les compétences.
Le nom "interception" provient du fait qu’on intercepte la classe pour lui rajouter des compétences.

Concrètement l’interception permet d’ajouter un comportement à une classe sans inplémenter directement ce comportement dans la classe. Par exemple, si on souhaite logguer des évènements, il suffit de configurer un comportement pour qu’il se déclenche lors de l’appel de fonctions et d’attacher ce comportement à la class. L’intérêt est aussi de pouvoir rajouter plusieurs comportements.

Interception d’instances

Dans ce cas, Unity instancie:
– Un objet proxy (du même type que la classe cible) qui est inséré entre la classe appelante et la classe cible. C’est sur cet objet qu’on rajoutera les différentes compétences.
– Un pipeline de comportements ("behavior pipeline") permettant de rajouter des compétences à l’objet proxy. On utilise le terme "pipeline" car les compétences sont appelées à la suite.

La classe cible doit dériver de "MarshalByRefObject" ou implémenter une interface qui définit les méthodes à intercepter. Il n’est pas possible de faire un "cast" de l’objet proxy vers le type de la classe cible.

Interception de type

Dans ce cas, Unity génère directement la classe cible avec les compétences voulues en fonction de son type. Les fonctions à intercepter doivent être virtuelles.

Implémentation de l’interception avec Unity

Prérequis

Pour utiliser l’interception avec Unity, il faut ajouter l’extension correspondante avec NuGet:

using Microsoft.Practices.Unity.InterceptionExtension;
...
IUnityContainer container = new UnityContainer();
container.AddNewExtension();

Définition d’un intercepteur

Il suffit de satisfaire l’interface "IInterceptionBehavior" et d’implémenter la méthode:

public IMethodReturn Invoke(IMethodInvocation input, GetNextInterceptionBehaviorDelegate getNext)

Cette méthode est exécutée lorsqu’on appelle la méthode dans la classe interceptée. "Input" permet de connaître les paramètres d’entrée de la fonction et "getNext" permet d’exécuter ou non le prochain intercepteur dans le pipeline.
L’appel au prochain intercepteur peut se faire en implémentant:

IMethodReturn result = getNext()(input, getNext);

Enregistrer un intercepteur

container.AddNewExtension();
container.RegisterType(
  new Interceptor(),
  new InterceptionBehavior(),
  new InterceptionBehavior());

"TenantStore" est la classe interceptée; "ITenantStore" correspond à l’interface de "TenantStore".

"InterfaceInterceptor" est la méthode d’interception (par interface), on peut utiliser:
TransparentProxyInterceptor pour l’interception d’instance
VirtualMethodInterceptor pour l’interception par type.

"LoggingInterceptionBehavior" et "CachingInterceptionBehavior" sont des intercepteurs satisfaisant l’interface IInterceptionBehavior.

L’ordre de l’enregistement est important puisqu’il sera le même dans le pipeline de comportements.

Appel à un intercepteur

SaveTenant() est une méthode définie dans TenantStore et qui sera interceptée. La variable "tenantStore" n’est pas de type "TenantStore" mais il s’agit d’une classe proxy qui satisfait "ITenantStore" et qui sera interceptée.

Interception d’instance et interception de type

TransarentProxyInterceptor

Permet d’intercepter un objet qui satisfait plusieurs interfaces.

Par exemple:

container.RegisterType<ITenantStore, TenantStore>(
  new Interceptor(),
  new InterceptionBehavior(),
  new InterceptionBehavior());

var tenantStore = container.Resolve();

// From the ITenantStore interface.
tenantStore.SaveTenant(tenant);

// From the ITenantLogoStore interface.
((ITenantLogoStore)tenantStore).SaveLogo("adatum", logo);

Dans l’exemple, "TenantStore" satisfait "ITenantStore" et "ITenantLogoStore". Avec TransparentProxyInterceptor, la classe proxy satisfera aussi les deux interfaces.
TransparentProxyInterceptor est plus lent à s’exécuter.

VirtualMethodInterceptor

Contrairement à InterfaceInterceptor et TransparentProxyInterceptor qui utilisent un objet proxy qui satisfait une interface, VirtualMethodInterceptor utilise un objet qui dérive de la classe modèle et dont les méthodes virtuelles sont réimplémentées.

Utiliser un comportement pour ajouter une interface à une classe existante

Il peut être nécessaire de rajouter une interface par implémentation, on peut le faire en considérant l’interface comme un comportement:

container.Register(
    new Interceptor(),
    new InterceptorBehavior(),
    new InterceptorBehavior(),
    new AdditionalInterface());

Dans l’exemple, on rajoute l’interface "ILogger".

Implémenter l’interception sans utiliser de container

On peut éviter d’appeler UnityContainer" en faisant appel directement à la classe "intercept".

Le code avec "container" est:

// Example 1. Using a container.
// Configure the container for interception.
container = new UnityContainer();
container.AddNewExtension();

// Register the TenantStore type for interception.
container.RegisterType(
  new Interceptor(),
  new InterceptionBehavior(),
  new InterceptionBehavior());

// Obtain a proxy object with an interception pipeline.
var tenantStore = container.Resolve();

L’équivalent sans "container" est:

// Example 2. Using the Intercept class.
ITenantStore tenantStore = Intercept.ThroughProxy(
  new TenantStore(tenantContainer, blobContainer), new InterfaceInterceptor(),
  new IInterceptionBehavior [] { new LoggingInterceptionBehavior(), 
    new CachingInterceptionBehavior()
  });

Intercept.ThroughProxy permet de créer un objet proxy.
Intercept.NewInstance permet de créer une nouvelle instance qui dérive de la classe à intercepter.

Politique d’injection

L’inconvénient de l’approche présentée précédemment est qu’elle nécessite d’enregistrer les interceptions auprès de la classe "container" pour toutes les classes pour lesquelles on souhaite un comportement d’interception. Une autre approche est de permettre l’interception lorsque certaines conditions sont réunies:
– La classe se trouve dans un namespace particulier.
– La classe possède des propriétés avec certaines valeurs.
– La classe possède un attribut particulier.
– etc…

La politique d’injection permet de définir les conditions que doivent satisfaire une classe pour qu’elle soit interceptée évitant ainsi d’avoir à configurer explicitement l’interception pour cette classe.

L’exemple suivant permet de montrer une implémentation de la politique d’injection pour les classes se
trouvant dans un namespace particulier ("Tailspin.Web.Survey.Shared") et dont l’interception se déclenche lors de l’appel à des fonctions avec des signatures commençant par "Get*" et "Save*":

container.RegisterType(
  new InterceptionBehavior(),
  new Interceptor());

container.RegisterType(
  new InterceptionBehavior(),
  new Interceptor());

var first = new InjectionProperty("Order", 1);
var second = new InjectionProperty("Order", 2);

container.Configure()
  .AddPolicy("logging")
  .AddMatchingRule(new InjectionConstructor(
    new InjectionParameter("Tailspin.Web.Survey.Shared")))
  .AddCallHandler(new ContainerControlledLifetimeManager(),
    new InjectionConstructor(), first);

container.Configure()
  .AddPolicy("caching")
  .AddMatchingRule(new InjectionConstructor(new [] {"Get*", "Save*"}, true))
  .AddMatchingRule(new InjectionConstructor(
      "Tailspin.Web.Survey.Shared.Stores", true))
  .AddCallHandler(new ContainerControlledLifetimeManager(),
    new InjectionConstructor(), second);

Il existe aussi une implémentation qui permet de configurer des comportements par attribut. Ainsi on enregistre le comportement d’interception au niveau de "container" et l’application des comportements se fait en décorant la classe ou des fonctions avec un attribut particulier.

Références

Developer’s Guide to Dependency Injection Using Unity: https://msdn.microsoft.com/en-us/library/dn223671%28v=pandp.30%29.aspx
List of .NET Dependency Injection Containers (IOC): http://www.hanselman.com/blog/ListOfNETDependencyInjectionContainersIOC.aspx

Principe de développement orienté-objet SOLID

L’acronyme signifie:
Single responsability principle (SRP°: responsabilité unique,
Open/close principle (OCP): principe ouvert/fermé,
Liskov substitution principle (LSP): substitution de Liskov,
Interface segragation principle (ISP): ségrégation des interfaces,
Dependency inversion principle (DIP): inversion des dépendances.

Responsabilité unique

Une classe doit avoir une et seulement une seule raison de changer.

Principe ouvert/fermé

"Les entités logicielles (classes, modules, fonctions…) doivent être ouvertes aux extensions et fermées aux modifications" (Bertrand Meyer, Object-Oriented Software Construction, 1988).

Il doit être possible de modifier le code d’une classe pour résoudre des défauts cependant on doit pouvoir étendre les fonctionnalités d’une classe sans la modifier. Ce principe permet d’aider à garder un code maintenable et testable car le comportement actuel ne doit pas changer, les nouveaux comportements existent dans de nouvelles classes. En suivant le principe ouvert/fermé, il est ainsi possible d’ajouter des fonctionnalités transverses à une application.
Par exemple, si on souhaite ajouter la fonctionnalité de "logging" à un ensemble de classes, on ne doit pas changer l’implémentation des classes existantes.

Substitution de Liskov

Ce principe indique que: si S est un sous-type de T alors les objets de type T peuvent être remplacés par des objets de type S sans altérer les propriétés ni l’exactitude du programme.

Une façon de répondre à ce principe est d’utiliser des interfaces ou des classes abstraites. Ainsi si on définit le constructeur d’une classe avec un objet définit par son interface, il est possible de remplacer l’objet injecté pourvu qu’il satisfasse toujours l’interface:

public interface IShape
{
  void Draw();
}

public class ShapeDrawHandler
{
  private IShape shape;
  public ShapeDrawHandler(IShape shape)
  {
    this.shape = shape;
  }

  public void DrawShape()
  {
    this.shape.Draw();
  }
}

Quelque soit la forme géométrique IShape, elle pourra être dessinée par ShapeDrawHandler si elle satisfait l’interface IShape.

Ségrégation des interfaces

Ce principe a pour but de rendre un logiciel plus maintenable. Le principe de ségrégation des interfaces encourage le faible couplage et ainsi, rend un système plus facile à refactorer, changer et redéployer.
Ce principe indique qu’une interface très grande devrait être séparée en plusieurs interfaces plus petites et plus spécifiques de façon à ce que les classes consommatrices de ces interfaces connaissent seulement les méthodes qu’elles utilisent: aucune classe consommatrice d’une interface ne devrait être forcée à dépendre de méthodes qu’elle n’utilise pas.

Par exemple:

public interface IItemInitializer
{
  void InitializeItem();
  Tenant GetCreateItem(string itemId);
  IEnumerable GetItems();
}

public interface IItemUpdater
{
  void SaveItem(Item item);
}


public class ItemStore : IItemInitializer, IItemUpdater
{
  ...

  public ItemStore()
  {
    ...
  }

  ...
}

Dans ce cas, on a séparé la fonctionnalité d’initialisation de la fonctionnalité de mise à jour.

Inversion des dépendances

Ce principe indique que:
– Les modules de haut niveau ne doivent pas dépendre des modules de bas niveau. Les deux doivent dépendre d’abstractions.
– Les abstractions ne doivent pas dépendre des détails. Les détails doivent dépendre des abstractions.

Par exemple si on considère l’exemple suivant:

public class Square
{
  public void Draw()
  {...}
}

public class ShapeDrawer
{
  private Square shape;
  public ShapeDrawer()
  {
    this.shape = new Square();
  }

  public void DrawShape()
  {
    this.shape.Draw();
  }
}

La classe générale ShapeDrawer dépend d’une classe plus spécifique Square, ce qui limite la possibilité de réutiliser ShapeDrawer dans un cadre plus générale.
La modification suivante permet de casser la dépendance entre ShapeDrawer et Square:

public interface IShape
{
  void Draw();	
}

public class Square : IShape
{
  public void Draw()
  {...}
}

public class ShapeDrawer
{
  private IShape shape;
  public ShapeDrawer(IShape shape)
  {
    this.shape = shape;
  }

  public void DrawShape()
  {
    this.shape.Draw();
  }
}

Aide-mémoire SQL Oracle

Requêtage

INSERT, UPDATE, DELETE

INSERT

Exemple 1:

INSERT INTO Table(nom colonnes) values (valeurs) 

Exemple 2:

INSERT INTO Table(nom colonnes) SELECT colonnes FROM Table2 WHERE … 
Remarque:

Possible d’utiliser le mot clé EXISTS

INSERT INTO clients 
(client_id, client_name, client_type) 
SELECT supplier_id, supplier_name, 'advertising' 
FROM suppliers 
WHERE not exists (select * from clients 
                  where clients.client_id = suppliers.supplier_id); 

UPDATE

UPDATE Nom_Table SET nom_colonne1=valeur1, nom_colonne2=valeur2 WHERE condition 

Avec le mot clé EXISTS:

UPDATE suppliers  
SET supplier_name =  
(SELECT customers.name FROM customers WHERE customers.customer_id = suppliers.supplier_id)  
WHERE EXISTS (SELECT customers.name FROM customers WHERE customers.customer_id = suppliers.supplier_id);

DELETE

DELETE FROM Nom_Table WHERE Condition

Avec le mot clé EXISTS:

DELETE FROM suppliers  
WHERE EXISTS (  
SELECT customers.customer_name FROM customers WHERE customers.customer_id = suppliers.supplier_id and customers.customer_name = 'IBM' );

IS NULL/IS NOT NULL

SELECT * FROM suppliers WHERE supplier_name IS NULL;

DISTINCT

SELECT DISTINCT city, state FROM suppliers;

Jointures

SELECT nom_colonnes FROM Table1
INNER JOIN Table2 ON Table1.colonne = Table2.colonne

Types de jointure

INNER JOIN/JOIN: jointure classique = les éléments communs aux 2 tables.
LEFT OUTER JOIN/LEFT JOIN: jointure ouverte = les éléments de la table de gauche plus les éléments communs.
RIGHT OUTER JOIN/RIGHT JOIN: jointure ouverte = les éléments de la table de droite plus les éléments communs.
FULL OUTER JOIN/FULL JOIN: les éléments des 2 tables plus les éléments communs.
La syntaxe est la même pour tous les types de jointure.

Equivalence avec les anciennes notations

Inner join

SELECT nom_colonnes FROM Table1  
INNER JOIN Table2 ON Table1.colonne = Table2.colonne

équivaut à:

SELECT nom_colonnes FROM Table1, Table2  
WHERE Table1.colonne = Table2.colonne

Left outer join

SELECT nom_colonnes FROM Table1 
LEFT OUTER JOIN Table2 ON Table1.colonne = Table2.colonne

équivaut à:

SELECT nom_colonnes FROM Table1, Table2  
WHERE Table1.colonne = Table2.colonne (+)

Right outer join

SELECT nom_colonnes FROM Table1 
RIGHT OUTER JOIN Table2 ON Table1.colonne = Table2.colonne

équivaut à:

SELECT nom_colonnes FROM Table1, Table2 
WHERE Table1.colonne (+) = Table2.colonne

Full outer join

SELECT nom_colonnes FROM Table1 
FULL OUTER JOIN Table2 ON Table1.colonne = Table2.colonne

n’a pas d’équivalent dans l’ancienne notation.

Sous-requêtes

Dans le WHERE

SELECT *  
FROM all_tables tabs  
WHERE tabs.table_name IN ( 
   SELECT cols.table_name  
   FROM all_tab_columns cols  
   WHERE cols.column_name = 'SUPPLIER_ID' 
);
ATTENTION

La fonction "IN" est limité à 1000 éléments.

Dans le FROM

SELECT suppliers.name, subquery1.total_amt  
FROM suppliers,  
( 
   SELECT supplier_id, SUM(orders.amount) as total_amt  
   FROM orders  
   GROUP BY supplier_id 
) subquery1  
WHERE subquery1.supplier_id = suppliers.supplier_id;

Dans le SELECT

SELECT tbls.owner, tbls.table_name,  
(SELECT COUNT(column_name) AS total_columns from all_tab_columns cols where cols.owner = tbls.owner and cols.table_name = tbls.table_name) subquery2 FROM all_tables tbls;

ORDER BY

On peut en mettre deux:

SELECT supplier_city, supplier_state  
FROM suppliers  
WHERE supplier_name = 'IBM'  
ORDER BY supplier_city DESC, supplier_state ASC;

On peut indiquer la position relative d’une colonne pour le tri:

SELECT supplier_city, supplier_state  
FROM suppliers  
WHERE supplier_name = 'IBM'  
ORDER BY 1;

Dans ce cas le tri se fera par "supplier_city".

COUNT

Pour compter le nombre d’éléments pour une colonne donnée i.e. les éléments nuls ne seront pas comptés:

SELECT COUNT(State) FROM suppliers;

Pour compter le nombre d’éléments différents pour une colonne donnée:

SELECT COUNT(DISTINCT department) as "Unique departments"  
FROM employees WHERE salary > 25000;

SUM

On peut sommer les éléments différents:

SELECT SUM(DISTINCT salary) as "Total Salary"  
FROM employees WHERE salary > 25000;

LIKE

A placer dans les conditions:
Pour appliquer une condition sur une partie d’une chaîne de caractères quelque soit le nombre de caractères:

SELECT * FROM suppliers WHERE supplier_name LIKE 'Hew%'; 
SELECT * FROM suppliers WHERE supplier_name LIKE '%Hew%'; 
SELECT * FROM suppliers WHERE supplier_name LIKE '%Hew';

Pour appliquer une condition sur une partie d’une chaîne de caractères en prenant en compte le nombre de caractères:

SELECT * FROM suppliers WHERE supplier_name LIKE 'Sm_th';

Pour appliquer les conditions sur des chaînes de caractères contenant le mot clé ‘%’ ou ‘_’, il faut définir le caractère d’échappement:

SELECT * FROM suppliers WHERE supplier_name LIKE 'H%!_' ESCAPE '!';

IN/NOT IN

SELECT * FROM suppliers  
WHERE supplier_name not in ( 'IBM', 'Hewlett Packard', 'Microsoft');

BETWEEN

SELECT * FROM suppliers WHERE supplier_id BETWEEN 5000 AND 5010;

EXISTS/NOT EXISTS

Permet de définir une condition en utilisant une sous-requête:

SELECT * FROM suppliers  
WHERE EXISTS  
( 
    SELECT * FROM orders WHERE suppliers.supplier_id = orders.supplier_id 
);

GROUP BY

SELECT department, SUM(sales) as "Total sales"  
FROM order_details  
GROUP BY department

HAVING

Permet de rajouter une condition avec les opérateurs SUM, COUNT, MIN, MAX:

SELECT department, SUM(sales) as "Total sales"  
FROM order_details  
GROUP BY department  
HAVING SUM(sales) > 1000;

UNION

Pour additionner les résultats de 2 requêtes sans afficher les doublons:

SELECT supplier_id FROM suppliers  
UNION  
SELECT supplier_id FROM orders;

On peut ordonner les résultats en utilisant ORDER BY:

SELECT supplier_id, supplier_name FROM suppliers WHERE supplier_id > 2000 UNION  
SELECT company_id, company_name FROM companies WHERE company_id > 1000  
ORDER BY 2;

Pour afficher les doublons, il faut utiliser UNION ALL:

SELECT upplier_id FROM suppliers  
UNION ALL  
SELECT supplier_id FROM orders;

INTERSECT

Syntaxe similaire à UNION mais il permet de ramener seulement l’intersection des deux requêtes:

SELECT supplier_id FROM suppliers  
INTERSECT  
SELECT supplier_id FROM orders;

MINUS

Permet de récupérer les lignes de la 1ère requête qui ne sont pas présentes dans la 2e requête.
Même syntaxe que UNION.

Opérations sur les tables et les vues

CREATE TABLE

CREATE TABLE suppliers  
(  
    supplier_id number(10) not null,  
    supplier_name varchar2(50) not null,  
    contact_name varchar2(50)  
);

Clé primaire

CONSTRAINT customers_pk PRIMARY KEY (customer_id)

Avec la requête:

CREATE TABLE suppliers  
(  
    supplier_id number(10) not null,  
    supplier_name varchar2(50) not null,  
    contact_name varchar2(50), 
    CONSTRAINT customers_pk PRIMARY KEY (customer_id) 
);

Clé étrangère

CONSTRAINT fk_departments FOREIGN KEY (department_id) REFERENCES departments(department_id)

Avec la requête:

CREATE TABLE employees  
(  
    employee_number number(10) not null,  
    employee_name varchar2(50) not null,  
    department_id number(10), 
    salary number(6),  
    CONSTRAINT employees_pk PRIMARY KEY (employee_number),  
    CONSTRAINT fk_departments FOREIGN KEY (department_id) REFERENCES departments(department_id)  
);

ON DELETE CASCADE
Permet de lier l’entrée de la table mère à l’entrée de la table fille. Ainsi si il existe une clé étrangère entre les 2 tables et si on supprime l’entrée dans la classe mère, l’entrée liée par la clé étrangère dans la classe fille sera aussi supprimée.

CREATE TABLE products  
(  
    product_id numeric(10) not null,  
    supplier_id numeric(10) not null,  
    CONSTRAINT fk_supplier FOREIGN KEY (supplier_id) REFERENCES supplier(supplier_id) ON DELETE CASCADE  
);

ON DELETE SET NULL
Permet de paramètrer "null" dans la colonne de la table fille avec la clé étrangère si l’entrée de la classe mère est supprimée:

CREATE TABLE products  
(  
    product_id numeric(10) not null,  
    supplier_id numeric(10),  
    CONSTRAINT fk_supplier FOREIGN KEY (supplier_id) REFERENCES supplier(supplier_id) ON DELETE SET NULL  
);

Supprimer la contrainte sur la clé étrangère:

ALTER TABLE products drop CONSTRAINT fk_supplier;

Pour créer une table à partir d’une autre

CREATE TABLE suppliers AS  
(SELECT * FROM companies WHERE id > 1000);

ALTER TABLE

Renommer une table:

ALTER TABLE suppliers  
RENAME TO vendors;

Ajouter une ou plusieurs colonnes

ALTER TABLE supplier  
ADD (supplier_name varchar2(50), city varchar2(45));

Modifier une colonne

ALTER TABLE supplier  
MODIFY (supplier_name varchar2(100) not null, city varchar2(75));

Supprimer une colonne

ALTER TABLE supplier  
RENAME COLUMN supplier_name to sname;

DROP TABLE

DROP TABLE supplier;

TRUNCATE TABLE

Pour vider le contenu d’une table

TRUNCATE TABLE supplier;

Tables temporaires

Leur existence persiste seulement pendant la durée de la session.

CREATE GLOBAL TEMPORARY TABLE supplier  
(  
    supplier_id numeric(10) not null,  
    supplier_name varchar2(50) not null,  
    contact_name varchar2(50) 
);

Création d’une vue

Les vues sont des tables virtuelles qui n’existent pas en réalité.

CREATE VIEW sup_orders AS  
SELECT suppliers.supplier_id, orders.quantity, orders.price FROM suppliers INNER JOIN orders ON suppliers.supplier_id = orders.supplier_id WHERE suppliers.supplier_name = 'IBM';

Suppression d’une vue

DROP VIEW view_name;

Contraintes

UNIQUE

Permet d’indiquer que les entrées de la colonne sont uniques. Cette colonne n’est pas la clé primaire (car il ne peut y avoir une colonne en tant que clé primaire et avec la contrainte unique).

CREATE TABLE table_name  
(  
    column1 datatype null/not null,  
    column2 datatype null/not null,  
    ...  
    CONSTRAINT constraint_name UNIQUE (column1, column2, . column_n)  
);

CHECK

Permet d’effectuer un contrôle entre la colonne avec la contrainte et une autre colonne de la table. Une contrainte CHECK ne peut référencer une autre table.

CREATE TABLE suppliers  
(  
    supplier_id numeric(4),  
    supplier_name varchar2(50),  
    CONSTRAINT check_supplier_name CHECK (supplier_name = upper(supplier_name))  
);

ou

...
CONSTRAINT check_supplier_id CHECK (supplier_id BETWEEN 100 and 9999)

Fonctions particulières

Conversion d’une chaîne de caractères en date

A utilisant dans une condition par exemple:

TO_DATE('2003/12/31','yyy/mm/dd')

CASE…WHEN…ELSE…END

Peut être utiliser dans une clause SELECT:

SELECT table_name,  
CASE owner
    WHEN 'SYS' THEN 'The owner is SYS'  
    WHEN 'SYSTEM' THEN 'The owner is SYSTEM'  
    ELSE 'The owner is another value'  
END  
FROM all_tables;

CONCAT

CONCAT( string1, string2 )

CURRENT_DATE

select CURRENT_DATE from dual;

DECODE

Espèce de If…then…else:

SELECT supplier_name,  
DECODE(supplier_id,  
    10000, 'IBM',  
    10001, 'Microsoft',  
    10002, 'Hewlett Packard',  
    'Gateway') result  
FROM suppliers;

est équivalent à:

if (supplier_id == 10000) 
  result := 'IBM' 
else if (supplier_id == 10001) 
  result := 'Microsoft' 
etc...

TRANSLATE

Permet de remplacer une séquence de caractères, caractère par caractère.

TRANSLATE(string1, string_to_replace, replacement_string)
ATTENTION

Le remplacement se fait caractère par caractère.

Exemple:

TRANSLATE('1tech23', '123', '456'); would return '4tech56' 
TRANSLATE('222tech', '2ec', '3it'); would return '333tith'

NVL

NVL( string1, replace_with_if_null )

Si ‘string1’ est non nul alors ‘string1’ sinon ‘replace_with_if_null’

LPAD/RPAD

Permet de rajouter des caractères avant/après une chaîne de caractères.

LPAD('tech', 8, '0');

renvoie '0000tech'

Utilisation de LIKE dans des requêtes SQL

LIKE s’utilise avec quelques caractères pour indiquer des conditions de recherche:

Caractère Signification
% Chaîne de caractères contenant un nombre variable de caractères. Pas de restrictions sur les caractères.
_ Représente un seul caractère.
[ ] Permet d’indiquer un caractère possible parmi un ensemble.
Par exemple:
[a-f] permet d’indiquer un caractère entre a et f;
[abcdef] permet d’indiquer un caractère parmi a, b, c, d, e ou f). Cet ensemble ne concerne qu’un seul caractère.
[^] Permet d’indiquer le caractère qui ne sera pas possible.
Par exemple:
[^a - f] permet d’indiquer n’importe quel caractère sauf de a à f;
[^abcdef] permet d’indiquer tous les caractères sauf a, b, c, d, e ou f.

Caractère d’échappement

Permet d’échapper ainsi:

Sql Signification
LIKE '5[%]' Signifie 5%
LIKE '5%' Signifie 5 suivi de n’importe quel caractère.
LIKE '[_]n' Signifie _n
LIKE '_n' Permet d’indiquer un caractère. Exemple: an, in, on (etc…)
LIKE '[a-cdf]' Signifie a, b, c, d, ou f
LIKE '[-acdf]' Signifie -, a, c, d, ou f
LIKE '[ [ ]' Signifie [
LIKE ']' Signifie ]

PL/SQL

Différences entre une fonction et une procédure

– Une fonction ne peut ramener qu’une seule valeur et obligatoire une valeur.
– Une procédure stockée peut ramener une ou plusieurs valeurs.
– Une fonction peut être appelée par une procédure alors qu’une procédure ne peut être appelée par une fonction.
– Les procédures permettent de gérer les transactions contrairement aux fonctions.
– Les fonctions peuvent être appeler dans des clauses WHERE, HAVING et SELECT.

Déclaration d’une fonction

CREATE OR REPLACE Function FindCourse ( name_in IN varchar2 )  
    RETURN number  
IS  
    cnumber number;  
    cursor c1 IS  
        select course_number from courses_tbl  
        where course_name = name_in;  
BEGIN  
    open c1;  
    fetch c1 into cnumber;  
    if c1%notfound then  
        cnumber := 9999;  
    end if;  
    close c1;  
RETURN cnumber;  
EXCEPTION  
WHEN OTHERS THEN  
    raise_application_error(-20001,'An error was encountered - '||SQLCODE||' -ERROR- '||SQLERRM);  
END;

Appel d’une fonction:

SELECT FindCourse('Yop') FROM DUAL;
SELECT course_name, FindCourse(course_name) AS course_id FROM courses

Déclaration d’une procédure stockée

Les paramètres

IN: peuvent être utilisés dans la procédure mais ils ne peuvent être modifiés.
OUT: ils doivent obligatoirement être affectés dans la procédure.
IN OUT: ils peuvent être utilisés dans la procédure et ils peuvent être modifiés.

Déclaration

CREATE OR REPLACE Procedure FindCourse ( name_in IN varchar2 )  
IS  
    cnumber number;  
    cursor c1 IS  
        select course_number from courses_tbl  
        where course_name = name_in;  
BEGIN  
    ... 
END;

Appel d’une procédure stockée:

Execute FindCourse('Yop');

Trigger

Procédure stockée exécutée lors d’ordres SQL: SELECT, UPDATE, INSERT ou DELETE.

On peut utiliser dans un trigger 2 variables particulières:
OLD représentant la valeur avant modification. Elle est renseignée pour les ordres DELETE et UPDATE (elle n’est pas renseignée pour INSERT).
Pour utiliser OLD il faut utiliser la syntaxe ":OLD.[nom de la colonne]" dans le trigger.
NEW représentant la valeur après modification. Elle est renseignée pour INSERT et UPDATE (elle n’est pas renseignée pour DELETE).
Pour utiliser NEW il faut utiliser la syntaxe ":NEW.[nom de la colonne]" dans le trigger.

La syntaxe est:


CREATE OR REPLACE TRIGGER [Nom Trigger] 
    BEFORE INSERT OR UPDATE OR DELETE 
    ON [Nom Table] 
    FOR EACH ROW  
Begin 
    If INSERTING Then 
        (…) 
    End if; 
    If UPDATING Then 
        (…) 
    End if; 
    If DELETING Then 
        (…) 
    End if; 
    -- OLD s'utilise comme si elle contenait une ligne modifiée: ":OLD.colonne1" par exemple 
    -- NEW s'utilise comme si elle contenait une ligne modifiée: ":NEW.colonne1" par exemple 
End;

Cursor

Permet d’effectuer une requête dans une function ou une procédure stockée:

CREATE OR REPLACE Function FindCourse (name_in IN varchar2)  
    RETURN number  
IS  
    cnumber number;  
    CURSOR c1 IS  
        SELECT course_number from courses_tbl where course_name = name_in;  
BEGIN  
    open c1;  
    fetch c1 into cnumber;  
    
    if c1%notfound then  
        cnumber := 9999;  
    end if;  
    close c1;  
RETURN cnumber;  
END;

On peut ramener une valeur et tester la valeur ramenée:
%FOUND: TRUE si une valeur est ramenée et FALSE si aucune valeur n’est ramenée.
%NOTFOUND: FALSE si une valeur est ramenée et FALSE si aucune valeur n’est ramenée.
%ISOPEN: permet de tester si le curseur est ouvert.
%ROWCOUNT: permet d’indiquer le nombre de lignes qui sont ramenées.

Cursor avec plusieurs lignes:
Si on doit effectuer des traitements sur toutes les lignes ramenées par le cursor:

open cTest;    
loop      
    fetch cTest into lvText;      
    exit when cTest%notfound;      
    dbms_output.put_line(lvText);    
end loop;    
close cTest;

Boucles dans une procédure stockée

IF…THEN…ELSE

IF condition  
THEN {...statements...}  
ELSIF condition  
THEN {...statements...}  
ELSE {...statements...}  
END IF;

FOR

FOR Lcntr IN 1..20  
LOOP  
    LCalc := Lcntr * 31;  
END LOOP;

LOOP

LOOP  
    monthly_value := daily_value * 31;  
    EXIT WHEN monthly_value > 4000;  
END LOOP;

WHILE

WHILE monthly_value <= 4000  
LOOP  
    monthly_value := daily_value * 31;  
END LOOP;

Autres précisions

Type de données

Différences entre CHAR et NCHAR

CHAR permet de stocker une taille fixe. Si la taille de la donnée est inférieure alors Oracle rajoute des caractères vides.

NCHAR permet de stocker jusqu’à 2000 caractères mais la chaine n’est pas ralongée si elle ne fait pas 2000 caractères.

NCHAR permet de stocker des caractères sur 2000 bytes mais il permet de stocker des caractères qui sont codés sur plusieurs bytes (Globalization support comme NVARCHAR2 et NCLOB).

Différences entre CHAR et VARCHAR2

CHAR permet de stocker jusqu’à 2000 bytes alors que VARCHAR2 permet de stocker 4000 bytes (32kb en PL/SQL):
– CHAR stockage chaîne ASCII taille fixe
– VARCHAR stockage chaîne ASCII taille variable
– VARCHAR2 stockage chaîne Unicode prenant en compte les paramètres régionaux.

Regex en .NET en 5 min

Les regex permettent:

  • Vérifier la syntaxe,
  • Remplacer une partie d’une chaîne de caractères,
  • Découper une chaîne.

Une regex se définit par une suite de motifs décrivant entièrement ou en partie le contenu. Le contenu peut être décrit en définissant:

  • La position du motif
  • Le type du motif: en utilisant une syntaxe explicite ou des raccourcis.
  • Le nombre d’occurrence du motif en utilisant les quantificateurs.

Définition des motifs

Position du motif

La position est définie directement par son emplacement dans la regex par rapport aux autres.
On peut s’aider des motifs:

Symbole Signification Exemple
^ Début de ligne "^2015": la chaine commence par "2015…"
$ Fin de ligne "2015$": la chaîne se termine par "…2015"
Utilisation des motifs de début et fin de ligne

Ne pas hésiter à utiliser le motif de début et de fin de ligne, par exemple:
“2015” et “2015/12/04” peuvent être validées par la même regex: "[0-9]{4}" alors "^[0-9]{4}$" est seulement valide pour “2015”.

Type de motif

Echappement

Utiliser "\" pour "échapper" les caractères utilisés plus bas.
Par exemple:
Pour reconnaître la chaîne "Pourquoi?", on peut utiliser la regex "Pourquoi\?".

Classe de caractères

Symbole Signification Exemple
() Groupement ordonné de caractères "(aze)" chaîne contenant “aze”
| "ou" pour indiquer un groupement ou un autre "((aze)|(qsd))" chaîne contenant "aze" ou "qsd"
- Intervalle de caractères "[0-9]": un caractère de 0 à 9
"[A-Z]": un caractère de A à Z en majuscule
"[A-Za-z]": un caractère de A à Z en majuscule ou en minuscule
[ ] Ensemble de caractères possibles "[0-9]"
[^] Tout sauf l’ensemble de caractères "[^a-d]": tous les caractères sauf a, b, c ou d

Exemple de classes de caractères usuels:

regex Explication
[a-z] Caractères minuscules
[a-zA-Z] Caractères majuscules et minuscules
[\w-[0-9_]] Caractères alphabétiques sans les caractères numériques

Raccourcis ou classes abrégées

On peut utiliser des raccourcis pour simplifier la syntaxe des regex ou indiquer des motifs supplémentaires:

Raccourci Signification Equivalence
. Désigne tous les caractères
\n Nouvelle ligne
\r Retour à la ligne
\t Caractère de tabulation
\s Caractère d’espacement (espace, tabulation, saut de page etc…) [\n\r\t]
\S Caractère n’étant pas un caractère d’espacement [^\n\r\t]
\d Un chiffre [0-9]
\D Caractère n’étant pas un chiffre [^0-9]
\w Caractère alphanumérique (les caractères accentués et spéciaux sont inclus) [a-zA-Z0-9_]
\W Caractère n’étant pas un caractère alphanumérique [^a-zA-Z0-9_]

Quantificateurs

On place le motif de quantification pour indiquer les répétitions, juste après le motif définissant la classe de caractères:
"h{6}" indique que le caractères "h" doit être répété 6 fois.

Symbole Signification Exemple
+ 1 fois ou plus "(aze)+": la chaîne "aze" doit être utilisée 1 fois ou plus
? 0 ou 1 fois "(aze)?": la chaîne "aze" doit être utilisée 0 ou 1 fois
* 0 ou plusieurs fois
{x} Exactement x fois "(aze){5}": la chaîne "aze" doit être utilisée exactement 5 fois
{x,} x fois ou plus
{x,y} Entre x et y fois "(aze){3,6}": la chaîne "aze" doit être utilisée entre 3 et 6

D’autres motifs existent:
https://msdn.microsoft.com/fr-fr/library/az24scfc%28v=vs.110%29.aspx

Utilisation des expressions régulières en .NET

La classe regex en .NET se trouve dans le namespace System.Text.RegularExpressions.

Une regex se définit sous forme de chaîne de caractères (avec des guillemets). L’utilisation du caractères d’échappement "\" doit être doublé ou il faut préfixer la chaîne avec @.

Par exemple:
Pour définir la regex "^[\d]$" en .NET, il faut utiliser la chaîne "^[\\d]$" ou @"^[\d]$".

Validation d’une chaîne de caractères

La validation se fait avec "Regex.IsMatch()":

Exemple de Validation d’une adresse mail:

string mailAddress = "user@server.com"; 
Regex regex = new Regex(@"^([\w]+)@([\w]+)\.([\w]+)$");
bool isValid = regex.IsMatch(mailAddress);

Remplacement d’une chaîne de caractères

On peut remplacer une chaîne par une autre en utilisant "Regex.Replace".

Par exemple:

string stringToCorrect = "yo monsieur..."; 
Regex regex = new Regex("(lut|salut|yo)");  
string replacedString = regex.Replace(stringToCorrect, "bonjour");

Il est possible de remplacer seulement les ‘N’ premières occurrences d’une chaîne en utilisant la surcharge de la classe Regex: "Regex.Replace(stringToCorrect, "bonjour", N)".

Remplacement d’une chaîne par capture

Il est possible de définir des parties d’une chaîne, de nommer ces parties puis de les utiliser pour reconstruire la chaîne de remplacement. Pour définir une partie d’une chaine, on utilise des parenthèses de capture:

Définition des parenthèses de capture:

Les parties d’une chaîne ou “groupe” sont capturées à l’aide de parenthèses de capture: "(...)". Une succession de plusieurs parenthèses de capture définit plusieurs groupes.

Par exemple:

"(\w+)(\s)(\w+)"

La chaîne précédente comporte 3 groupes:

  • Groupe 1 définit par "(\w+)": capturant une suite de caractères,
  • Groupe 2 définit par "(\s)": capturant un caractère d’espacement,
  • Groupe 3 définit par "(\w+)": capturant une autre suite de caractères.

Ainsi on peut utiliser ces groupes en les désignant par:

  • "$1" pour le groupe 1,
  • "$2" pour le groupe 2 et
  • "$3" pour le groupe 3.

On peut donc utiliser les groupes dans une même chaine. Si on utilise la chaîne de départ "un deux", la regex de capture "(\w+)(\s)(\w+)" et la chaîne de remplacement "$3$2$1", le résultat de la substitution sera: "deux un".

Autre exemple:
Si on utilise la regex de capture "([\w\-.]+)@([\w\-.]+)" qui possède 2 groupes et la chaine d’entrée: "yop@cdiese.fr", les 2 groupes seront:

  • Groupe 1 définit par "([\w\-.]+)": "yop"
  • Groupe 2 définit par "([\w\-.]+)": "cdiese.fr".

On peut donc remplacer une adresse par un lien en exécutant:

string mailAddress = "yop@cdiese.fr";
Regex regex = new Regex(@"([\w\-.]+)@([\w\-.]+)");
string addressLink = regex.Replace(mailAddress, "$1@$2);
Autre possibilité pour nommer une sous-chaîne capturée

Dans les exemples précédents, les sous-chaînes capturées sont désignées par des index: $1, $2, $3, etc…
Il est possible de définir et de désigner les sous-chaînes plus explicitement.

Par exemple:
Dans l’exemple précédent, on avait utilisé la regex "(\w+)(\s)(\w+)". On peut redéfinir cette regex en nommant les groupes: "(?<word1>\w+)(\s)(?<word2>\w+)".

Dans cet exemple, les groupes sont donc:

  • Sous-chaîne "word1" capturée par "(?<word1>\w+)",
  • Sous-chaîne "word2" capturée par "(?<word2>\w+)".

On peut utiliser les sous-chaînes avec:

  • Sous-chaîne "word1" par "${word1}"
  • Sous-chaîne "word2" par "${word2}"

Découpage d’une chaîne de caractères

Découpage par séparateur

Le découpage se fait en utilisant "Regex.Split".

Par exemple:
Pour séparer les mots d’une phrase contenant des caractères d’espacement:

string sentence = "un deux trois quatre";
Regex regex = new Regex(@"\s+");
string[] result = regex.Split(sentence);

Le résultat sous forme de tableau contiendra: "un", "deux", "trois" et "quatre".

Découpage par regroupement

Dans le cas du découpage par regroupement, on peut utiliser "Regex.Match" ou la forme statique "Regex.Matches".
Il faut définir une regex qui utilise des "parenthèses de capture" (définies plus haut).

Par exemple:
Si on définit la regex: "(\d{3})-(\d{3}-\d{4})" permet de capturer 2 groupes:

  • Groupe 1: "(\d{3})" et
  • Groupe 2: "(\d{3}-\d{4})".

Ainsi en exécutant:

Regex regex = new Regex(@"(\d{3})-(\d{3}-\d{4})");
Match m = regex.Match("212-435-6534");
string areaCode = m.Groups[1].ToString();
string phoneNumber = m.Groups[2].ToString();

Le groupe 1 s’obtient par m.Groups[1] et le groupe 2 par m.Groups[2].

En utilisant la forme statique "Regex.Matches", on peut itérer sur plusieurs résultats trouvés dans une chaine.

Par exemple:

string input = "212-555-6666 906-932-1111 415-222-3333 425-888-9999";
MatchCollection matches = Regex.Matches(input, @"(\d{3})-(\d{3}-\d{4})");
foreach (Match match in matches)
{
    string areaCode = match.Groups[1].ToString();
    string phoneNumber = match.Groups[2].ToString();
}

Ainsi on pourra capturer successivement les 4 numéros: "212-555-6666", "906-932-1111", "415-222-3333" et "425-888-9999".

Syntaxe pour exclure un groupe de capture

Il est possible de définir une regex contenant des parenthèses de capture qui ne seront pas prises en compte lors de la capture. Pour exclure un groupe de capture, il faut utiliser la syntaxe:

(?:...)

Par exemple:
Précédemment, on avait utilisé la regex "(\d{3})-(\d{3}-\d{4})" qui contient 2 groupes:

  • Groupe 1: "(\d{3})" et
  • Groupe 2: "(\d{3}-\d{4})".

Si on exclut le 1er groupe avec la syntaxe indiquée prédédemment, la regex devient "(?:\d{3})-(\d{3}-\d{4})". Ainsi, la regex ne contient qu’un seul groupe qui est "(\d{3}-\d{4})".

Outil pour écrire des regex

Le site https://regex101.com/ permet de facilement tester des regex.

Utilisation des "Task" en 5 min

1. Quelques patterns courants

Lancer l’exécution d’une tâche

Une tâche est une opération asynchrone qui se lance de la façon suivante (à partir du framework 4.5):

Task t = Task.Run(() => { ... Code à exécuter...});

Une autre syntaxe en utilisant la "Task.Factory":

Task t = Task.Factory.StartNew(() => { ... Code à exécuter...});

Sur les versions antérieures au framework 4.5:

Task t = Task.Factory.StartNew(() => { ... Code à exécuter... });
Task.Run() et Task.Factory.StartNew() se sont pas strictement équivalents

Les 2 notations ne sont pas strictement équivalentes. La notation Task.Run() est équivalente à:

Task.Factory.StartNew(() => 
    {
        // Code de la tâche
    },
    CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default);

Plus de détails sur l’option TaskCreationOptions.DenyChildAttach dans Notion de tâche parente.

Attendre la fin de l’exécution de la tâche

Equivalent à JOIN pour les threads:

t.Wait();

Attendre la fin de l’exécution de plusieurs tâches:

Si on a une liste de tâche:

Task[] tasks = new Task[3]; 
tasks[0] = Task.Run(() => { ... Some work;});
tasks[1] = Task.Run(() => { ... Some work;}); 
tasks[2] = Task.Run(() => { ... Some work;}); 

Pour attendre la fin de l’exécution de toutes les tâches:

Task.WaitAll(tasks);

Attendre la fin de l’exécution de n’importe quelle tâche dans une liste de tâches:

Si on prends l’exemple précédent, pour attendre la fin de l’exécution de n’importe quelle tâche dans une liste de tâches, il suffit de faire:

int i = Task.WaitAny(tasks);

Le type de retour est le type générique qui définit la tâche: Task<int>.

Cette fonction permet par exemple d’effectuer un traitement à chaque fois qu’une tâche a terminé son exécution:

while (tasks.Length > 0) 
{ 
  int i = Task.WaitAny(tasks); 
  // etc... 
} 

Les appels à Wait(), WaitAll() et WaitAny() sont bloquants.

Récupérer le résultat d’une tâche

Task t = Task.Run(() => { return 42;}); 
int taskResult = t.Result; 

Effectuer un travail après l’exécution de la tâche (continuation):

On peut exécuter un bout de code à la fin de l’exécution d’une tâche. Ce bout de code s’appelle une continuation. On peut effectuer autant de continuation que l’on veut. Pour effectuer une continuation, il suffit de faire appel à la fonction ContinueWith():

Task t = Task.Run(() => { return 42;}) 
     .ContinueWith((i) => { return i.Result*2;}); 
int taskResult = t.Result; 

Dans le cas de l’exemple, "I" est un entier à cause du type générique précisé dans la définition de la tâche Task<int>.

Retour de la fonction ContinueWith

Le retour de la fonction ContinueWith est la tâche "t" qui n’est pas la même instance que la tâche renvoyée par Task.Run().
Si on fait:

Task t = Task.Run(() => { return 42;});
t.ContinueWith((i) => { return i.Result*2;}); 
int taskResult = t.Result; 

Le résultat t.Result contiendra le résultat de Task.Run() et non de ContinueWith(). Pour avoir le résultat de ContinueWith, il faut faire:

Task t = Task.Run(() => { return 42;}) 
t = t.ContinueWith((i) => { return i.Result*2;}); 
int taskResult = t.Result; 

ATTENTION: en utilisant t.Result après un ContinueWith() comme dans l’exemple précédent, l’exécution est bloquante, c’est-à-dire qu’on attend la fin de l’exécution de la tâche. Il n’est pas nécessaire d’utiliser Task.Wait()

Options TaskCreationOptions.RunContinuationsAsynchronously et TaskContinuationOptions.RunContinuationsAsynchronously

Comme indiqué plus haut, les "continuations" sont exécutées, par défaut, de façon synchrone dans le même thread qui passe la tâche initiale dans son état final. Si la tâche initiale est terminée au moment de la création de la "continuation", elle sera exécutée, par défaut, dans le thread qui a créé la "continuation".

A partir du framework 4.6, on peut indiquer qu’une "continuation" doit s’exécuter de façon asynchrone:

  • A la création de la tâche initiale: en utilisant l’option TaskCreationOptions.RunContinuationsAsynchronously, on peut indiquer que les "continuations" se déclenchant à la suite de cette tâche seront exécutées de façon asynchrone. Cette option peut être utilisée avec les méthodes de création de "Task": Task.Factory.StartNew(), les constructeurs Task() ou le constructeur TaskCompletionSource().
  • A la création de la "continuation": en utilisant TaskContinuationOptions.RunContinuationsAsynchronously, on peut indiquer que la "continuation" sera exécutée de façon asynchrone. Cette option peut être utilisée par les méthodes de création d’une continuation comme Task.ContinueWith().

Notion de tâche parente

Lorsqu’on définit une tâche dans une tâche:

  • La tâche initiale est la tâche parente et
  • La tâche dans la première tâche est la tâche enfant.

On distingue les tâches enfant détachées, c’est le type par défaut si on ne précise rien et les tâches enfant attachées.

Tâche enfant détachée

Une tâche enfant détachée se définit de cette façon:

var parent = Task.Factory.StartNew(() => {
  // Code tâche parente

  var child = Task.Factory.StartNew(() => {
    // Code tâche enfant
  });
});

Pas d’attente de la tâche enfant par défaut
Lorsque la tâche enfant est détachée, la tâche parente n’attends pas la fin de l’exécution de la tâche enfant pour passer dans un des états terminés (par exemple TaskStatus.RanToCompletion). Il faut donc utiliser Wait() pour attendre la fin de la l’exécution de la tâche enfant.

Pas de propagations des exceptions
La tâche parente ne propagent pas les éventuelles exceptions aux tâches enfant. De même, la tâche enfant ne propage pas ses éventuelles exceptions à la tâche parente. Il faut que la tâche parente utilise une “continuation” ou vérifie l’état du statut de la tâche enfant pour savoir si une exception a été lancée.

En cas d’annulation en utilisant un CancellationToken

Si on utilise le même CancellationToken entre la tâche parente et la tâche enfant:

  • Si on annule au niveau de la tâche enfant: pas de propagation vers la tâche parente.
  • Si on annule au niveau de la tâche parente: si l’exécution de la tâche enfant a déjà commencé, son exécution ne sera pas stoppé. En revanche, si l’exécution de la tâche enfant n’a pas débuté, elle ne sera pas lancée.

Tâche enfant attachée

Contrairement au type de tâche enfant par défaut, il faut rajouter l’option AttachedToParent pour qu’une tâche enfant soit attachée à la tâche parente:

var parent = Task.Factory.StartNew(() => {
  // Code tâche parente

  var child = Task.Factory.StartNew(() => {
    // Code tâche enfant
  }, TaskCreationOptions.AttachedToParent);
});

Le statut de la tâche parente dépends de ceux des tâches enfant.

Attente de la tâche enfant
Il n’est pas nécessaire d’utiliser Wait() pour attendre la fin de l’exécution de la tâche. Par défaut la tâche parente attends la fin de l’exécution de ses enfants.

Propagation des exceptions
Les exceptions qui surviennent éventuellement dans la tâche enfant sont propagées à la tâche parente et affectera le statut final de la tâche parente.

En cas d’annulation en utilisant un CancellationToken
Si on utilise le même CancellationToken entre la tâche parente et la tâche enfant attachée, en cas d’annulation dans la tâche enfant, elle sera propagée dans la tâche parente et elle affectera le résultat de la tâche parente.

Attention au démarrage de la tâche parente:

Si on utilise Task.Run(), il n’est pas possible d’utiliser des tâches enfant attachées. Les tâches enfant sont systématiquement détachées.
En revanche, Task.Factory.StartNew() permet de créer des tâches enfant attachées.

L’équivalent de:

Task.Run(() => 
    {
        // Code de la tâche
    });

est:

Task.Factory.StartNew(() => 
    {
        // Code de la tâche
    },
    CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default);

Plus de détails sur MSDN.

Utilisation de l’option de création “LongRunning”

Par défaut, le processus dans lequel est exécuté une tâche n’attends, pas la fin de son exécution pour sortir. Pour que le processus attende la fin de l’exécution d’une tâche avant de sortir, il faut utiliser l’option TaskCreationOptions.LongRunning.

TaskCreationOptions.LongRunning va aussi imposer d’exécuter la tâche un thread particulier.

Par exemple:

var cancellationTokenSource = new CancellationTokenSource();
Task.Factory.StartNew(() => { ... Code à exécuter...},
  TaskCreationOptions.LongRunning,
  cancellationTokenSource.Token);

Il est fortement conseillé d’utiliser un CancellationToken dans le cas où on utilise l’option LongRunning.

FromException(), FromCanceled() et FromResult()

A partir du framework 4.6, on peut créer directement une "Task" avec un statut particulier:

  • Si on souhaite une "Task" avec le statut TaskStatus.RanToCompletion, on peut utiliser Task.FromResult() en indiquant directement le résultat,
  • Si on souhaite une "Task" avec le statut TaskStatus.Canceled, on peut utiliser Task.FromCanceled() avec un CancellationToken,
  • et si on souhaite une "Task" avec le statut TaskStatus.Faulted, on peut utiliser Task.FromException() avec une exception.

L’intérêt de ces nouvelles méthodes est d’effectuer un traitement synchrone lorsqu’on est obligé d’utiliser d’avoir une signature de méthode asynchrone. Par exemple, si une interface impose une signature asynchrone du type:

public Task<TResult> ExecuteAsync()
{
    // ...
}

Toutefois dans l’implémentation, au lieu de lancer une exécution asynchrone, si on peut vouloir affecter directement le résultat (parce qu’on le possède déjà) et avoir une "Task" dont le résultat est déjà affectée. Dans ce cas:

  • Avant le framework 4.6, on peut utiliser la classe TaskCompletionSource,
  • A partir du framework 4.6, on peut utiliser directement les fonctions FromResult(), FromCanceled() ou FromException().

Par exemple:

public Task<int> GetResultAsync(CancellationToken cancellationToken)
{
    if (cancellationToken.IsCancellationRequested)
    {
        return Task.FromCanceled<int>(cancellationToken);
    }

    try
    {
        return Task.FromResult(GetResultSync());
    }
    catch (Exception exception)
    {
        return Task.FromException(exception);
    }
}

public int GetResultSync()
{
    // ...
}

2. Gestion des exceptions

Sans gestion explicite, les exceptions qui surviennent dans les "Task" ne seront jamais remontées. La "Task" stoppera son exécution et l’exception passera inaperçue.
Une façon simple de gérer les exceptions qui peuvent survenir dans une "Task" est d’utiliser une "continuation":

Task task =  Task.Factory.StartNew(() => DoPrintConfigPage(serial))
    .ContinueWith(tsk => {
        logger.ErrorFormat("Exception occured: {0}", tsk.Exception.ToString());
    }, TaskContinuationOptions.OnlyOnFaulted);
Attention au retour de ContinueWith

Dans l’exemple précédent, task contient l’instance de la continuation et non l’instance de la "Task" qui est lancée par Task.Factory.StartNew()

AggregateException

Dans l’exemple précédent, tsk.Exception est une instance de "AggregateException". Cette exception contient d’autres exceptions accessibles avec tsk.Exception.InnerExceptions (AggregateException.InnerExceptions).

CancellationToken

Cet objet permet de signaler à une ou plusieurs tâches d’annuler leur exécution. Mais attention, à l’intérieur de la tâche, il faut explicitement vérifier qu’une annulation est requise sinon la tâche ne s’arrêtera jamais.
Une instance de "CancellationToken" est liée à une "CancellationTokenSource":

var tokenSource = new CancellationTokenSource();
var token = tokenSource.Token;

La demande d’annulation se fait au niveau de "CancellationTokenSource":

tokenSource.Cancel();

Ainsi, on peut utiliser le "CancellationToken" au moment de lancer la tâche:

var tokenSource = new CancellationTokenSource();
var token = tokenSource.Token;
var task = Task.Factory.StartNew(() => {
  if (token.IsCancellationRequested)
  {...}
}, token);
L’utilisation de "CancellationToken" doit respecter certaines règles:
  • Si une tâche est en cours d’exécution, son statut est TaskStatus.Running. En cas d’annulation, pour que la tâche passe au statut TaskStatus.Canceled, il faut qu’une exception de type OperationCanceledException soit lancée à l’intérieur de la tâche (ne pas oublier d’inclure le token lors du lancement de l’exception: throw new OperationCanceledException(token)).
    Une façon de lancer directement cette exception est d’exécuter: token.ThrowIfCancellationRequested() (l’exception sera lancée seulement si l’annulation a été signalée).
  • Si une annulation est requise (i.e. si tokenSource.Cancel() a été exécutée), toutes les utilisations suivantes de l’instance de la "CancellationToken" vont conduire à une OperationCanceledException.

Par exemple si on implémente:

var tokenSource = new CancellationTokenSource();
var token = tokenSource.Token;
var task = Task.Factory.StartNew(() => {
  if (token.IsCancellationRequested)
  {...}
}, token);
tokenSource.Cancel();
task.Wait(token);

La ligne task.Wait(token) lancera une OperationCanceledException. Ainsi les utilisations extérieures à la tâche de token pouvant être exécutées après l’annulation doivent être entourées d’un bloc try...catch (OperationCanceledException) {}.

Utilisation d’une continuation

Si on utilise un "CancellationToken", on peut utiliser une continuation pour notifier l’annulation de la tâche:

var tokenSource = new CancellationTokenSource();
var token = tokenSource.Token;
var task = Task.Factory.StartNew(() => {
  while (true)
    {
      token.ThrowIfCancellationRequested();
       ...
    }
}, token);
var continuationTask = task.ContinueWith(tsk => {
        logger.InfoFormat("Task canceled !");
    }, TaskContinuationOptions.OnlyOnCanceled);
tokenSource.Cancel();
task.Wait();
ATTENTION: exécution de la continuation

Pour que la continuation soit exécutée, il faut que la tâche soit dans le statut TaskStatus.Canceled c’est-à-dire que la ligne token.ThrowIfCancellationRequested() soit exécutée.

Exemple d’une implémentation utilisant des continuations pour l’annulation et pour une exception:

var tokenSource = new CancellationTokenSource();
var token = tokenSource.Token;
var task = Task.Factory.StartNew(() => {
  while (true)
    {
      token.ThrowIfCancellationRequested();
       ...
    }
}, token);
var exceptionTask = task.ContinueWith(tsk => {
        logger.InfoFormat("Task canceled !");
    }, TaskContinuationOptions.OnlyOnCanceled);
var continuationTask = task.ContinueWith(tsk => {
        logger.ErrorFormat("Exception occured: {0}", tsk.Exception.ToString());
    }, TaskContinuationOptions.OnlyOnFaulted);

3. TaskScheduler

Cet objet permet de manipuler un ensemble de tâches comme le threadpool pour les threads. Ainsi on peut ajouter des tâches dans une file d’attente QueueTask(), enlever une tâche de la file d’attente TryDequeue() etc…

Task et thread principal

Il est possible de démarrer une tâche associée au contexte de synchronisation du thread principal en particulier si on veut intervenir sur l’interface graphique pour effectuer certains traitements. En effet il n’est pas possible d’interagir avec des objets définis dans le thread principal puisque le contexte de synchronisation n’est pas le même: SynchronizationContext.

Pour obtenir une tâche associé au contexte de synchronisation du thread principal:

TaskScheduler.FromCurrentSynchronizationContext();

Problème lecteur/écrivain

Depuis le framework 4.5, TaskScheduler permet de répondre au problème lecteur/écrivain avec ConcurrentExclusiveSchedulerPair.

Cette classe fournit une paire d’écrivains et de lecteurs. Les lecteurs peuvent s’exécuter de façon non exclusives, les écrivains s’exécutent de façon exclusives. Les écrivains ont la priorité par rapport aux lecteurs.

var pair = new ConcurrentExclusiveSchedulerPair(TaskScheduler.Default); 
var writers = new Task.Factory(pair.ExclusiveScheduler); 
var readers = new Task.Factory(pair.ConcurrentScheduler);

writers est un objet qui permet de créer des tâches d’écrivain et reader permet de créer des tâches de lecteurs.

4. Exécution asynchrone

Les continuations permettent d’exécuter des tâches de façon asynchrone mais il est possible d’effectuer des traitements asynchrones en utilisant la Task.Factory ().

Cette classe permet de définir un délégué qui sera exécutée à la fin de l’exécution en utilisant FromAsync().

Par exemple dans le cadre de la lecture d’un fichier:

FileInfo fi = new FileInfo(path);  
byte[] data = null;  
data = new byte[fi.Length];  
FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, data.Length, true);  

//Task returns the number of bytes read  
Task task = Task.Factory.FromAsync( fs.BeginRead, fs.EndRead, data, 0, data.Length, null); 
Appel non bloquant

Cet exemple permet de montrer comment on peut créer une tâche en utilisant FromAsync() mais il n’est pas bloquant.

Voir Exécution asynchrone avec “await” et “async” en 5 min pour avoir plus détails sur l’exécution asynchrone en utilisant async et await.

5. TaskCompletionSource

L’objet TaskCompletionSource encapsule une “task” sur laquelle il est possible d’effectuer tous les traitements possibles au même titre qu’une “task” classique. L’intérêt de cette “task” encapsulée est qu’elle ne possède pas de corps, il n’y a donc pas besoin d’une implémentation particulière pour l’utiliser et on bénéficie de tous les autres éléments d’une “task” classique comme les “continuations” ou la gestion des exceptions.

Le point d’entrée de cette “task” est l’objet TaskCompletionSource. On peut donc préciser le résultat de la “task” encapsulée ou affecter une exception en utilisant directement l’objet TaskCompletionSource. Pour rappel, lorsqu’on affecte un résultat à une “task”, son état change à TaskStatus.RanToCompletion. De même si on affecte une exception à une “task”, son état change pour passer en TaskStatus.Faulted. Enfin, annuler la “task” affecte l’état TaskStatus.Canceled. RanToCompletion, Faulted et Canceled sont des états finaux affectées lorsque la tâche est terminée.

L’intérêt de TaskCompletionSource peut être d’affecter une “continuation” à la “task” encapsulée et de maîtriser le démarrage de la continuation en affectant un état final à la “task” encapsulée en utilisant une des méthodes citées précédemment:

  • Affecter un résultat en faisant TaskCompletionSource.SetResult() pour que la “task” passe à l’état TaskStatus.RanToCompletion.
  • Affecter une exception en faisant TaskCompletionSource.SetException() pour que la “task” passe à l’état TaskStatus.Faulted.
  • Annuler la “task” en utilisant un objet CancellationToken passé en paramètre de TaskCompletionSource. La “task” passe alors, à l’état TaskStatus.Canceled.

Par exemple, si on utilise des “continuations”:

public class TaskUsage  
{  
  private readonly TaskCompletionSource<bool> taskCompletionSource = new TaskCompletionSource<bool>(); 
  private readonly Task canceledTask; 
  private readonly Task exceptionOccuredTask; 
 
  public TaskUsage() 
  {  
      this.canceledTask = this.taskCompletionSource.Task.ContinueWith( 
          tsk => Console.WriteLine("Canceled"), 
          TaskContinuationOptions.OnlyOnCanceled); 
      this.exceptionOccuredTask = this.taskCompletionSource.Task.ContinueWith( 
          tsk => Console.WriteLine("Exception occured: " + tsk.Exception.ToString()), 
          TaskContinuationOptions.OnlyOnFaulted); 
  } 
 
  public bool Wait(TimeSpan timeout) 
  { 
    bool executedWithinTimeout = this.taskCompletionSource.Task.Wait(timeout) 
   
    return executedWithinTimeout; 
  } 

  public void SetException(Exception e) 
  {
    this.taskCompletionSource.SetException(e); 
  } 
 
  public void Signal() 
  {
    this.taskCompletionSource.SetResult(true); 
  } 
}

Dans cet exemple, on affecte des “continuations” qui s’exécutent si on affecte un résultat ou si on affecte une exception.

Plus de détails sur MSDN.

FromException(), FromCanceled() et FromResult()

A partir du framework 4.6, on peut s’aider des fonctions FromException(), FromCanceled() et FromResult() qui permettent d’éviter d’utiliser la classe TaskCompletionSource.

Par exemple, l’équivalent de:

var taskCompletionSource = new TaskCompletionSource<TResult>();
taskCompletionSource.SetException(exception);
return taskCompletionSource.Task;


devient:

Task.FromException(exception)

Le résultat est directement une "Task" avec le statut TaskStatus.Faulted.
FromResult() et FromCanceled() permettent d'obtenir une "Task" avec un statut différent.

Pour plus de détails, on peut se reporter à FromException(), FromCanceled() et FromResult().

Task.CompletedTask

A partir du framework 4.6, on peut créer directement une "Task" avec le statut TaskStatus.RanToCompletion sans utiliser TaskCompletionSource en utilisant la propriété statique:

Task.CompletedTask

6. Les "closures"

Une "closure" est une fonction qui capture au moins une référence vers une variable libre (source wikipedia), par exemple:

var freeVariable = "Value from the outside";
Func<string,string> myFunc = funcVariable => 
{
  return funcVariable + freeVariable;
};

La variable freeVariable est une variable libre, elle est définie à l'extérieur du délégué et elle est passée dans le délégué par référence. Dans le cas d'une utilisation de la variable libre par un seul délégué, il n'y a pas de surprises sur sa valeur. En revanche, si on utilise la variable dans une boucle ou si on effectue des affectations à la variable libre dans plusieurs délégués, on peut avoir des surprises quant à sa valeur.

Utilisation d'une "closure" dans une boucle

Par exemple si on écrit:

List<Func<int>> actions = new List<Func<int>>();

int freeVariable = 0;
for (int freeVariable = 0; freeVariable < 5; freeVariable++)
{
    actions.Add(() => freeVariable * 2);
}

foreach (var act in actions)
{
    Console.WriteLine(act.Invoke());
}

On ne maitrise pas la valeur de la variable libre car elle est passée par référence dans le délégué. Ainsi, au moment de l'exécution du délégué, sa valeur correspondra à la dernière valeur de la variable, à savoir, 10 c'est-à-dire la dernière valeur de la référence.

Pour que la valeur soit spécifique à chaque boucle, il faut copier la variable dans une autre pour que la variable libre soit copiée par valeur:

for (int freeVariable = 0; freeVariable < 5; freeVariable++)
{
    int variableCopy = freeVariable;
    actions.Add(() => variableCopy * 2);
}

La valeur de variableCopy est spécifique à chaque itération puisque la valeur est copiée et qu'on utilise plus la référence de la variable libre.

Utilisation d'une variable dans plusieurs délégués

Par exemple si on écrit:

static void Main(string[] args)
{
  var definedDeleguate = DefineDeleguate();
  Console.WriteLine(definedDeleguate(2));
  Console.WriteLine(definedDeleguate(3));
}
 
public static Func<int,int> DefineDeleguate()
{
  var freeVariable = 1;
  Func<int, int> definedDeleguate = (funcVariable) =>
  {
    freeVariable = freeVariable + 1;
    return funcVariable + freeVariable;
  };
  return definedDeleguate;
}

Après exécution, on obtient 4 et 6. Au premier abord, on s'attendrait à 4 et 5 à cause de l'initialisation de freeVariable à 1. Cependant comme la valeur est passée au délégué par référence, la référence est gardée lors de la 2e exécution du délégué. Ainsi à la première exécution, la valeur de freeVariable est 2. Cette valeur persistera lors de la 2e exécution du délégué d'où le résultat 6.

Explication sur les "closures"

Sans rentrer dans les détails, il faut considérer une "closure" comme une classe à part entière dans laquelle se trouve la fonction correspondant au délégué et un ou plusieurs membres correspondant à une ou plusieurs variables libres.
Ainsi la vie de cette classe correspond à la vie du délégué. Sachant que la variable libre est passée par référence à la fonction, la valeur de la variable libre sera affectée durant la vie de la classe:

  • Dans une boucle, sa valeur est incrémentée pendant l'exécution de la boucle,
  • Si on exécute plusieurs fois le délégué, sa valeur sera modifiée et persistée à chaque exécution du délégué.

Les "closures" et les "tasks"

Les "tasks" utilisent des délégués et on peut être amené à utiliser des "closures". Ainsi, les mêmes problèmes peuvent se poser quant à la valeur des variables libres utilisées dans le délégué.

De la même façon, les variables libres sont passées par référence. En outre, sachant que l'exécution d'une "task" ne s'effectue rigoureusement au même moment d'une exécution à l'autre par rapport à l'enchaînement des instructions, on peut aussi avoir des surprises sur la valeur de variables libres quand elles sont utilisées dans le corps d'une "task".

Par exemple, si on écrit:

int freeVariable = 45; 
Task.Factory.StartNew( () =>
{
   freeVariable++;
}
);
// ...
Console.WriteLine(freeVariable);

On peut obtenir pour certaines exécution 45 et pour d'autres 46, suivant si la "task" a eu le temps de s'exécuter ou non.

Pourquoi implémenter "GetHashCode()" quand "Equals()" est surchargé ?

GetHashCode() permet de fournir un clé de hashage qui sera utilisée pour différencier l’objet par rapport à un autre. Par exemple, il sert pour les dictionnaires pour comparer rapidement des objets entre eux. Des objets de même type et ayant les valeurs (de propriétés par exemple) doivent avoir le même hash code.

Tous les composants .NET n’utilisent pas seulement l’opérateur d’égalité pour comparer deux objets entre eux comme les dictionnaires par exemple. Ainsi si on définit l’opérateur d’égalité, il faut aussi redéfinir GetHashCode().

Comment redéfinir GetHashCode() pour une classe ?

Une astuce consiste à utiliser le GetHashCode() des propriétés de la classe. On peut les associer en utilisant l’opérateur "^" ("ou" exclusif bit à bit):

GetHashCode(classe) = prop1.GetHashCode() ^ prop2.GetHashCode() ^ prop3.GetHashCode()… 

On peut alors redéfinir l’opérateur GetHashCode() pour les types de ces propriétés ou l’utiliser tel que pour les types primitifs.

Le type doit être le même: par exemple pour un entier et un long, il faudra "caster" le long en int.