Design pattern: Façade

Objectif:

Simplifie l’interface d’une ou plusieurs classes

Justification

Problème

Lorsqu’une interface doit être consommée par une classe cliente, il est courant de vouloir simplifier cette interface:
– pour cacher la complexité de l’implémentation interne et présenter une interface simple à utiliser,
– simplifier l’appel à beaucoup d’objets internes en ne proposant qu’une interface unique,
– limiter les dépendances des classes clientes en évitant d’exposer trop d’objets internes. Ces objets internes étant invisibles de l’extérieur, on est sûr qu’ils sont utilisés uniquement en interne. On est alors plus libre de les modifier pour des éventuelles évolutions futures sans casser les dépendances des classes clientes extérieures.

Par exemple
On réalise un système d’émission de billets qui utilise d’autres systèmes de billeterie hétérogènes:
– un système de réservation de billets d’avion accessible au moyen d’une API propriétaire,
– un système de réservation de billets de train interrogeable avec un web service sécurisé,

Les demandes d’émissions peuvent être lancées au moyen d’une interface graphique ou d’un web service. Pour lancer les émissions, on ne communique que les références de dossier. C’est au système de récupérer toutes les données nécessaires pour effectuer les émissions.

Sachant que le système peut être interrogée au moins de 2 façons: interface graphique et web service, on voudrait avoir une interface unique pour lancer l’émission:

public interface ITicketIssuingSystem
{
    TicketIssue IssueTicket(string ticketReference);
    bool IsTicketAlreadyIssued(string ticketReference);
}

public class TicketIssue
{
    public bool HasBeenIssued { get; set; }
    public string OccuredError { get; set; }
}

L’interface de l’objet interne permettant de s’interfacer avec le système de réservation de billets d’avion est:

public interface IAirlineTicketBookingSystem
{
    ...
    ITicketData GetTicket(string ticketReference);
}

L’interface de l’objet interne permettant de s’interfacer avec le système de réservation de billets de train est:

public interface ITrainTicketBookingSystem
{
    ...
    ITicketData GetTicket(string ticketReference);
}

Pour interroger la base de données, on utilise la classe "TicketRepository" qui effectue la requête sur la table "IssuedTicket":

internal class IssuedTicketRepository
{
    ...
    public bool AddNewIssuedTicket(string ticketReference)
    {...}

    public bool ContainsTicket(string ticketReference)
    {
        using (var context = new TicketDBEntities())
        {
            return context.IssuedTickets.Any(t => t.Reference.Equals(ticketReference));
        };
    }
}

Pour effectuer chaque émission, le système doit:
– interroger les deux autres systèmes de réservation pour récupérer les données relatives au billet,
– interroger une base de données pour vérifier que l’émission du billet n’a pas déjà été demandée, de façon à ne pas émettre 2 fois le même billet,
– logger des informations relatives à la requête,
– envoyer des mails aux passagers pour indiquer l’émission du ou des billets.

Solution

"Façade" permet d’apporter une solution:
– en proposant une interface unique aux classes clientes,
– en évitant d’exposer les autres objets internes aux classes clientes et
– en utilisant les objets internes pour effectuer l’émission et renvoyer les résultats.

La "Façade" sera alors l’unique objet appelé par l’interface graphique et par le web service et c’est elle qui va interroger tous les autres objets pour effectuer l’émission:

public class TicketIssueFacade : ITicketIssuingSystem
{
    private IAirlineTicketBookingSystem _airlineSystem;
    private ITrainTicketBookingSystem _trainSystem;
    private IssuedTicketRepository _issuedTicketRepository;
    private ILog _logger;

    public TicketIssueFacade(IAirlineTicketBookingSystem airlineSystem, 
        ITrainTicketBookingSystem trainSystem, IssuedTicketRepository _issuedTicketRepository,
        ILog logger)
    {
        this._airlineSystem = airlineSystem;
        this._trainSystem = trainSystem;
        this._issuedTicketRepository = issuedTicketRepository;
        this._logger = logger;
    }

    public TicketIssue IssueTicket(string ticketReference)
    {
        if (this.IsTicketAlreadyIssued(ticketReference))
            return new TicketIssue
            {
                HasBeenIssued = false,
                OccuredError = "Ticket has been alreadu issued."
            };

        var ticket = this._airlineSystem.GetTicket(ticketReference);
        if (ticket == null)
        {
            ticket = this._trainSystem.GetTicket(ticketReference);
        }

        if (ticket == null)
            return new TicketIssue
            {
                HasBeenIssued = false,
                OccuredError = "Ticket data not found."
            };

        bool ticketIssued = false;
        string occuredError = string.Empty;
        if (this._issuedTicketRepository.AddNewIssuedTicket(ticketReference))
        {
            ticketIssued = true;
            this._logger.InfoFormat("Ticket {0} has been issued.", ticketReference);
        }
        else
        {
            occuredError = "Ticket not issued for an unknown reason."
            this._logger.InfoFormat("Ticket {0} not issued.", ticketReference);
        }

        return new TicketIssue
        {
            HasBeenIssued = ticketIssued,
            OccuredError = occuredError,
        };
    }

    public bool IsTicketAlreadyIssued(string ticketReference)
    {
        return this._issuedTicketRepository.ContainsTicket(ticketReference);
    }
}

TicketIssueFacade devient alors la seule classe accessible par des classes clientes permettant d’émettre des billets.

Pour aller plus loin…

Diagramme théorique

Limites

Le plus gros inconvénient à "Façade" est qu’une classe façade peut rapidement devenir une classe "fourre-tout" où on aura tendance à placer tout le code. La raison principale est que c’est la classe qui met en relation d’autres classes sous-jacentes. Le risque est d’avoir une façade contenant beaucoup de code métier. Elle va donc perdre son objectif de simplifier des interfaces internes au profit d’une classe mettant tous les objets internes en relation. Cette tendance sera renforcée si la façade est consommée par d’autres objets internes.
"Façade" ne peut donc suffire seul à organiser l’implémentation, il faut y ajouter une rigueur et garder en tête que la façade sert à simplifier une implémentation ou des interfaces internes et doit être consommée par des objets externes.

Différences entre "Adapter" et "Façade"

"Adapter" et "Façade" sont très semblables, ils visent tous deux à adapter une complexité à une classe cliente.
Toutefois, "Adapter" s’utilise dans un contexte plus précis de l’adaptation ou de la conversion d’une ou plusieurs classes pour un besoin particulier.
"Façade" s’utilisera davantage pour cacher la complexité d’une fonctionnalité plus générale c’est-à-dire simplifier une implémentation ou plus généralement une interface, en particulier lorsque plusieurs classes internes doivent être appelées.

"Façade" simplifie les interfaces d’une ou plusieurs classes alors que "Adapter" convertit des interfaces pré-existantes.

Design pattern: Adapter

Objectif:

Convertir l’interface d’une ou plusieurs classes pour qu’elle soit adaptée à un ou plusieurs clients.

Justifications

Problèmes

Le besoin de présenter différemment un objet à une autre classe qui le consomme peut se justifier par plusieurs raisons:
– On veut présenter un objet plus adapté aux besoins de la classe cliente, de façon à volontairement éviter de présenter trop de fonctions, trop de membres ou des signatures trop complexes. On cible alors plus précisemment les besoins de la classe cliente et on évite de maintenir des fonctions ou membres non consommés.
– Les besoins des classes clientes peuvent nécessiter l’utilisation de plusieurs objets sous-jacents. Pour éviter des appels à tous ces objets, on peut vouloir aggréger les appels dans un seul objet.
– On souhaite limiter l’exposition d’objets internes.
– Présenter un objet plus adapté peut aussi signifier qu’une conversion de données est nécessaire entre le ou les objets consommés et la classe cliente. Cette conversion peut être unique ou spécifique à chaque classe cliente.

Par exemple:
On développe une API d’application lourde. Différents éléments sur cette application lourde permettent d’ajouter un bouton: le menu MenuHandler, la barre de raccourci ShortcutBar et d’une barre d’accès rapide EasyAccessToolbar.
Dans une première implémentation, ces éléments dérivent de l’objet ButtonContainer qui satisfait l’interface IButtonContainer. Seule cette interface est publique et est accessible dans l’API:

public interface IButtonContainer
{
   void AddButton(string buttonName, string caption, Action callback);
   void RemoveButton(string buttonName);
}

internal class ButtonContainer : IButtonContainer
{
   public virtual void  AddButton(string buttonName, string caption, Action callback)
   { ... }

   public virtual void RemoveButton(string buttonName)
   { ... }
}

internal class MenuHandler : ButtonContainer 
{
   public override void  AddButton(string buttonName, string caption, Action callback)
   { ... }

   public override void RemoveButton(string buttonName)
   { ... }
}

internal class ShortcutBar : ButtonContainer 
{
}

internal class EasyAccessToolbar : ButtonContainer 
{
}

Le client de l’API peut accéder aux objets de cette façon:

public interface IMainWindow
{
    IButtonContainer GetButtonContainer(ButtonContainerType containerType);
}

public class MainWindow : IMainWindow
{
    private MenuHandler _menuHandler;
    private ShortcutBar _shortcutBar;
    private EasyAccessToolbar _easyAccessToolbar;

    public MainWindow
    {
        this._menuHandler = new MenuHandler();
        this._shortcutBar = new ShortcutBar();
        this._easyAccessToolbar = new EasyAccessToolbar();
    }

    public IButtonContainer GetButtonContainer(ButtonContainerType containerType)
    {
        switch (containerType)
        {
            case ButtonContainerType.Menu:
                return this._menuHandler;
                break;
            case ButtonContainerType.Shortcut:
                return this._shortcutBar;
                break;
            case ButtonContainerType.EasyAccessToolbar:
                return this._easyAccessToolbar;
                break;
            default:
                throw new NotSupportedException();
        }
    }
}

public enum ButtonContainerType
{
    Menu,
    Shortcut,
    EasyAccessToolbar,
}

Deux demandes d’évolution imposent quelques changements:
– devoir afficher des menus déroulants dans le composant EasyAccessToolbar. On ne peut donc plus dériver de ButtonContainer.
– devoir ajouter un "ribbon" qui contient aussi des boutons. Ce ribbon provient d’un autre éditeur et impossible aussi de le faire dériver de ButtonContainer ou de le faire satisfaire l’interface IButtonContainer.

Enfin on doit pouvoir assurer la compatiblité ascendante (compatiblité par rapport aux anciennes versions) et on ne peut pas casser IMainWindow et IButtonContainer.

Solution

Le pattern "Adapter" permet de résoudre ce problème en permettant:
– D’adapter tous les composants pour qu’ils soient visibles de l’extérieur sous forme de IButtonContainer et ainsi encapsuler la complexité du polymorphisme dans une interface unique.
– Ne pas modifier d’interfaces et ainsi assurer la compatiblité ascendante.
– Organiser l’architecture sans trop casser l’existant.

On introduit donc un intermédiaire qui sera un adaptateur entre les objets contenant des boutons (MenuHandler, ShortcutBar, EasyAccessToolbar et Ribbon) et une classe cliente.
Les "adapters" satisferont IButtonContainer et appeleront directement les objets sous-jacents.

On peut proposer l’implémentation suivante en définissant un "adapter" abstrait:

internal abstract class ButtonContainerAdapter : IButtonContainer
{
    public abstract void AddButton(string buttonName, string caption, Action callback);
    public abstract void RemoveButton(string buttonName);
}

On implémente les différents "adapters":

internal class EasyAccessToolbarAdapter : ButtonContainerAdapter
{
   private EasyAccessToolbar _easyAccessToolbar;

   public EasyAccessToolbarAdapter(EasyAccessToolbar easyAccessToolbar)
   {
       this._easyAccessToolbar = easyAccessToolbar;
   }

   public override void AddButton(string buttonName, string caption, Action callback)
   {
       this._easyAccessToolbar.AddButton(buttonName, caption, callback);
   }

   public override void RemoveButton(string buttonName)
   {
       this._easyAccessToolbar.RemoveButton(buttonName);
   }
}

internal class RibbonAdapter : ButtonContainerAdapter
{
   private Ribbon _ribbon;

   public RibbonAdapter(Ribbon ribbon)
   {
       this._ribbon = ribbon;
   }

   public override void AddButton(string buttonName, string caption, Action callback)
   { ... }

   public override void RemoveButton(string buttonName)
   { ... }
}

Enfin on modifie MainWindow:

public class MainWindow : IMainWindow
{
    private MenuHandler _menuHandler;
    private ShortcutBar _shortcutBar;
    private EasyAccessToolbar _easyAccessToolbar;
    private Ribbon _ribbon;

    public MainWindow
    {
        this._menuHandler = new MenuHandler();
        this._shortcutBar = new ShortcutBar();
        this._easyAccessToolbar = new EasyAccessToolbar();
        this._ribbon = new Ribbon();
    }

    public IButtonContainer GetButtonContainer(ButtonContainerType containerType)
    {
        switch (containerType)
        {
            case ButtonContainerType.Menu:
                return this._menuHandler;
                break;
            case ButtonContainerType.Shortcut:
                return this._shortcutBar;
                break;
            case ButtonContainerType.EasyAccessToolbar:
                return new EasyAccessToolbarAdapter(this._easyAccessToolbar);
                break;
            case ButtonContainerType.Ribbon:
                return new RibbonAdapter(this._ribbon);
                break;
            default:
                throw new NotSupportedException();
        }
    }
}

public enum ButtonContainerType
{
    Menu,
    Shortcut,
    EasyAccessToolbar,
}

Pour aller plus loin…

Diagramme théorique

La variante présentée ci-dessus se base sur l’exemple présenté plus haut:
– Un "adapter" par classe adaptée,
– Les "adapters" dérivent d’un "adapter" abstrait.

Seulement avec une interface et sans classe abstraite:

Autres variantes

Comme tous les patterns, il n’y a pas de définition définitive ou absolue d’"Adapter", d’autres variantes sont possibles suivant le contexte:
– Au lieu d’utiliser une classe abstraite "Adapter", on peut utiliser seulement une interface. Et donc tous les "Adapters" doivent simplement satisfaire l’interface.
– Il peut y avoir un seul "adapter" pour plusieurs objets adaptés:

– Il peut aussi y avoir plusieurs "adapters" pour un seul objet adapté suivant la classe cliente qui le consomme.

Différences entre "Adapter" et "Façade"

"Adapter" et "Façade" sont très semblables, ils visent tous deux à adapter une complexité à une classe cliente.
Toutefois, "Adapter" s’utilise dans un contexte plus précis de l’adaptation ou de la conversion d’une ou plusieurs classes pour un besoin particulier.
"Façade" s’utilisera davantage pour cacher la complexité d’une fonctionnalité plus générale c’est-à-dire simplifier une implémentation ou plus généralement une interface, en particulier lorsque plusieurs classes internes doivent être appelées.

En définitive, "Façade" simplifie les interfaces d’une ou plusieurs classes alors que "Adapter" convertit des interfaces pré-existantes.

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.