Motif d’usine en C #: Comment s’assurer qu’une instance d’object ne peut être créée que par une classe de fabrique?

Récemment, j’ai pensé à sécuriser une partie de mon code. Je suis curieux de savoir comment on peut s’assurer qu’un object ne peut jamais être créé directement, mais uniquement par une méthode d’une classe d’usine. Disons que j’ai une classe “object métier” et je veux m’assurer que toute instance de cette classe aura un état interne valide. Pour y parvenir, je devrai effectuer quelques vérifications avant de créer un object, probablement dans son constructeur. Tout est correct jusqu’à ce que je décide de faire de cette vérification une partie de la logique métier. Alors, comment puis-je faire en sorte qu’un object métier ne soit créable que par une méthode quelconque 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 ++ ne correspondra pas à C #. Nous avons donc besoin d’autres options …

Essayons un exemple:

public MyBusinessObjectClass { public ssortingng MyProperty { get; private set; } public MyBusinessObjectClass (ssortingng myProperty) { MyProperty = myProperty; } } public MyBusinessLogicClass { public MyBusinessObjectClass CreateBusinessObject (ssortingng myProperty) { // Perform some check on myProperty if (true /* check is okay */) return new MyBusinessObjectClass (myProperty); return null; } } 

Tout est correct jusqu’à ce que vous vous rappeliez que vous pouvez toujours créer l’instance MyBusinessObjectClass directement, sans vérifier l’entrée. Je voudrais exclure complètement cette possibilité technique.

Alors, que pense la communauté de cela?

On dirait que vous voulez simplement exécuter une logique métier avant de créer l’object – alors pourquoi ne créez-vous pas simplement une méthode statique à l’intérieur de la classe BusinessClass qui vérifie tous les travaux et rend le constructeur privé?

 public BusinessClass { public ssortingng MyProperty { get; private set; } private BusinessClass() { } private BusinessClass(ssortingng myProperty) { MyProperty = myProperty; } public static BusinessClass CreateObject(ssortingng 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); 

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

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

Cela fonctionne car les types nesteds ont access aux membres privés de leurs types fermants. Je sais que c’est un peu ressortingctif, mais j’espère que ça aidera …

Ou, si vous voulez aller très loin, inversez le contrôle: demandez à la classe de renvoyer la fabrique et d’instruisez l’usine avec un délégué capable de créer la classe.

 public class BusinessObject { public static BusinessObjectFactory GetFactory() { return new BusinessObjectFactory (p => new BusinessObject (p)); } private BusinessObject(ssortingng property) { } } public class BusinessObjectFactory { private Func _ctorCaller; public BusinessObjectFactory (Func ctorCaller) { _ctorCaller = ctorCaller; } public BusinessObject CreateBusinessObject(ssortingng myProperty) { if (...) return _ctorCaller (myProperty); else return null; } } 

🙂

Vous pouvez rendre le constructeur de votre classe MyBusinessObjectClass interne et le déplacer ainsi que la fabrique dans leur propre assembly. Maintenant, seule la fabrique doit pouvoir construire une instance de la classe.

Mis à part ce que Jon a suggéré, vous pouvez également faire en sorte que la méthode factory (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 que tous les autres utilisateurs soient obligés d’utiliser la méthode statique.

 public class BusinessObject { public static Create (ssortingng 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?

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(ssortingng property) { return new BusinessObject(); } private BusinessObject() { } } 

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 de logique.

Peut-être que je pourrais le faire d’une manière simple, il suffit de faire une méthode de construction dans la classe d’objects d’abord appeler la classe logique pour vérifier l’entrée?

 public MyBusinessObjectClass { public ssortingng MyProperty { get; private set; } private MyBusinessObjectClass (ssortingng myProperty) { MyProperty = myProperty; } pubilc static MyBusinessObjectClass CreateInstance (ssortingng myProperty) { if (MyBusinessLogicClass.ValidateBusinessObject (myProperty)) return new MyBusinessObjectClass (myProperty); return null; } } public MyBusinessLogicClass { public static bool ValidateBusinessObject (ssortingng myProperty) { // Perform some check on myProperty return CheckResult; } } 

De cette façon, l’object métier n’est pas directement créable et la méthode de vérification publique dans la logique métier ne fera pas de mal non plus.

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 object 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 apscope à BusinessObject.New() initialiseur BusinessObject.New() donne la solution:

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

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

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

  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(ssortingng[] args) { // Handler handler = new Handler(); - this will error out! var factory = new HandlerFactory(); var handler = factory.GetHandler(); handler.DoWork(); // this works! } 

Après tant d’années, cela a été demandé, et toutes les réponses que je vois vous disent malheureusement comment vous devriez faire votre code au lieu de donner une réponse claire. La réponse réelle 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 que de nouvelles instances à partir d’autres instances existantes … qui ne sont disponibles que dans la fabrique:

L’interface pour vos classes:

 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 knownObjects = new List(); public static void RegisterType(FactoryObject obj) { knownObjects.Add(obj); } public static T Instantiate() 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 parameters supplémentaires pour l’instanciation ou pour prétraiter les instances que vous créez. Et ce code vous permettra de forcer l’instanciation via la fabrique, car le constructeur de la classe est privé.

Je mettrais la fabrique dans le même assemblage que la classe de domaine et marquerais le constructeur interne de la classe de domaine. De cette façon, toute classe de votre domaine peut créer une instance, mais vous ne vous y fiez pas, n’est-ce pas? Toute personne qui écrit du code en dehors de la couche domaine doit 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 placer le code dans le constructeur.

 public class Person { public Person(ssortingng firstName, ssortingng lastName) { FirstName = firstName; LastName = lastName; Validate(); } } 

Cette solution est basée sur l’idée de munificents d’utiliser un jeton dans le constructeur. Fait dans cette réponse, assurez-vous que l’object est uniquement créé 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); } } 

Plusieurs approches avec des compromis différents ont été mentionnées.

  • L’imbrication de la classe de fabrique dans la classe construite de manière privée permet uniquement à la fabrique de construire 1 classe. À ce stade, il est préférable d’utiliser une méthode Create et un moteur privé.
  • L’utilisation de l’inheritance et d’un moteur protégé pose le même problème.

Je voudrais proposer la fabrique comme une classe partielle contenant des classes nestedes privées avec des constructeurs publics. Vous masquez à 100% l’object que votre usine est en train de construire et en exposant uniquement ce que vous avez choisi via 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 access à la création d’instances de «produits chimiques» définies dans «l’usine» et évite de recourir à un assemblage distinct 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(ssortingng[] args) { var ox = ChemicalFactory.CreateOxygen(); Console.WriteLine(ox.AtomicNumber); } } 

Je ne comprends pas pourquoi vous voulez séparer la “logique métier” de l’object métier. Cela ressemble à une distorsion de l’orientation de l’object et vous finirez par vous attacher en adoptant cette approche.

Je ne pense pas qu’il existe une solution qui ne soit pas pire que le problème, tout ce qu’il a besoin ci-dessus d’une usine statique publique dont IMHO est un problème pire et n’empêche pas les gens d’appeler l’usine à utiliser votre object – il ne cache rien. Mieux vaut exposer une interface et / ou garder le constructeur comme interne si vous le pouvez, ce qui est la meilleure protection car l’assembly est du code de confiance.

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

Voici une autre solution dans la veine de “juste parce que vous le pouvez ne signifie pas que vous devriez” …

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

La classe de fabrique a une méthode statique pour créer des objects métier. Il dérive de la classe d’object 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 (car ce serait aussi un object métier, donc ce serait bizarre), et elle a un constructeur privé, donc le code client ne peut pas en dériver.

Ce qui n’est pas empêché, c’est que le code client dérive également de la classe d’object métier et appelle la méthode de construction statique protégée (mais non validée). Ou pire, appeler le constructeur par défaut protégé que nous devions append pour que la classe de fabrique se comstack en premier lieu. (Ce qui, par ailleurs, risque de poser problème avec un modèle qui sépare la classe de fabrique de la classe d’object métier.)

Je n’essaie pas de suggérer à qui que ce soit dans leur 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 ssortingng MyProperty { get; private set; } private MyBusinessObjectClass(ssortingng myProperty) { MyProperty = myProperty; } // Need accesible default constructor, or else MyBusinessObjectFactory declaration will generate: // error CS0122: 'MyBusinessObjectClass.MyBusinessObjectClass(ssortingng)' is inaccessible due to its protection level protected MyBusinessObjectClass() { } protected static MyBusinessObjectClass Construct(ssortingng myProperty) { return new MyBusinessObjectClass(myProperty); } } public abstract class MyBusinessObjectFactory : MyBusinessObjectClass { public static MyBusinessObjectClass CreateBusinessObject(ssortingng myProperty) { // Perform some check on myProperty if (true /* check is okay */) return Construct(myProperty); return null; } private MyBusinessObjectFactory() { } }