Modello di fabbrica in C #: come garantire che un’istanza dell’object possa essere creata solo da una class factory?

Recentemente ho pensato di proteggere parte del mio codice. Sono curioso di sapere come si possa essere sicuri che un object non possa mai essere creato direttamente, ma solo tramite un metodo di una class factory. Diciamo che ho una class “object business” e voglio assicurarmi che ogni istanza di questa class abbia uno stato interno valido. Per raggiungere questo objective, dovrò eseguire alcuni controlli prima di creare un object, probabilmente nel suo costruttore. Questo va bene fino a quando deciderò di voler fare questo controllo come parte della logica del business. Quindi, come posso organizzare la creazione di un object business solo tramite un metodo nella mia class di business logic ma mai direttamente? Il primo desiderio naturale di utilizzare una buona vecchia parola chiave “amico” di C ++ non sarà sufficiente con C #. Quindi abbiamo bisogno di altre opzioni …

Proviamo un esempio:

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; } } 

Va tutto bene finché non ti ricordi che puoi ancora creare istanze MyBusinessObjectClass direttamente, senza verificare l’input. Vorrei escludere del tutto questa possibilità tecnica.

Quindi, cosa pensa la comunità di questo?

Sembra che tu voglia solo eseguire alcune logiche di business prima di creare l’object, quindi perché non creare un metodo statico all’interno di “BusinessClass” che esegue tutto il controllo sporco “myProperty” e rendere privato il costruttore?

 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; } } 

Chiamarlo sarebbe piuttosto semplice:

 BusinessClass objBusiness = BusinessClass.CreateObject(someProperty); 

Puoi rendere privato il costruttore e la fabbrica un tipo annidato:

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

Funziona perché i tipi nidificati hanno accesso ai membri privati ​​dei loro tipi di chiusura. So che è un po ‘restrittivo, ma spero che possa aiutare …

Oppure, se vuoi essere davvero fantasioso, inverti il ​​controllo: chiedi alla class di restituire la fabbrica e di strumentare la fabbrica con un delegato che possa creare la class.

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

🙂

È ansible rendere interno il costruttore della class MyBusinessObjectClass e spostarlo e il factory nel proprio assembly. Ora solo la factory dovrebbe essere in grado di build un’istanza della class.

A parte quello che Jon ha suggerito, potresti anche avere il metodo factory (incluso il check) come metodo statico di BusinessObject in primo luogo. Quindi, avere il costruttore privato e tutti gli altri saranno obbligati a utilizzare il metodo statico.

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

Ma la vera domanda è: perché hai questo requisito? È accettabile spostare la fabbrica o il metodo di fabbrica nella class?

Un’altra opzione (leggera) è quella di creare un metodo factory statico nella class BusinessObject e mantenere privato il costruttore.

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

Dopo tanti anni questo è stato chiesto e tutte le risposte che vedo purtroppo ti dicono come dovresti fare il tuo codice invece di dare una risposta diretta. La risposta effettiva che stavi cercando è avere le tue classi con un costruttore privato ma un instantiator pubblico, il che significa che puoi solo creare nuove istanze da altre istanze esistenti … che sono disponibili solo in fabbrica:

L’interfaccia per le tue classi:

 public interface FactoryObject { FactoryObject Instantiate(); } 

La tua class:

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

E, infine, la fabbrica:

 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(); } } 

Quindi è ansible modificare facilmente questo codice se sono necessari parametri aggiuntivi per l’istanziazione o per pre-elaborare le istanze create. E questo codice ti permetterà di forzare l’istanziazione attraverso la factory, dato che il costruttore della class è privato.

Quindi, sembra che quello che voglio non possa essere fatto in un modo “puro”. È sempre una sorta di “richiamo” alla class logica.

Forse potrei farlo in un modo semplice, basta creare un metodo contructor nella class dell’object per chiamare prima la class logica per verificare l’input?

 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; } } 

In questo modo, l’object business non è creabile direttamente e il metodo di controllo pubblico nella logica aziendale non farà alcun danno.

In un caso di buona separazione tra interfacce e implementazioni
Il modello di inizializzatore pubblico-costruttore protetto consente una soluzione molto accurata.

Dato un object aziendale:

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

e una fabbrica di affari:

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

la seguente modifica all’inizializzatore BusinessObject.New() fornisce la soluzione:

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

Qui è necessario un riferimento al business factory concreto per chiamare l’inizializzatore BusinessObject.New() . Ma l’unico che ha il riferimento richiesto è la stessa fabbrica di affari.

Abbiamo ottenuto ciò che volevamo: l’unico che può creare BusinessObject è 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(string[] args) { // Handler handler = new Handler(); - this will error out! var factory = new HandlerFactory(); var handler = factory.GetHandler(); handler.DoWork(); // this works! } 

Metterei la fabbrica nello stesso assembly della class di dominio e contrassegnare il costruttore della class di dominio interno. In questo modo, qualsiasi class nel tuo dominio potrebbe essere in grado di creare un’istanza, ma non ti fidi di te stesso, giusto? Chiunque scriva codice al di fuori del livello del dominio dovrà utilizzare la propria fabbrica.

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

Tuttavia, devo mettere in dubbio il tuo approccio 🙂

Penso che se vuoi che la tua class Person sia valida al momento della creazione, devi inserire il codice nel costruttore.

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

Questa soluzione si basa sull’idea di usare un token nel costruttore. Fatto in questa risposta assicurati che l’object sia stato creato solo dalla fabbrica (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); } } 

Sono stati menzionati approcci multipli con diversi compromessi.

  • Annidare la class factory nella class costruita privatamente consente solo alla factory di build 1 class. A quel punto starai meglio con un metodo Create e un ctor privato.
  • L’utilizzo dell’ereditarietà e di un ctor protetto ha lo stesso problema.

Mi piacerebbe proporre la fabbrica come una class parziale che contiene classi nidificate private con costruttori pubblici. Stai nascondendo al 100% l’object che la tua fabbrica sta costruendo e esponendo solo ciò che scegli attraverso una o più interfacce.

Il caso d’uso che ho sentito per questo sarebbe quando si desidera monitorare il 100% delle istanze in fabbrica. Questo design non garantisce nessuno, ma la fabbrica ha accesso alla creazione di istanze di “sostanze chimiche” definite nella “fabbrica” ​​e rimuove la necessità di un assemblaggio separato per raggiungere tale objective.

 == 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); } } 

Non capisco perché tu voglia separare la “logica di business” dal “business object”. Questo suona come una distorsione dell’orientamento degli oggetti, e finirai per legarti in nodes prendendo quell’approccio.

Non penso che ci sia una soluzione che non sia peggiore del problema, tutto ciò che richiede richiede una fabbrica statica pubblica che IMHO sia un problema peggiore e non impedirà alle persone di chiamare la fabbrica per usare il tuo object – non nasconde nulla. È meglio esporre un’interfaccia e / o mantenere il costruttore come interno se è ansible che sia la migliore protezione dal momento che l’assembly è un codice attendibile.

Un’opzione è avere un costruttore statico che registri una fabbrica da qualche parte con qualcosa come un container IOC.

Ecco un’altra soluzione sulla scia di “solo perché puoi non significa che dovresti” …

Soddisfa i requisiti per mantenere privato il costruttore dell’object business e inserire la logica factory in un’altra class. Dopo ciò diventa un po ‘approssimativo.

La class factory ha un metodo statico per la creazione di oggetti business. Deriva dalla class dell’object business per accedere a un metodo di costruzione protetto statico che richiama il costruttore privato.

La fabbrica è astratta quindi non è ansible creare un’istanza di essa (perché sarebbe anche un object business, quindi sarebbe strano) e ha un costruttore privato, quindi il codice client non può derivarne.

Ciò che non viene impedito è che il codice client deriva anche dalla class dell’object business e chiama il metodo di costruzione statica protetto (ma non convalidato). O peggio, chiamando il costruttore predefinito protetto che dovevamo aggiungere per ottenere la class factory da compilare in primo luogo. (Che incidentalmente potrebbe essere un problema con qualsiasi modello che separa la class factory dalla class dell’object business.)

Non sto cercando di suggerire a chiunque sia sano di mente di fare qualcosa del genere, ma è stato un esercizio interessante. FWIW, la mia soluzione preferita sarebbe utilizzare un costruttore interno e il limite dell’assembly come guardia.

 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() { } }