Matthieu Mezil
L’objet, c’est beau
Le développement orienté objet peut être apparenté, osons le mot, à une forme d’art. Cependant, avant de devenir un « artiste » de l’objet, il y a plusieurs notions à maîtriser. A travers cet article, nous allons en aborder quelques-unes.
Par Matthieu Mezil publié le 27/04/2009 à 07:10, lu 1504 fois, 10 pages
 6 | Le design pattern Abstract Factory
Nous allons compléter l’exercice en introduisant deux nouvelles demandes fonctionnelles :
  • C’est l’utilisateur qui doit choisir les différentes combinaisons de traitements qu’il souhaite effectuer
  • Il faut permettre l’ajout de nouveaux traitements sans toucher à l’assembly FileTransformBLL.dll
Pour cela, nous allons fortement modifier notre architecture.
Notons au passage que cela illustre bien pourquoi il est souhaitable de connaître l’ensemble des besoins fonctionnels avant de concevoir l’architecture logicielle.
Pour cela, il y a deux façons de faire :
  • Permettre l’instanciation des classes de traitement ainsi que de la classe CompositeStringsTransform (constructeurs publiques) directement par l’UI
  • Utiliser une Factory
Le point intéressant avec l’instanciation directe (le premier point) est sa facilité. Il suffit de rendre publique les constructeurs et ça marche. Cependant, il présente un inconvénient majeur : dans un découpage « optimal », l’UI n’a pas à connaître le fonctionnement de la BLL. Par conséquent, l’UI n’a pas à savoir que nous utilisons le design pattern Composite. Lui cacher, c’est aussi s’assurer de pouvoir changer de fonctionnement sans impact. Aussi, nous éliminerons l’instanciation directe au profit du design pattern Factory.
En quoi ce pattern consiste-t-il ?
Ce pattern consiste tout simplement à créer une méthode (souvent statique) qui va faire l’instanciation pour nous.
Il faut bien garder en tête la deuxième nouvelle demande fonctionnelle : certains traitements peuvent être définis dans des assemblies inconnus de notre BLL. C’est pourquoi plutôt qu’une Factory classique, nous opterons pour une Abstract Factory.

public interface IStringsTransformInstanciator

{

    IStringsTransform CreateInstance();

}

 

public class InvertCharsPerStringTransformInstanciator : IStringsTransformInstanciator

{

    internal InvertCharsPerStringTransformInstanciator()

    {

    }

 

    public IStringsTransform CreateInstance()

    {

        return new InvertCharsPerStringTransform();

    }

 

    public override string ToString()

    {

        return TransformResources.InvertCharsPerStringTransform;

    }

}

 

public class NumberPerStringTransformInstanciator : IStringsTransformInstanciator

{

    internal NumberPerStringTransformInstanciator()

    {

    }

 

    public IStringsTransform CreateInstance()

    {

        return new NumberPerStringTransform();

    }

 

    public override string ToString()

    {

        return TransformResources.NumberPerStringTransform;

    }

}

 

internal class CompositeStringsTransform : IStringsTransform

{

    private List<IStringsTransformInstanciator> _transforms = new List<IStringsTransformInstanciator>();

 

    public CompositeStringsTransform(IEnumerable<IStringsTransformInstanciator> transforms)

    {

        _transforms.AddRange(transforms);

    }

    public CompositeStringsTransform(params IStringsTransform[] transforms)

        : this((IEnumerable<IStringsTransformInstanciator>)transforms)

    {

    }

 

    public List<IStringsTransformInstanciator> Transforms

    {

        get { return _transforms; }

    }

 

    public IEnumerable<string> Do(IEnumerable<string> lines)

    {

        foreach (var transform in Transforms)

            lines = transform.CreateInstance().Do(lines);

        return lines;

    }

 

    public override string ToString()

    {

        var sb = new StringBuilder();

        foreach (var transform in Transforms)

        {

            sb.Append(" & ");

            sb.Append(transform);

        }

        return sb.ToString().Substring(3);

    }

}

 

public static class FileTransform

{

    private static Dictionary<string, BackgroundWorker> _transformsRunning =

        new Dictionary<string, BackgroundWorker>();

    private static object _lockObject = new object();

 

    public static void Transform(string inFilePath, string outFilePath,

        IEnumerable<IStringsTransformInstanciator> transforms, Action<string> showError,

        Func<string, bool> showErrorWithRetry, Action<bool> transformDone)

    {

        if (!(File.Exists(inFilePath) || _transformsRunning.ContainsKey(inFilePath)))

            FileNotFoundShowError(inFilePath, showError);

        else if (!Directory.Exists(Path.GetDirectoryName(outFilePath)))

            DirectoryNotFoundShowError(outFilePath, showError);

        else

        {

            var bg = new BackgroundWorker();

            bg.DoWork += (s, e) =>

            {

                e.Result = false;

                while (true)

                {

                    try

                    {

                        FileUtil.WriteLines(outFilePath,

                            new CompositeStringsTransform(transforms).Do(FileUtil.GetLines(inFilePath)));

                        e.Result = true;

                    }

                    catch (FileNotFoundException)

                    {

                        FileNotFoundShowError(inFilePath, showError);

                    }

                    catch (DirectoryNotFoundException)

                    {

                        DirectoryNotFoundShowError(outFilePath, showError);

                    }

                    catch (IOException)

                    {

                        if (showErrorWithRetry(string.Format(ErrorResources.IOException, outFilePath)))

                            continue;

                    }

                    catch (Exception ex)

                    {

                        showError(string.Format(ErrorResources.FileNotFoundException,

                            string.Concat(ex.GetType(), " : ", ex.Message)));

                    }

                    break;

                }

                lock (_lockObject)

                {

                    _transformsRunning.Remove(outFilePath);

                }

            };

            bg.RunWorkerCompleted += (s, e) =>

            {

                if (transformDone != null)

                    transformDone((bool)e.Result);

            };

 

            StartBGAndCache(inFilePath, outFilePath, bg);

        }

    }

 

    private static void StartBGAndCache(string inFilePath, string outFilePath, BackgroundWorker bg)

    {

        lock (_lockObject)

        {

            RunWorkerCompletedEventHandler runWorkerCompletedTryAgain = null;

            Action<BackgroundWorker> tryAgain = (bgTryAgain) =>

            {

                StartBGAndCache(inFilePath, outFilePath, bg);

                bgTryAgain.RunWorkerCompleted -= runWorkerCompletedTryAgain;

            };

            if (_transformsRunning.ContainsKey(inFilePath))

            {

                var bgTryAgain = _transformsRunning[inFilePath];

                runWorkerCompletedTryAgain = (s, e) => tryAgain(bgTryAgain);

                bgTryAgain.RunWorkerCompleted += runWorkerCompletedTryAgain;

            }

            else if (_transformsRunning.ContainsKey(outFilePath))

            {

                var bgTryAgain = _transformsRunning[outFilePath];

                runWorkerCompletedTryAgain = (s, e) => tryAgain(bgTryAgain);

                bgTryAgain.RunWorkerCompleted += runWorkerCompletedTryAgain;

            }

            else

            {

                _transformsRunning.Add(outFilePath, bg);

                bg.RunWorkerAsync();

            }

        }

    }

 

    private static void DirectoryNotFoundShowError(string directoryPath, Action<string> showError)

    {

        showError(string.Format(ErrorResources.DirectoryNotFoundException, Path.GetDirectoryName(directoryPath)));

    }

 

    private static void FileNotFoundShowError(string filePath, Action<string> showError)

    {

        showError(string.Format(ErrorResources.FileNotFoundException, filePath));

    }

}

Je souhaiterais revenir sur la méthode StartBGAndCache et tout particulièrement sur ce code :

RunWorkerCompletedEventHandler runWorkerCompletedTryAgain = null;

Action<BackgroundWorker> tryAgain = (bgTryAgain) =>

    {

        StartBGAndCache(fileTransformInfo, bg, transformDone);

        bgTryAgain.RunWorkerCompleted -= runWorkerCompletedTryAgain;

    };

L’idée est de se désabonner de l’évènement BackgroundWorker.RunWorkerCompleted pour éviter de s’abonner plusieurs fois, le tout en factorisant le code entre le _transformsRunning.ContainsKey(inFilePath) et le _transformsRunning.ContainsKey(outFilePath).
Bien sûr on aurait pu utiliser une nouvelle méthode qui aurait pris en paramètre le RunWorkerCompletedEventHandler et le BackgroundWorker. Cependant, autant profiter de cet article pour proposer des choses moins « conventionnelles ».
Comment ça marche ?
La question peut effectivement être posée car runWorkerCompletedTryAgain est égal à null. Oui mais… quand l’appel au délégué tryAgain sera effectif, runWorkerCompletedTryAgain sera initialisé.
Dans le même genre, voici un exemple plus simple à comprendre : il est possible de réaliser un calcul factoriel de la façon suivante :

Func<int, int> fact = null;

fact = n => n == 0 ? 1 : n * fact(n - 1);

Pourquoi cette affectation à null ?
Car si elle est omise, le compilateur génère une erreur car il y a utilisation d’une variable non initialisé.
Revenons sur notre code. Notre propriété Transforms.TransformsAllowed ne retournera désormais plus des IStringsTransform mais des IStringsTransformInstanciator.

public static class Transforms

{

    private static List<IStringsTransformInstanciator> _transforms;

    public static List<IStringsTransformInstanciator> TransformsAllowed

    {

        get

        {

            if (_transforms == null)

                _transforms = new List<IStringsTransformInstanciator>()

                {

                    new NumberPerStringTransformInstanciator(),

                    new InvertCharsPerStringTransformInstanciator()

                };

            return _transforms;

        }

    }

}

Maintenant, il suffit aux autres assemblies de créer deux classes par traitements : une qui implémente l’interface IStringsTransformInstanciator et la classe de traitement « réelle » qui implémente IStringsTransform et d’ajouter les traitements dans Transforms.
Cependant, il reste un « problème » : c’est l’UI qui va devoir connaître l’ensemble des classes exposant des collections de traitements afin de les ajouter à la liste de la classe transforms.
Ce qui serait mieux, c’est que cette liste soit construite dynamiquement. En plus, cela permettrait de ne pas avoir à modifier l’exécutable. Pour faire cela, nous pourrions utiliser la Reflection pour récupérer dans toute une liste d’assemblies toutes les classes instanciant l’interface IStringsTransformInstanciator mais autant utiliser une technologie faite pour ça : MEF.
 
» Démarrer une discussion
 
Discussion démarée par killscores le 28/04/2009 à 17:06, 3 commentaire(s).