Antoine Sauvinet — Technology Executive & Entrepreneur
FR | EN
Blog CV Contact

[EnhanceYourCode] : the Factory Method Pattern

Dead factory - Alexander Kaiser - Ce fichier est sous licence Creative Commons Attribution 2.0 Generic.

Le Factory Method pattern est, à mon avis, l’un des design patterns de création les plus utiles.

Il nous permet de déléguer la création d’un objet à une classe dédiée, et ainsi d’encapsuler toute la logique de cette création qui n’est souvent pas directement liée à la responsabilité de la classe.

1) Exemple simple : Considérons le code suivant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
using System;

namespace Oinant.Blog.Factory.SimpleExemple
{
    public class MyBusinessObject
    {
        private string _content;
        private DateTime _dateTimeReleventToBusinessLogic;
        public MyBusinessObject(Tuple<DateTime, String> creationData)
        {
            _content = creationData.Item2;
            _dateTimeReleventToBusinessLogic = creationData.Item1;
        }

        public bool IsBusinessRequirementMet()
        {
            return !string.IsNullOrEmpty(_content);
        }

        public void PerformBusinessLogic()
        {
            
        }

        public string GetContent()
        {
            return _content;
        }
    }

    class MyBusinessObjectFactory
    {
        private readonly IBusinessRepository _businessRepository;

        public MyBusinessObjectFactory(IBusinessRepository businessRepository)
        {
            _businessRepository = businessRepository;
        }

        public MyBusinessObject Create(Guid id)
        {
            var content = _businessRepository.GetContent(id);
            return new MyBusinessObject(new Tuple<DateTime, string>(DateTime.Now, content));
        }
    }

    public class Client
    {
        
        private void Run()
        {
            var factory = new MyBusinessObjectFactory(new DummyBusinessRepository());

            var myRunnerId = new Guid();

            MyBusinessObject myObject = factory.Create(myRunnerId);

            if(myObject.IsBusinessRequirementMet())
                myObject.PerformBusinessLogic();
        }
        
    }

    public interface IBusinessRepository
    {
        string GetContent(Guid id);
    }

    public class DummyBusinessRepository : IBusinessRepository
    {
        public string GetContent(Guid id)
        {
            return id.ToString();
        }
    }
}

La première classe est un véritable objet métier : elle est construite à partir de certaines propriétés, possède une logique métier. Sa responsabilité unique est d’exécuter la logique métier. Elle ne devrait pas embarquer de préoccupations infrastructurelles (comme une connexion à la base de données par exemple).

La deuxième classe est la factory elle-même, qui gère le processus complet de création de notre BusinessObject. Sa responsabilité inclut de récupérer les données d’un repository et d’appeler le constructeur de la classe métier.

La troisième classe est un simple runner, qui instancie la factory, obtient l’objet métier de celle-ci, et exécute ce que le programme est censé faire. Grâce au Factory Method Pattern, le Runner n’est pas pollué par la logique de construction, tout est délégué à l’objet factory.

2) Améliorer la testabilité du système : Évidemment, en séparant les responsabilités de votre code en différents modules, vous améliorez, de facto, la testabilité de votre programme. Mais dans certains cas, vous pouvez utiliser le factory method pattern comme moyen d’utiliser un TestDouble là où vos techniques habituelles de mocking ne peuvent pas s’appliquer. Par exemple, quand votre code consomme un objet externe, et que cet objet n’a pas d’interface, comme ce NetworkMessage.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
using System;

namespace ExternalLibrary
{
    public class NetworkMessage
    {
        static readonly TimeSpan TimeToLive = TimeSpan.FromSeconds(10);

        private readonly DateTime _creation;
        private Status _status;
        private readonly object _content;

        public NetworkMessage(Status status, object content)
        {
            _creation = DateTime.UtcNow;
            _status = status;
            _content = content;
        }

        public bool IsSuccess()
        {
            if (HasTimedOut())
                _status = Status.Failed;
            return _status == Status.Succeeded;
        }

        private bool HasTimedOut()
        {
            return (_creation.Add(TimeToLive)) < DateTime.UtcNow;
        }

        public T GetContentAs<T>() where T : class
        {
            var castedContent = _content as T;
            if (castedContent == null)
                throw new InvalidCastException("contet couldn't be casted into " + typeof(T));
            return _content as T;
        }
    }
    
    public enum Status
    {
        Pending,
        Succeeded,
        Failed
    }
    
    public class NetworkService
    {
        public Tuple<int, object> SendMessage()
        {
            // for ske of simplicity, some static data...
            return new Tuple<int, object>(1, new object());
        }
    }
}

Comme vous pouvez le voir, la classe principale repose sur DateTime.Now, ce qui rend le code qui en dépend assez compliqué à tester.

Voici notre propre NetworkClient, sans factory pour le NetworkMessage :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
using ExternalLibrary;

namespace Oinant.Blog.Factory.ForTesting
{

    public class NetworkClientWithoutMessageFactory
    {
        public string SendRequest()
        {
            var message = new NetworkService().SendMessage();
            var networkMessage = new NetworkMessage((Status)message.Item1, message.Item2);

            return networkMessage.IsSuccess().ToString() + " " + networkMessage.GetContentAs<string>();
        }
    }
}

Ici, pour pouvoir tester chaque chemin d’exécution de notre client, nous devons savoir précisément comment la bibliothèque externe se comporte en interne, et même utiliser des Microsoft.Fakes pour créer des shims de System.DateTime. C’est beaucoup de code de test qui n’est pas vraiment pertinent, car c’est hors de notre périmètre. De plus, nous voulons juste contrôler la sortie du NetworkMessage pour nous assurer que notre client réseau se comporte correctement. Pour y parvenir, nous pouvons ajouter une factory à notre client :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
using ExternalLibrary;

namespace Oinant.Blog.Factory.ForTesting
{
    public class NetworkClientWithMessageFactory
    {
        private readonly INetworkMessageFactory _networkMessageFactory;

        public NetworkClientWithMessageFactory(INetworkMessageFactory networkMessageFactory)
        {
            _networkMessageFactory = networkMessageFactory;
        }

        public string SendRequest()
        {
            var message = new NetworkService().SendMessage();
            var networkMessage = _networkMessageFactory.Create((Status)message.Item1, message.Item2);

            return networkMessage.IsSuccess().ToString() + " " + networkMessage.GetContentAs<string>();
        }
    }

    public interface INetworkMessageFactory
    {
        NetworkMessage Create(Status status, object content);
    }

    public class ConcreteNetworkMessageFactory : INetworkMessageFactory
    {
        public NetworkMessage Create(Status status, object content)
        {
            return new NetworkMessage(status, content);
        }
    }
}

Et tester devient vraiment facile, en implémentant une autre factory, qui crée un objet qui hérite de notre objet externe :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
using ExternalLibrary;
using Oinant.Blog.Factory.ForTesting;

namespace Factory.Tests
{
    public class TestFactory : INetworkMessageFactory
    {
        public NetworkMessage Create(Status status, object content)
        {
            var message = new NetworkMessageDouble(status, content);
            return message;
        }
    }

    class NetworkMessageDouble : NetworkMessage
    {
        private readonly Status _status;
        private readonly object _content;

        public NetworkMessageDouble(Status status, object content) : base(status, content)
        {
            _status = status;
            _content = content;
        }

        public new bool IsSuccess()
        {
            return _status == Status.Succeeded;
        }

        public T GetContentAs<T>() where T : class
        {
            return  _content as T;
        }
    }
}

Dans cet article, nous avons vu que le Factory Method pattern nous aide à respecter le Single Responsibility Principle de SOLID, et peut nous aider avec l’intégration de code externe/legacy, en nous fournissant un moyen d’améliorer la testabilité.

Note : tout le code de cet article est accessible sur ce repository github

Commentaires

💬 Les commentaires sont partagés entre toutes les versions linguistiques de cet article.