web-dev-qa-db-fra.com

Modèle d'usine en C #: comment garantir qu'une instance d'objet ne peut être créée que par une classe d'usine?

Récemment, j'ai pensé à sécuriser une partie de mon code. Je suis curieux de savoir comment on pourrait s'assurer qu'un objet ne peut jamais être créé directement, mais uniquement via une méthode d'une classe d'usine. Disons que j'ai une classe "objet métier" et je veux m'assurer que toute instance de cette classe aura un état interne valide. Pour ce faire, je devrai effectuer une vérification avant de créer un objet, probablement dans son constructeur. Tout va bien jusqu'à ce que je décide que je veux faire de ce contrôle une partie de la logique métier. Alors, comment puis-je faire en sorte qu'un objet métier puisse être créé uniquement via une méthode de ma classe de logique métier, mais jamais directement? Le premier désir naturel d'utiliser un bon vieux mot-clé "ami" de C++ échouera avec C #. Nous avons donc besoin d'autres options ...

Essayons un exemple:

public MyBusinessObjectClass
{
    public string MyProperty { get; private set; }

    public MyBusinessObjectClass (string myProperty)
    {
        MyProperty = myProperty;
    }
}

public MyBusinessLogicClass
{
    public MyBusinessObjectClass CreateBusinessObject (string myProperty)
    {
        // Perform some check on myProperty

        if (true /* check is okay */)
            return new MyBusinessObjectClass (myProperty);

        return null;
    }
}

Tout va bien jusqu'à ce que vous vous souveniez que vous pouvez toujours créer une instance MyBusinessObjectClass directement, sans vérifier l'entrée. Je voudrais exclure complètement cette possibilité technique.

Alors, qu'en pense la communauté?

84
User

Il semble que vous souhaitiez simplement exécuter une logique métier avant de créer l'objet - alors pourquoi ne créez-vous pas simplement une méthode statique à l'intérieur de la "BusinessClass" qui effectue tout le sale travail de vérification de "myProperty" et rend le constructeur privé?

public BusinessClass
{
    public string MyProperty { get; private set; }

    private BusinessClass()
    {
    }

    private BusinessClass(string myProperty)
    {
        MyProperty = myProperty;
    }

    public static BusinessClass CreateObject(string myProperty)
    {
        // Perform some check on myProperty

        if (/* all ok */)
            return new BusinessClass(myProperty);

        return null;
    }
}

L'appeler serait assez simple:

BusinessClass objBusiness = BusinessClass.CreateObject(someProperty);
56
Ricardo Nolde

Vous pouvez rendre le constructeur privé et la fabrique un type imbriqué:

public class BusinessObject
{
    private BusinessObject(string property)
    {
    }

    public class Factory
    {
        public static BusinessObject CreateBusinessObject(string property)
        {
            return new BusinessObject(property);
        }
    }
}

Cela fonctionne car les types imbriqués ont accès aux membres privés de leurs types englobants. Je sais que c'est un peu restrictif, mais j'espère que ça aidera ...

61
Jon Skeet

Ou, si vous voulez vraiment aller de l'avant, inversez le contrôle: demandez à la classe de renvoyer l'usine et d'instrumenter l'usine avec un délégué qui peut créer la classe.

public class BusinessObject
{
  public static BusinessObjectFactory GetFactory()
  {
    return new BusinessObjectFactory (p => new BusinessObject (p));
  }

  private BusinessObject(string property)
  {
  }
}

public class BusinessObjectFactory
{
  private Func<string, BusinessObject> _ctorCaller;

  public BusinessObjectFactory (Func<string, BusinessObject> ctorCaller)
  {
    _ctorCaller = ctorCaller;
  }

  public BusinessObject CreateBusinessObject(string myProperty)
  {
    if (...)
      return _ctorCaller (myProperty);
    else
      return null;
  }
}

:)

49
Fabian Schmied

Vous pouvez rendre le constructeur de votre classe MyBusinessObjectClass interne et le déplacer ainsi que la fabrique dans leur propre assembly. Désormais, seule l'usine doit être capable de construire une instance de la classe.

15
Matt Hamilton

En dehors de ce que Jon a suggéré, vous pouvez également avoir la méthode d'usine (y compris la vérification) être une méthode statique de BusinessObject en premier lieu. Ensuite, ayez le constructeur privé, et tout le monde sera obligé d'utiliser la méthode statique.

public class BusinessObject
{
  public static Create (string myProperty)
  {
    if (...)
      return new BusinessObject (myProperty);
    else
      return null;
  }
}

Mais la vraie question est - pourquoi avez-vous cette exigence? Est-il acceptable de déplacer l'usine ou la méthode d'usine dans la classe?

7
Fabian Schmied

Une autre option (légère) consiste à créer une méthode d'usine statique dans la classe BusinessObject et à garder le constructeur privé.

public class BusinessObject
{
    public static BusinessObject NewBusinessObject(string property)
    {
        return new BusinessObject();
    }

    private BusinessObject()
    {
    }
}
4
Dan C.

Après tant d'années, cela a été demandé, et toutes les réponses que je vois vous disent malheureusement comment vous devez faire votre code au lieu de donner une réponse directe. La vraie réponse que vous cherchiez est d'avoir vos classes avec un constructeur privé mais un instanciateur public, ce qui signifie que vous ne pouvez créer que de nouvelles instances à partir d'autres instances existantes ... qui ne sont disponibles qu'en usine:

L'interface pour vos cours:

public interface FactoryObject
{
    FactoryObject Instantiate();
}

Ta classe:

public class YourClass : FactoryObject
{
    static YourClass()
    {
        Factory.RegisterType(new YourClass());
    }

    private YourClass() {}

    FactoryObject FactoryObject.Instantiate()
    {
        return new YourClass();
    }
}

Et, enfin, l'usine:

public static class Factory
{
    private static List<FactoryObject> knownObjects = new List<FactoryObject>();

    public static void RegisterType(FactoryObject obj)
    {
        knownObjects.Add(obj);
    }

    public static T Instantiate<T>() where T : FactoryObject
    {
        var knownObject = knownObjects.Where(x => x.GetType() == typeof(T));
        return (T)knownObject.Instantiate();
    }
}

Vous pouvez ensuite facilement modifier ce code si vous avez besoin de paramètres supplémentaires pour l'instanciation ou pour prétraiter les instances que vous créez. Et ce code vous permettra de forcer l'instanciation à travers l'usine car le constructeur de classe est privé.

4
Alberto Alonso
    public class HandlerFactory: Handler
    {
        public IHandler GetHandler()
        {
            return base.CreateMe();
        }
    }

    public interface IHandler
    {
        void DoWork();
    }

    public class Handler : IHandler
    {
        public void DoWork()
        {
            Console.WriteLine("hander doing work");
        }

        protected IHandler CreateMe()
        {
            return new Handler();
        }

        protected Handler(){}
    }

    public static void Main(string[] args)
    {
        // Handler handler = new Handler();         - this will error out!
        var factory = new HandlerFactory();
        var handler = factory.GetHandler();

        handler.DoWork();           // this works!
    }
2
lin

En cas de bonne séparation entre les interfaces et les implémentations, le
le modèle protected-constructor-public-initializer permet une solution très soignée.

Étant donné un objet métier:

public interface IBusinessObject { }

class BusinessObject : IBusinessObject
{
    public static IBusinessObject New() 
    {
        return new BusinessObject();
    }

    protected BusinessObject() 
    { ... }
}

et une usine commerciale:

public interface IBusinessFactory { }

class BusinessFactory : IBusinessFactory
{
    public static IBusinessFactory New() 
    {
        return new BusinessFactory();
    }

    protected BusinessFactory() 
    { ... }
}

le changement suivant en BusinessObject.New() initializer donne la solution:

class BusinessObject : IBusinessObject
{
    public static IBusinessObject New(BusinessFactory factory) 
    { ... }

    ...
}

Ici, une référence à une usine commerciale concrète est nécessaire pour appeler l'initialiseur BusinessObject.New(). Mais le seul qui a la référence requise est l'usine commerciale elle-même.

Nous avons obtenu ce que nous voulions: le seul qui peut créer BusinessObject est BusinessFactory.

2
Reuven Bass

Donc, il semble que ce que je veux ne peut pas être fait de manière "pure". C'est toujours une sorte de "rappel" à la classe logique.

Peut-être que je pourrais le faire d'une manière simple, juste faire une méthode de contructor dans la classe d'objet appeler d'abord la classe logique pour vérifier l'entrée?

public MyBusinessObjectClass
{
    public string MyProperty { get; private set; }

    private MyBusinessObjectClass (string myProperty)
    {
        MyProperty  = myProperty;
    }

    pubilc static MyBusinessObjectClass CreateInstance (string myProperty)
    {
        if (MyBusinessLogicClass.ValidateBusinessObject (myProperty)) return new MyBusinessObjectClass (myProperty);

        return null;
    }
}

public MyBusinessLogicClass
{
    public static bool ValidateBusinessObject (string myProperty)
    {
        // Perform some check on myProperty

        return CheckResult;
    }
}

De cette façon, l'objet métier ne peut pas être créé directement et la méthode de vérification publique dans la logique métier ne fera pas de mal non plus.

2
User

Cette solution est basée sur munificents idée d'utiliser un token dans le constructeur. Fait dans cette réponse assurez-vous que l'objet créé uniquement par l'usine (C #)

  public class BusinessObject
    {
        public BusinessObject(object instantiator)
        {
            if (instantiator.GetType() != typeof(Factory))
                throw new ArgumentException("Instantiator class must be Factory");
        }

    }

    public class Factory
    {
        public BusinessObject CreateBusinessObject()
        {
            return new BusinessObject(this);
        }
    }
1
Lindhard

De multiples approches avec différents compromis ont été mentionnées.

  • L'imbrication de la classe d'usine dans la classe privée permet uniquement à l'usine de construire 1 classe. À ce stade, il vaut mieux utiliser une méthode Create et un ctor privé.
  • L'utilisation de l'héritage et d'un ctor protégé a le même problème.

Je voudrais proposer l'usine comme une classe partielle qui contient des classes imbriquées privées avec des constructeurs publics. Vous cachez à 100% l'objet que votre usine construit et n'exposez que ce que vous choisissez à travers une ou plusieurs interfaces.

Le cas d'utilisation que j'ai entendu à ce sujet serait lorsque vous souhaitez suivre 100% des instances en usine. Cette conception ne garantit à personne, mais l'usine a accès à la création d'instances de "produits chimiques" définis dans l '"usine" et elle supprime la nécessité d'un assemblage séparé pour y parvenir.

== ChemicalFactory.cs ==
partial class ChemicalFactory {
    private  ChemicalFactory() {}

    public interface IChemical {
        int AtomicNumber { get; }
    }

    public static IChemical CreateOxygen() {
        return new Oxygen();
    }
}


== Oxygen.cs ==
partial class ChemicalFactory {
    private class Oxygen : IChemical {
        public Oxygen() {
            AtomicNumber = 8;
        }
        public int AtomicNumber { get; }
    }
}



== Program.cs ==
class Program {
    static void Main(string[] args) {
        var ox = ChemicalFactory.CreateOxygen();
        Console.WriteLine(ox.AtomicNumber);
    }
}
1
ubershmekel

Je mettrais l'usine dans le même assemblage que la classe de domaine et marquerais le constructeur de la classe de domaine interne. De cette façon, n'importe quelle classe de votre domaine peut créer une instance, mais vous vous fiez pas à vous, non? Quiconque écrit du code en dehors de la couche de domaine devra utiliser votre usine.

public class Person
{
  internal Person()
  {
  }
}

public class PersonFactory
{
  public Person Create()
  {
    return new Person();
  }  
}

Cependant, je dois remettre en question votre approche :-)

Je pense que si vous voulez que votre classe Person soit valide lors de sa création, vous devez mettre le code dans le constructeur.

public class Person
{
  public Person(string firstName, string lastName)
  {
    FirstName = firstName;
    LastName = lastName;
    Validate();
  }
}
1
Peter Morris

Je ne comprends pas pourquoi vous voulez séparer la "logique métier" de "l'objet métier". Cela ressemble à une distorsion de l'orientation de l'objet, et vous finirez par vous nouer en prenant cette approche.

0
Jim Arnold

Je ne pense pas qu'il y ait une solution qui n'est pas pire que le problème, tout ce qu'il a besoin ci-dessus nécessite une usine statique publique, à mon humble avis est un problème pire et n'empêchera pas les gens d'appeler l'usine pour utiliser votre objet - cela ne cache rien. Il est préférable d'exposer une interface et/ou de conserver le constructeur comme interne si vous le pouvez, c'est la meilleure protection car l'assembly est un code de confiance.

Une option consiste à avoir un constructeur statique qui enregistre une usine quelque part avec quelque chose comme un conteneur IOC.

0
user1496062

Voici une autre solution dans la veine de "juste parce que vous pouvez ne signifie pas que vous devriez" ...

Il répond aux exigences de confidentialité du constructeur d'objet métier et de placement de la logique d'usine dans une autre classe. Après cela, cela devient un peu sommaire.

La classe d'usine possède une méthode statique pour créer des objets métier. Il dérive de la classe d'objets métier afin d'accéder à une méthode de construction protégée statique qui invoque le constructeur privé.

La fabrique est abstraite, vous ne pouvez donc pas en créer une instance (car ce serait également un objet métier, ce qui serait bizarre), et elle a un constructeur privé, le code client ne peut donc pas en dériver.

Ce qui n'est pas empêché, c'est le code client également dérivant de la classe d'objets métier et appelant la méthode de construction statique protégée (mais non validée). Ou pire, en appelant le constructeur par défaut protégé que nous avons dû ajouter pour obtenir la classe d'usine à compiler en premier lieu. (Qui est d'ailleurs susceptible d'être un problème avec tout modèle qui sépare la classe d'usine de la classe d'objet métier.)

Je n'essaye pas de suggérer à quelqu'un de sensé de faire quelque chose comme ça, mais c'était un exercice intéressant. FWIW, ma solution préférée serait d'utiliser un constructeur interne et la limite de l'Assemblée comme garde.

using System;

public class MyBusinessObjectClass
{
    public string MyProperty { get; private set; }

    private MyBusinessObjectClass(string myProperty)
    {
        MyProperty = myProperty;
    }

    // Need accesible default constructor, or else MyBusinessObjectFactory declaration will generate:
    // error CS0122: 'MyBusinessObjectClass.MyBusinessObjectClass(string)' is inaccessible due to its protection level
    protected MyBusinessObjectClass()
    {
    }

    protected static MyBusinessObjectClass Construct(string myProperty)
    {
        return new MyBusinessObjectClass(myProperty);
    }
}

public abstract class MyBusinessObjectFactory : MyBusinessObjectClass
{
    public static MyBusinessObjectClass CreateBusinessObject(string myProperty)
    {
        // Perform some check on myProperty

        if (true /* check is okay */)
            return Construct(myProperty);

        return null;
    }

    private MyBusinessObjectFactory()
    {
    }
}
0
yoyo

J'apprécierais entendre quelques réflexions sur cette solution. Le seul capable de créer "MyClassPrivilegeKey" est l'usine. et 'MyClass' l'exige dans le constructeur. Évitant ainsi la réflexion sur les entrepreneurs privés/"inscription" à l'usine.

public static class Runnable
{
    public static void Run()
    {
        MyClass myClass = MyClassPrivilegeKey.MyClassFactory.GetInstance();
    }
}

public abstract class MyClass
{
    public MyClass(MyClassPrivilegeKey key) { }
}

public class MyClassA : MyClass
{
    public MyClassA(MyClassPrivilegeKey key) : base(key) { }
}

public class MyClassB : MyClass
{
    public MyClassB(MyClassPrivilegeKey key) : base(key) { }
}


public class MyClassPrivilegeKey
{
    private MyClassPrivilegeKey()
    {
    }

    public static class MyClassFactory
    {
        private static MyClassPrivilegeKey key = new MyClassPrivilegeKey();

        public static MyClass GetInstance()
        {
            if (/* some things == */true)
            {
                return new MyClassA(key);
            }
            else
            {
                return new MyClassB(key);
            }
        }
    }
}
0
Ronen Glants