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


93

Recentemente ho pensato di proteggere parte del mio codice. Sono curioso di sapere come si possa essere sicuri che un oggetto non possa mai essere creato direttamente, ma solo tramite un metodo di una classe factory. Diciamo che ho una classe "oggetto business" e voglio assicurarmi che ogni istanza di questa classe abbia uno stato interno valido. Per ottenere ciò dovrò eseguire qualche controllo prima di creare un oggetto, probabilmente nel suo costruttore. Va tutto bene finché non decido che voglio che questo controllo faccia parte della logica aziendale. Quindi, come posso fare in modo che un oggetto aziendale sia creabile solo tramite 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 ++ fallirà con C #. Quindi abbiamo bisogno di altre opzioni ...

Proviamo qualche 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.

Allora, cosa ne pensa la comunità di questo?


il metodo MyBoClass è scritto male?
pradeeptp

2
Sono confuso, perché NON vorresti che i tuoi oggetti di business fossero quelli responsabili della protezione dei propri invarianti? Il punto del pattern factory (a quanto ho capito) è A) occuparsi della creazione di una classe complessa con molte dipendenze oppure B) lasciare che sia la factory a decidere quale tipo di runtime restituire, esponendo solo un'interfaccia / abstract classe al cliente. Mettere le tue regole di business nei tuoi oggetti di business è l'ESSENZA stessa dell'OOP.
sara

@kai Vero, l'oggetto business è responsabile della protezione del suo invariante, ma il chiamante del costruttore deve assicurarsi che gli argomenti siano validi prima di passarli al costruttore ! Lo scopo del CreateBusinessObjectmetodo è convalidare gli argomenti AND a (restituire un nuovo oggetto valido O restituire un errore) in una singola chiamata al metodo quando un chiamante non sa immediatamente se gli argomenti sono validi per il passaggio al costruttore.
Lightman

2
Non sono d'accordo. Il costruttore è responsabile di eseguire qualsiasi controllo richiesto per i parametri dati. Se chiamo un costruttore e non viene generata alcuna eccezione, confido che l'oggetto creato sia in uno stato valido. Non dovrebbe essere fisicamente possibile istanziare una classe usando argomenti "sbagliati". La creazione di Distanceun'istanza con un argomento negativo dovrebbe generare un, ArgumentOutOfRangeExceptionad esempio, non guadagneresti nulla creando un DistanceFactoryche esegue lo stesso controllo. Continuo a non vedere cosa hai da guadagnare qui.
sara

Risposte:


55

Sembra che tu voglia solo eseguire un po 'di logica aziendale prima di creare l'oggetto, quindi perché non crei semplicemente un metodo statico all'interno di "BusinessClass" che fa tutto il lavoro di controllo "myProperty" sporco e rende 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);

6
Ah, potresti anche lanciare un'eccezione invece di restituire null.
Ricardo Nolde

5
non ha senso avere un costruttore predefinito privato se ne hai dichiarato uno personalizzato. inoltre, in questo caso non c'è letteralmente nulla da guadagnare dall'uso di un "costruttore" statico invece di fare solo la convalida nel costruttore reale (dal momento che fa parte della classe comunque). è anche PEGGIORE, dal momento che ora puoi ottenere un valore di ritorno nullo, aprendo per NRE e "cosa diavolo significa questo nullo?" domande.
sara

Qualche buon modo per richiedere questa architettura tramite un'interfaccia / classe base astratta? cioè un'interfaccia / classe base astratta che impone agli implementatori di non esporre un ctor.
Arash Motamedi

1
@Arash No. Le interfacce non possono definire costruttori e una classe astratta che definisce un costruttore protetto o interno non può impedire che le classi ereditate lo espongano tramite un proprio costruttore pubblico.
Ricardo Nolde

66

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

Questo funziona perché i tipi annidati hanno accesso ai membri privati ​​dei loro tipi che li racchiudono. So che è un po 'restrittivo, ma spero che possa aiutare ...


Ci ho pensato, ma sposta efficacemente questi controlli nell'oggetto aziendale stesso, cosa che sto cercando di evitare.
Utente

@ so-tester: puoi ancora avere i controlli in fabbrica invece del tipo BusinessObject.
Jon Skeet,

3
@ JonSkeet So che questa domanda è molto vecchia, ma sono curioso di sapere quale sia il vantaggio di mettere il CreateBusinessObjectmetodo all'interno di una Factoryclasse annidata invece di avere quel metodo statico come metodo direttamente della BusinessObjectclasse ... puoi ricordare il tuo motivazione per farlo?
Kiley Naro

3
@KileyNaro: Beh, questo è ciò che la domanda stava chiedendo :) Non conferisce necessariamente molti vantaggi, ma risponde alla domanda ... ci possono essere momenti in cui questo è utile, però - mi viene in mente il modello builder. (In tal caso il builder sarebbe la classe nidificata e avrebbe un metodo di istanza chiamato Build.)
Jon Skeet

53

Oppure, se vuoi essere davvero stravagante, inverti il ​​controllo: chiedi alla classe di restituire la fabbrica e strumentalizzare la 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;
  }
}

:)


Un bel pezzo di codice. La logica di creazione degli oggetti si trova in una classe separata e l'oggetto può essere creato solo utilizzando la fabbrica.
Nikolai Samteladze

Freddo. Esiste un modo per limitare la creazione di BusinessObjectFactory a BusinessObject.GetFactory statico?
Felix Keil

1
Qual è il vantaggio di questa inversione?
yatskovsky

1
@yatskovsky È solo un modo per garantire che l'oggetto possa essere istanziato solo in modo controllato pur essendo in grado di scomporre la logica di creazione in una classe dedicata.
Fabian Schmied,

15

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


7

A parte quanto suggerito da Jon, potresti anche fare in modo che il metodo factory (incluso il controllo) sia un metodo statico di BusinessObject in primo luogo. Quindi, rendere privato il costruttore 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 factory o il metodo factory nella classe?


Questo non è certo un requisito. Voglio solo avere una netta separazione tra l'oggetto aziendale e la logica. Così come non è appropriato avere questi controlli nel code-behind delle pagine, ritengo inappropriato avere questi controlli negli oggetti stessi. Beh, forse una convalida di base, ma non delle regole aziendali.
Utente

Se vuoi separare la logica e la struttura di una classe solo per comodità di revisione, puoi sempre usare una classe parziale e avere entrambe in file separati ...
Grx70

7

Dopo così tanti anni questo mi è 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 istanziatore pubblico, il che significa che puoi creare nuove istanze solo 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 consentirà di forzare l'istanziazione attraverso la fabbrica poiché il costruttore della classe è privato.


4

Un'altra opzione (leggera) consiste nel 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()
    {
    }
}

2

Quindi, sembra che quello che voglio non possa essere fatto in modo "puro". È sempre una sorta di "richiamo" alla classe di logica.

Forse potrei farlo in un modo semplice, basta fare in modo che un metodo di costruzione nella classe dell'oggetto chiami prima 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 è creabile direttamente e il metodo di controllo pubblico nella logica aziendale non danneggerà neanche.


2

In un caso di buona separazione tra interfacce e implementazioni, il
pattern protected-constructor-public-initializer consente una soluzione molto chiara.

Dato un oggetto aziendale:

public interface IBusinessObject { }

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

    protected BusinessObject() 
    { ... }
}

e una fabbrica aziendale:

public interface IBusinessFactory { }

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

    protected BusinessFactory() 
    { ... }
}

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

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

    ...
}

Qui è necessario un riferimento alla fabbrica aziendale concreta per chiamare l' BusinessObject.New()inizializzatore. Ma l'unico che ha il riferimento richiesto è la stessa fabbrica aziendale.

Abbiamo ottenuto quello che volevamo: l'unico che può creare BusinessObjectè BusinessFactory.


Quindi ti assicuri che l'unico costruttore pubblico funzionante dell'oggetto richieda una Factory e che il parametro Factory necessario possa essere istanziato solo chiamando il metodo statico della Factory? Sembra una soluzione funzionante, ma ho la sensazione che snippet come questo public static IBusinessObject New(BusinessFactory factory) solleveranno molte sopracciglia se qualcun altro mantiene il codice.
Flater

@Flater d'accordo. Cambierò il nome del parametro factoryin asFriend. Nella mia base di codice sarebbe quindi visualizzato comepublic static IBusinessObject New(BusinessFactory asFriend)
Reuven Bass

Non lo capisco affatto. Come funziona? La fabbrica non può creare nuove istanze di IBusinessObject né ha alcuna intenzione (metodi) di farlo ...?
lapsus

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

Prova il mio approccio, la "fabbrica" ​​è il contenitore del gestore e solo esso può creare un'istanza per se stesso. con alcune modifiche, potrebbe adattarsi alle tue esigenze.
lin

1
Nel tuo esempio, non sarebbe possibile quanto segue (il che non avrebbe senso) ?: factory.DoWork ();
Whyser

1

Metterei la factory nello stesso assembly della classe di dominio e contrassegnerei il costruttore della classe di dominio come 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 di dominio dovrà utilizzare la tua 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 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

Questa soluzione si basa su un'idea generosa di utilizzare un token nel costruttore. Fatto in questa risposta assicurati 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

Sono stati menzionati approcci multipli con diversi compromessi.

  • Annidare la classe di fabbrica nella classe costruita privatamente consente alla fabbrica di costruire solo 1 classe. A quel punto stai meglio con un Createmetodo e un privato.
  • L'utilizzo dell'ereditarietà e di un ctor protetto presenta lo stesso problema.

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

Il caso d'uso che ho sentito per questo sarebbe quando vuoi monitorare il 100% delle istanze in fabbrica. Questo design garantisce che nessuno, tranne la fabbrica, abbia accesso alla creazione di istanze di "sostanze chimiche" definite nella "fabbrica" ​​e rimuove la necessità di un assieme separato per ottenere ciò.

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

0

Non capisco perché vuoi separare la "logica di business" dall '"oggetto di business". Sembra una distorsione dell'orientamento degli oggetti e finirai per annoiarti adottando quell'approccio.


Non capisco nemmeno questa distinzione. Qualcuno può spiegare perché questo viene fatto e quale codice sarebbe nell'oggetto business?
pipTheGeek

È solo un approccio che potrebbe essere utilizzato. Voglio che l'oggetto di business sia principalmente strutture di dati ma non con tutto il campo aperto per la lettura / scrittura. Ma la domanda in realtà riguardava la possibilità di istanziare un oggetto solo con l'aiuto di un metodo di fabbrica e non direttamente.
Utente

@ Jim: personalmente sono d'accordo con il tuo punto, ma professionalmente, questo viene fatto costantemente. Il mio progetto attuale è un servizio web che prende una stringa HTML, la pulisce secondo alcune regole preimpostate, quindi restituisce la stringa pulita. Devo passare attraverso 7 livelli del mio codice "perché tutti i nostri progetti devono essere costruiti dallo stesso modello di progetto". È logico che la maggior parte delle persone che pongono queste domande su SO non abbiano la libertà di modificare l'intero flusso del proprio codice.
Flater

0

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

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


0

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

Soddisfa i requisiti per mantenere privato il costruttore di oggetti di business e inserire la logica di fabbrica in un'altra classe. Dopo di che diventa un po 'abbozzato.

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

La fabbrica è astratta, quindi non è possibile crearne un'istanza (perché sarebbe anche un oggetto di business, quindi sarebbe strano) e ha un costruttore privato in modo che il codice client non possa derivarne.

Ciò che non viene impedito è il codice client che deriva anche dalla classe dell'oggetto di business e chiama il metodo di costruzione statico protetto (ma non convalidato). O peggio, chiamando il costruttore predefinito protetto che abbiamo dovuto aggiungere per compilare la classe factory in primo luogo. (Che incidentalmente potrebbe essere un problema con qualsiasi modello che separa la classe factory dalla classe dell'oggetto business.)

Non sto cercando di suggerire a qualcuno sano di mente 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'assieme come protezione.

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

Gradirei sentire alcune riflessioni su questa soluzione. L'unico in grado di creare "MyClassPrivilegeKey" è la fabbrica. e "MyClass" lo richiede nel costruttore. Evitando così di riflettere su 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);
            }
        }
    }
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.