wake-up-neo.net

Modello di fabbrica in C #: come garantire che un'istanza di oggetto possa essere creata solo da una classe di fabbrica?

Recentemente ho pensato di proteggere parte del mio codice. Sono curioso di sapere come si possa assicurarsi che un oggetto non possa mai essere creato direttamente, ma solo tramite un metodo di una classe di fabbrica. Supponiamo che io abbia una classe "business object" e voglio assicurarmi che qualsiasi istanza di questa classe abbia uno stato interno valido. Per raggiungere questo obiettivo dovrò eseguire alcuni controlli prima di creare un oggetto, probabilmente nel suo costruttore. Va tutto bene finché non decido che voglio fare questo controllo come parte della logica aziendale. Quindi, come posso fare in modo che un oggetto business sia creabile solo attraverso un metodo nella mia classe di logica aziendale ma mai direttamente? Il primo desiderio naturale di usare una buona vecchia parola chiave "amico" di C++ non sarà all'altezza di C #. Quindi abbiamo bisogno di altre opzioni ...

Facciamo 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 l'istanza MyBusinessObjectClass direttamente, senza controllare l'input. Vorrei escludere del tutto questa possibilità tecnica.

Quindi, cosa ne pensa la community?

84
User

Sembra che tu voglia solo eseguire alcune logiche aziendali prima di creare l'oggetto, quindi perché non creare un metodo statico all'interno di "BusinessClass" che fa tutto il sporco controllo "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);
56
Ricardo Nolde

Puoi rendere privato il costruttore e factory un tipo nidificato:

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

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

Questo funziona perché i tipi nidificati hanno accesso ai membri privati ​​dei loro tipi racchiusi. So che è un po 'restrittivo, ma speriamo che possa aiutare ...

61
Jon Skeet

Oppure, se vuoi diventare davvero fantasioso, inverti il ​​controllo: chiedi alla classe di restituire la fabbrica e strumentala alla fabbrica con un delegato che può creare 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

È possibile rendere interno il costruttore sulla classe MyBusinessObjectClass e spostarlo e la fabbrica nel proprio assieme. Ora solo la factory dovrebbe essere in grado di costruire un'istanza della classe.

15
Matt Hamilton

A parte ciò che Jon ha suggerito, potresti anche avere il metodo factory (incluso il controllo) come primo metodo statico di BusinessObject. Quindi, avere il costruttore privato e tutti gli altri saranno costretti 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 factory nella classe?

7
Fabian Schmied

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

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

    private BusinessObject()
    {
    }
}
4
Dan C.

Dopo così 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 istante pubblico, il che significa che puoi creare solo 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 classe:

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

Quindi è possibile modificare facilmente questo codice se sono necessari parametri aggiuntivi per l'istanza o per preelaborare le istanze create. E questo codice ti permetterà di forzare l'istanza attraverso la factory poiché il costruttore della classe è privato.

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

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

Dato un oggetto business:

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 a BusinessObject.New() initializer fornisce la soluzione:

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

    ...
}

Qui è necessario un riferimento a una fabbrica di business concreta 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.

2
Reuven Bass

Quindi, sembra che ciò che voglio non possa essere fatto in modo "puro". È sempre una sorta di "richiamo" per la classe logica.

Forse potrei farlo in un modo semplice, basta fare un metodo contructor nella classe oggetto prima chiamare la classe logica per controllare 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'oggetto business non è direttamente creabile e il metodo di controllo pubblico nella logica aziendale non farà alcun danno.

2
User

Questa soluzione si basa sull'idea munificents dell'uso di un token nel costruttore. Fatto in questa risposta assicurarsi che l'oggetto 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);
        }
    }
1
Lindhard

Sono stati menzionati approcci multipli con diversi compromessi.

  • L'annidamento della classe factory nella classe costruita privatamente consente alla fabbrica di costruire solo 1 classe. A quel punto stai meglio con un metodo Create e un ctor privato.
  • L'uso dell'ereditarietà e un ctor protetto ha lo stesso problema.

Vorrei proporre la fabbrica come una classe parziale che contiene classi nidificate private con costruttori pubblici. Stai nascondendo al 100% l'oggetto che la tua fabbrica sta costruendo ed esponendo ciò che scegli attraverso una o più interfacce.

Il caso d'uso che ho sentito per questo sarebbe quando si desidera tenere traccia del 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 elimina la necessità di un assemblaggio separato per raggiungere questo obiettivo.

== 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

Metterei la fabbrica nello stesso Assembly della classe di dominio e contrassegnerei il costruttore della classe di dominio interno. In questo modo qualsiasi classe nel tuo dominio potrebbe essere in grado di creare un'istanza, ma ti fidi di non farlo, 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 discussione il tuo approccio :-)

Penso che se vuoi che la tua classe 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();
  }
}
1
Peter Morris

Non capisco perché desideri separare la "logica di business" da "business object". Questo suona come una distorsione dell'orientamento agli oggetti e finirai per annodarti seguendo questo approccio.

0
Jim Arnold

Non penso che ci sia una soluzione che non sia peggiore del problema, tutto ciò che richiede sopra è una fabbrica statica pubblica che IMHO è un problema peggiore e non impedirà alle persone di chiamare semplicemente la fabbrica per usare il tuo oggetto - non nasconde nulla. È meglio esporre un'interfaccia e/o mantenere il costruttore come interno se è possibile che sia la migliore protezione poiché l'Assemblea è un codice attendibile.

Un'opzione è quella di avere un costruttore statico che registri una fabbrica da qualche parte con qualcosa come un contenitore IOC.

0
user1496062

Ecco un'altra soluzione sulla scia di "solo perché non puoi significare che dovresti" ...

Soddisfa i requisiti di mantenere privato il costruttore di oggetti business e mettere la logica di fabbrica in un'altra classe. Dopodiché diventa un po 'impreciso.

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

La factory è astratta, quindi non è possibile crearne un'istanza (perché sarebbe anche un oggetto business, quindi sarebbe strano), e ha un costruttore privato, quindi il codice client non può derivarne.

Ciò che non viene impedito è il codice client anche derivante dalla classe dell'oggetto business e che chiama il metodo di costruzione statica protetta (ma non convalidata). O peggio, chiamando il costruttore predefinito protetto che abbiamo dovuto aggiungere per ottenere la compilazione della classe factory in primo luogo. (Che per inciso è probabilmente un problema con qualsiasi modello che separa la classe factory dalla classe dell'oggetto business.)

Non sto cercando di suggerire a nessuno nella loro mente giusta di fare qualcosa del genere, ma è stato un esercizio interessante. FWIW, la mia soluzione preferita sarebbe quella di utilizzare un costruttore interno e il confine dell'Assemblea 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()
    {
    }
}
0
yoyo

Gradirei sentire alcuni pensieri su questa soluzione. L'unico in grado di creare "MyClassPrivilegeKey" è la fabbrica. e 'MyClass' lo richiede nel costruttore. Evitando così la riflessione sugli appaltatori privati ​​/ "registrazione" alla fabbrica.

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