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

93

Récemment, j'ai pensé à sécuriser une partie de mon code. Je suis curieux de savoir comment on peut 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 que je veux m'assurer que toute instance de cette classe aura un état interne valide. Pour ce faire, je devrai effectuer quelques vérifications avant de créer un objet, probablement dans son constructeur. Tout va bien jusqu'à ce que je décide que je souhaite que cette vérification fasse 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é?

Utilisateur
la source
La méthode MyBoClass est-elle mal orthographiée?
pradeeptp
2
Je suis confus, pourquoi ne voudriez-vous PAS que vos objets métier soient responsables de la protection de leurs propres invariants? Le but du modèle d'usine (si je comprends bien) est soit A) de gérer la création d'une classe complexe avec de nombreuses dépendances ou B) de laisser l'usine décider quel type d'exécution retourner, en exposant uniquement une interface / un résumé classe au client. Mettre vos règles métier dans vos objets métier est l'ESSENCE même de la POO.
sara
@kai True, l'objet métier est responsable de la protection de son invariant, mais l'appelant du constructeur doit s'assurer que les arguments sont valides avant de les transmettre au constructeur ! Le but de la CreateBusinessObjectméthode est de valider les arguments AND to (retourner un nouvel objet valide OU renvoyer une erreur) dans un seul appel de méthode lorsqu'un appelant ne sait pas immédiatement si les arguments sont valides pour passer dans le constructeur.
Lightman
2
Je ne suis pas d'accord. Le constructeur est responsable de faire toutes les vérifications requises pour les paramètres donnés. Si j'appelle un constructeur et qu'aucune exception n'est levée, j'espère que l'objet créé est dans un état valide. Il ne devrait pas être physiquement possible d'instancier une classe en utilisant les «mauvais» arguments. Créer une Distanceinstance avec un argument négatif devrait lancer un ArgumentOutOfRangeExceptionpar exemple, vous ne gagneriez rien à créer un DistanceFactoryqui effectue la même vérification. Je ne vois toujours pas ce que vous avez à gagner ici.
sara

Réponses:

55

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 fait tout le travail de vérification sale 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);
Ricardo Nolde
la source
6
Ah, vous pouvez également lancer une exception au lieu de renvoyer null.
Ricardo Nolde
5
il ne sert à rien d'avoir un constructeur par défaut privé si vous en avez déclaré un personnalisé. aussi, dans ce cas, il n'y a littéralement rien à gagner à utiliser un "constructeur" statique au lieu de simplement faire la validation dans le constructeur réel (puisqu'il fait de toute façon partie de la classe). c'est encore pire, puisque vous pouvez maintenant obtenir une valeur de retour nulle, s'ouvrant pour les NRE et "qu'est-ce que ce bordel signifie?" des questions.
sara
Un bon moyen d'exiger cette architecture via une classe de base Interface / abstraite? c'est-à-dire une interface / classe de base abstraite qui dicte que les implémenteurs ne doivent pas exposer un ctor.
Arash Motamedi
1
@Arash Non. Les interfaces ne peuvent pas définir de constructeurs, et une classe abstraite qui définit un constructeur protégé ou interne ne peut pas empêcher l'héritage de classes de l'exposer via un constructeur public qui lui est propre.
Ricardo Nolde
66

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 cela aidera ...

Jon Skeet
la source
J'ai pensé à cela, mais cela déplace efficacement ces vérifications dans l'objet métier lui-même, ce que j'essaie d'éviter.
Utilisateur le
@ so-tester: vous pouvez toujours avoir les chèques en usine plutôt que le type BusinessObject.
Jon Skeet
3
@JonSkeet Je sais que cette question est vraiment ancienne, mais je suis curieux de savoir quel est l'avantage de placer la CreateBusinessObjectméthode à l'intérieur d'une Factoryclasse imbriquée au lieu d'avoir cette méthode statique comme une méthode directement de la BusinessObjectclasse ... pouvez-vous vous rappeler votre motivation pour le faire?
Kiley Naro le
3
@KileyNaro: Eh bien, c'est ce que la question demandait :) Cela ne confère pas nécessairement beaucoup d'avantages, mais cela répond à la question ... il peut y avoir des moments où cela est utile - le modèle du constructeur vient à l'esprit. (Dans ce cas, le générateur serait la classe imbriquée, et il aurait une méthode d' instance appelée Build.)
Jon Skeet
53

Ou, si vous voulez être vraiment sophistiqué, 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;
  }
}

:)

Fabian Schmied
la source
Très beau morceau de code. La logique de création d'objet est dans une classe distincte et l'objet ne peut être créé qu'en utilisant la fabrique.
Nikolai Samteladze
Cool. Existe-t-il un moyen de limiter la création de BusinessObjectFactory à BusinessObject.GetFactory statique?
Felix Keil
1
Quel est l'avantage de cette inversion?
yatskovsky du
1
@yatskovsky C'est juste un moyen de s'assurer que l'objet ne peut être instancié que de manière contrôlée tout en étant capable de factoriser la logique de création dans une classe dédiée.
Fabian Schmied le
15

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 la fabrique devrait être capable de construire une instance de la classe.

Matt Hamilton
la source
7

En dehors de ce que Jon a suggéré, vous pouvez également faire en sorte que la méthode de fabrique (y compris la vérification) soit une méthode statique de BusinessObject en premier lieu. Ensuite, faites en sorte que le constructeur soit 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?

Fabian Schmied
la source
Ce n'est guère une exigence. Je veux juste avoir une séparation nette entre l'objet métier et la logique. Tout comme il est inapproprié d'avoir ces vérifications dans les pages code-behind, je juge inapproprié d'avoir ces vérifications dans les objets eux-mêmes. Eh bien, peut-être une validation de base, mais pas vraiment des règles métier.
Utilisateur du
Si vous souhaitez séparer la logique et la structure d'une classe uniquement pour des raisons de commodité, vous pouvez toujours utiliser une classe partielle et avoir les deux dans des fichiers séparés ...
Grx70
7

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 recherchiez est d'avoir vos classes avec un constructeur privé mais un instanciateur public, ce qui signifie que vous ne pouvez créer de nouvelles instances qu'à partir d'autres instances existantes ... qui ne sont disponibles que dans l'usine:

L'interface de 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();
    }
}

Ensuite, vous pouvez 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 la fabrique car le constructeur de classe est privé.

Alberto Alonso
la source
4

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

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

    private BusinessObject()
    {
    }
}
Dan C.
la source
2

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, il suffit de faire une méthode de contructor dans la classe d'objet d'abord appeler 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 aucun mal non plus.

Utilisateur
la source
2

Dans un cas de bonne séparation entre les interfaces et les implémentations, 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() 
    { ... }
}

la modification suivante de l' BusinessObject.New()initialiseur 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' BusinessObject.New()initialiseur. Mais le seul qui dispose de la référence requise est l'usine commerciale elle-même.

Nous avons obtenu ce que nous voulions: le seul qui puisse créer l' BusinessObjectest BusinessFactory.

Basse Reuven
la source
Vous vous assurez donc que le seul constructeur public fonctionnel de l'objet nécessite une Factory, et que le paramètre Factory nécessaire ne peut être instancié qu'en appelant la méthode statique de Factory? Cela semble être une solution de travail, mais j'ai le sentiment que des extraits comme public static IBusinessObject New(BusinessFactory factory) cela soulèveront de nombreux sourcils si quelqu'un d'autre maintient le code.
Flater le
@Flater D'accord. Je changerais le nom du paramètre factoryen asFriend. Dans ma base de code, il apparaîtrait alors commepublic static IBusinessObject New(BusinessFactory asFriend)
Reuven Bass
Je ne comprends pas du tout. Comment ça marche? L'usine ne peut pas créer de nouvelles instances d'IBusinessObject et n'a-t-elle aucune intention (méthodes) de le faire ...?
lapsus le
2
    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!
    }
lin
la source
Veuillez essayer mon approche, la «fabrique» est le conteneur du gestionnaire, et elle seule peut créer une instance pour elle-même. avec quelques modifications, il peut répondre à vos besoins.
lin
1
Dans votre exemple, ce qui suit ne serait-il pas possible (ce qui n'aurait pas de sens) ?: factory.DoWork ();
Whyser
1

Je mettrais l'usine dans le même assemblage que la classe de domaine et marquerais le constructeur de la classe de domaine comme interne. De cette façon, n'importe quelle classe de votre domaine peut être en mesure de créer une instance, mais vous ne vous faites pas confiance, n'est-ce pas? 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 la création, vous devez mettre le code dans le constructeur.

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

Cette solution est basée sur l' idée courante d'utiliser un jeton 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);
        }
    }
Lindhard
la source
1

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

  • L'imbrication de la classe de fabrique dans la classe de construction privée permet uniquement à la fabrique de construire 1 classe. À ce stade, vous êtes mieux avec une Createméthode et un ctor privé.
  • L'utilisation de l'héritage et d'un ctor protégé pose le même problème.

Je voudrais proposer la fabrique comme une classe partielle qui contient des classes imbriquées privées avec des constructeurs publics. Vous cachez à 100% l'objet que votre usine est en train de construire et n'exposez que ce que vous choisissez via une ou plusieurs interfaces.

Le cas d'utilisation que j'ai entendu pour cela serait lorsque vous souhaitez suivre 100% des instances dans l'usine. Cette conception ne garantit à personne d'autre que l'usine d'avoir accès à la création d'instances de «produits chimiques» définis dans «l'usine» et élimine le besoin 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);
    }
}
ubershmekel
la source
0

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 adoptant cette approche.

Jim Arnold
la source
Je ne comprends pas non plus cette distinction. Quelqu'un peut-il expliquer pourquoi cela est fait et quel code serait dans l'objet métier?
pipTheGeek
C'est juste une approche qui pourrait être utilisée. Je veux que l'objet métier soit principalement des structures de données mais pas avec tout le champ ouvert en lecture / écriture. Mais la question était vraiment de pouvoir instancier un objet uniquement à l'aide d'une méthode de fabrique et non directement.
Utilisateur
@Jim: Je suis personnellement d'accord avec votre point de vue, mais professionnellement, cela se fait constamment. Mon projet actuel est un service Web qui prend une chaîne HTML, la nettoie selon certaines règles prédéfinies, puis renvoie la chaîne nettoyée. Je suis obligé de passer par 7 couches de mon propre code "car tous nos projets doivent être construits à partir du même modèle de projet". Il va de soi que la plupart des gens qui posent ces questions sur SO n'ont pas la liberté de changer tout le flux de leur code.
Flater le
0

Je ne pense pas qu'il existe une solution qui ne soit pas pire que le problème, tout ce qu'il a ci-dessus nécessite une usine statique publique, ce qui, à 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 du code de confiance.

Une option est d'avoir un constructeur statique qui enregistre une fabrique quelque part avec quelque chose comme un conteneur IOC.

user1496062
la source
0

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

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

La classe de fabrique a une méthode statique pour créer des objets métier. Il dérive de la classe d'objet métier afin d'accéder à une méthode de construction protégée statique qui appelle 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 serait donc bizarre), et elle a un constructeur privé afin que le code client ne puisse pas en dériver.

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

Je n'essaie pas de suggérer à quiconque sain d'esprit 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 d'assemblage 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()
    {
    }
}
yo-yo
la source
0

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 / «enregistrement» à 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);
            }
        }
    }
}
Ronen Glants
la source