Patrón de fábrica en C #: ¿Cómo garantizar que una instancia de objeto solo pueda ser creada por una clase de fábrica?


93

Recientemente he estado pensando en proteger parte de mi código. Tengo curiosidad por saber cómo uno podría asegurarse de que un objeto nunca se pueda crear directamente, sino solo a través de algún método de una clase de fábrica. Digamos que tengo una clase de "objeto comercial" y quiero asegurarme de que cualquier instancia de esta clase tenga un estado interno válido. Para lograr esto, necesitaré realizar algunas comprobaciones antes de crear un objeto, probablemente en su constructor. Todo esto está bien hasta que decida que quiero que esta verificación sea parte de la lógica empresarial. Entonces, ¿cómo puedo hacer que un objeto comercial se pueda crear solo a través de algún método en mi clase de lógica comercial, pero nunca directamente? El primer deseo natural de usar una palabra clave de "amigo" de C ++ se quedará corto con C #. Entonces necesitamos otras opciones ...

Probemos con un ejemplo:

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

Todo está bien hasta que recuerde que aún puede crear la instancia MyBusinessObjectClass directamente, sin verificar la entrada. Me gustaría excluir por completo esa posibilidad técnica.

Entonces, ¿qué piensa la comunidad sobre esto?


¿El método MyBoClass está mal escrito?
pradeeptp

2
Estoy confundido, ¿por qué NO querría que sus objetos comerciales fueran los responsables de proteger sus propios invariantes? El punto del patrón de fábrica (según tengo entendido) es A) tratar con la creación de una clase compleja con muchas dependencias o B) dejar que la fábrica sea la que decida qué tipo de tiempo de ejecución devolver, solo exponiendo una interfaz / resumen clase al cliente. Poner sus reglas de negocio en sus objetos de negocio es la ESENCIA de la POO.
Sara

@kai Es cierto, el objeto comercial es responsable de proteger su invariante, pero el constructor que llama debe asegurarse de que los argumentos sean válidos antes de pasarlos al constructor . El propósito del CreateBusinessObjectmétodo es validar argumentos Y para (devolver un nuevo objeto válido O devolver un error) en una única llamada al método cuando una persona que llama no sabe inmediatamente si los argumentos son válidos para pasar al constructor.
Lightman

2
Estoy en desacuerdo. El constructor es responsable de realizar las comprobaciones necesarias para los parámetros dados. Si llamo a un constructor y no se lanza ninguna excepción, confío en que el objeto creado esté en un estado válido. No debería ser físicamente posible instanciar una clase usando los argumentos "incorrectos". La creación de una Distanceinstancia con un argumento negativo debería arrojar un, ArgumentOutOfRangeExceptionpor ejemplo, no ganaría nada al crear un DistanceFactoryque haga la misma verificación. Todavía no veo lo que puedes ganar aquí.
Sara

Respuestas:


55

Parece que solo desea ejecutar algo de lógica comercial antes de crear el objeto, entonces, ¿por qué no crea un método estático dentro de "BusinessClass" que hace todo el trabajo de verificación sucio de "myProperty" y hace que el constructor sea privado?

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

Llamarlo sería bastante sencillo:

BusinessClass objBusiness = BusinessClass.CreateObject(someProperty);

6
Ah, también podría lanzar una excepción en lugar de devolver un valor nulo.
Ricardo Nolde

5
No tiene sentido tener un constructor predeterminado privado si ha declarado uno personalizado. Además, en este caso, literalmente, no se gana nada usando un "constructor" estático en lugar de simplemente hacer la validación en el constructor real (ya que es parte de la clase de todos modos). es incluso PEOR, ya que ahora puede obtener un valor de retorno nulo, abriéndose para NRE y "¿qué diablos significa este nulo?" preguntas.
Sara

¿Alguna buena manera de requerir esta arquitectura a través de una interfaz / clase base abstracta? es decir, una interfaz / clase base abstracta que dicta que los implementadores no deben exponer un ctor.
Arash Motamedi

1
@Arash No. Las interfaces no pueden definir constructores, y una clase abstracta que define un constructor interno o protegido no puede evitar que las clases heredadas lo expongan a través de un constructor público propio.
Ricardo Nolde

66

Puede hacer que el constructor sea privado y la fábrica un tipo anidado:

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

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

Esto funciona porque los tipos anidados tienen acceso a los miembros privados de sus tipos adjuntos. Sé que es un poco restrictivo, pero espero que ayude ...


Pensé en eso, pero efectivamente mueve estos controles al objeto comercial en sí, lo que estoy tratando de evitar.
Usuario

@ so-tester: aún puede tener los cheques en la fábrica en lugar del tipo BusinessObject.
Jon Skeet

3
@JonSkeet Sé que esta pregunta es realmente antigua, pero tengo curiosidad por saber cuál es la ventaja de poner el CreateBusinessObjectmétodo dentro de una Factoryclase anidada en lugar de que ese método estático sea un método directamente de la BusinessObjectclase ... ¿puede recordar su motivación para hacerlo?
Kiley Naro

3
@KileyNaro: Bueno, eso es lo que pedía la pregunta :) No necesariamente confiere muchas ventajas, pero responde a la pregunta ... sin embargo, puede haber ocasiones en las que esto sea útil: el patrón del constructor me viene a la mente. (En ese caso, el constructor sería la clase anidada y tendría un método de instancia llamado Build.)
Jon Skeet

53

O, si quieres ir realmente elegante, invierte el control: haz que la clase devuelva la fábrica e instrumente la fábrica con un delegado que pueda crear la clase.

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

:)


Muy buen fragmento de código. La lógica de creación de objetos está en una clase separada y el objeto solo se puede crear usando la fábrica.
Nikolai Samteladze

Frio. ¿Existe alguna forma de limitar la creación de BusinessObjectFactory al BusinessObject.GetFactory estático?
Felix Keil

1
¿Cuál es la ventaja de esta inversión?
yatskovsky

1
@yatskovsky Es solo una forma de garantizar que el objeto solo se pueda instanciar de manera controlada y al mismo tiempo poder factorizar la lógica de creación en una clase dedicada.
Fabian Schmied

15

Puede hacer que el constructor de su clase MyBusinessObjectClass sea interno y moverlo y la fábrica a su propio ensamblado. Ahora solo la fábrica debería poder construir una instancia de la clase.


7

Aparte de lo que sugirió Jon, también puede hacer que el método de fábrica (incluida la verificación) sea un método estático de BusinessObject en primer lugar. Luego, haga que el constructor sea privado, y todos los demás se verán obligados a usar el método estático.

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

Pero la verdadera pregunta es: ¿por qué tiene este requisito? ¿Es aceptable trasladar la fábrica o el método de fábrica a la clase?


Esto no es un requisito. Solo quiero tener una separación clara entre el objeto comercial y la lógica. Así como es inapropiado tener estas comprobaciones en el código subyacente de las páginas, considero inapropiado tener estas comprobaciones en los propios objetos. Bueno, tal vez una validación básica, pero no realmente reglas comerciales.
Usuario

Si desea separar la lógica y la estructura de una clase solo por comodidad de revisión, siempre puede usar una clase parcial y tener ambas en archivos separados ...
Grx70

7

Después de tantos años me preguntaron esto, y todas las respuestas que veo desafortunadamente le dicen cómo debe hacer su código en lugar de dar una respuesta directa. La respuesta real que estaba buscando es tener sus clases con un constructor privado pero un instanciador público, lo que significa que solo puede crear nuevas instancias a partir de otras instancias existentes ... que solo están disponibles en la fábrica:

La interfaz para tus clases:

public interface FactoryObject
{
    FactoryObject Instantiate();
}

Tu clase:

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

    private YourClass() {}

    FactoryObject FactoryObject.Instantiate()
    {
        return new YourClass();
    }
}

Y, finalmente, la fábrica:

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

Luego, puede modificar fácilmente este código si necesita parámetros adicionales para la creación de instancias o para preprocesar las instancias que cree. Y este código le permitirá forzar la creación de instancias a través de la fábrica, ya que el constructor de la clase es privado.


4

Otra opción más (ligera) es crear un método de fábrica estático en la clase BusinessObject y mantener el constructor privado.

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

    private BusinessObject()
    {
    }
}

2

Entonces, parece que lo que quiero no se puede hacer de una manera "pura". Siempre es una especie de "devolución de llamada" a la clase lógica.

Tal vez podría hacerlo de una manera simple, simplemente haga un método constructor en la clase de objeto, primero llame a la clase lógica para verificar la entrada.

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 esta manera, el objeto comercial no se puede crear directamente y el método de verificación pública en la lógica comercial tampoco perjudicará.


2

En el caso de una buena separación entre interfaces e implementaciones, el
patrón protected-constructor-public-initializer permite una solución muy ordenada.

Dado un objeto comercial:

public interface IBusinessObject { }

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

    protected BusinessObject() 
    { ... }
}

y una fábrica de negocios:

public interface IBusinessFactory { }

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

    protected BusinessFactory() 
    { ... }
}

el siguiente cambio al BusinessObject.New()inicializador da la solución:

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

    ...
}

Aquí se necesita una referencia a la fábrica de negocios de hormigón para llamar al BusinessObject.New()inicializador. Pero el único que tiene la referencia requerida es la propia fábrica de negocios.

Conseguimos lo que queríamos: el único que puede crear BusinessObjectes BusinessFactory.


Entonces, ¿se asegura de que el único constructor público en funcionamiento del objeto requiera una fábrica, y ese parámetro de fábrica necesario solo se puede instanciar llamando al método estático de la fábrica? Parece una solución que funciona, pero tengo la sensación de que los fragmentos como public static IBusinessObject New(BusinessFactory factory) levantarán muchas cejas si alguien más mantiene el código.
Flater

@Flater de acuerdo. Cambiaría el nombre del parámetro factorya asFriend. En mi base de código se mostraría entonces comopublic static IBusinessObject New(BusinessFactory asFriend)
Reuven Bass

No lo entiendo en absoluto. ¿Como funciona? ¿La fábrica no puede crear nuevas instancias de IBusinessObject ni tiene ninguna intención (métodos) de hacerlo ...?
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!
    }

Pruebe mi enfoque, la 'fábrica' es el contenedor del controlador, y solo él puede crear una instancia para sí mismo. con algunas modificaciones, puede adaptarse a sus necesidades.
lin

1
En su ejemplo, ¿no sería posible lo siguiente (lo que no tendría sentido) ?: factory.DoWork ();
Whyser

1

Pondría la fábrica en el mismo ensamblado que la clase de dominio y marcaría el constructor de la clase de dominio como interno. De esta manera, cualquier clase en su dominio puede crear una instancia, pero confía en que no lo hará, ¿verdad? Cualquiera que escriba código fuera de la capa de dominio tendrá que usar su fábrica.

public class Person
{
  internal Person()
  {
  }
}

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

Sin embargo, debo cuestionar su enfoque :-)

Creo que si desea que su clase Person sea válida en el momento de la creación, debe colocar el código en el constructor.

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

1

Esta solución se basa en la idea de munificents de usar un token en el constructor. Hecho en esta respuesta, asegúrese de que el objeto solo sea creado por fábrica (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

Se han mencionado múltiples enfoques con diferentes compensaciones.

  • Anidar la clase de fábrica en la clase construida de forma privada solo permite que la fábrica construya 1 clase. En ese momento, estará mejor con un Createmétodo y un cliente privado.
  • El uso de herencia y un ctor protegido tiene el mismo problema.

Me gustaría proponer la fábrica como una clase parcial que contiene clases anidadas privadas con constructores públicos. Estás ocultando al 100% el objeto que está construyendo tu fábrica y solo expones lo que eliges a través de una o varias interfaces.

El caso de uso que escuché para esto sería cuando desea rastrear el 100% de las instancias en la fábrica. Este diseño garantiza que nadie más que la fábrica tenga acceso a la creación de instancias de "productos químicos" definidos en la "fábrica" ​​y elimina la necesidad de un ensamblaje separado para lograrlo.

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

No entiendo por qué desea separar la "lógica empresarial" del "objeto empresarial". Esto suena como una distorsión de la orientación del objeto, y terminará haciendo nudos al adoptar ese enfoque.


Tampoco entiendo esta distinción. ¿Alguien puede explicar por qué se hace esto y qué código estaría en el objeto comercial?
pipTheGeek

Es solo un enfoque que podría usarse. Quiero que el objeto comercial sea principalmente estructuras de datos, pero no con todo el campo abierto para lectura / escritura. Pero la cuestión realmente era poder crear una instancia de un objeto solo con la ayuda de un método de fábrica y no directamente.
Usuario

@Jim: Personalmente estoy de acuerdo con tu punto, pero profesionalmente, esto se hace constantemente. Mi proyecto actual es un servicio web que toma una cadena HTML, la limpia de acuerdo con algunas reglas preestablecidas y luego devuelve la cadena limpia. Debo pasar por 7 capas de mi propio código "porque todos nuestros proyectos deben construirse a partir de la misma plantilla de proyecto". Es lógico que la mayoría de las personas que hacen estas preguntas sobre SO no tengan la libertad de cambiar todo el flujo de su código.
Flater

0

No creo que haya una solución que no sea peor que el problema, todo lo anterior requiere una fábrica estática pública que, en mi humilde opinión, es un problema peor y no impedirá que la gente simplemente llame a la fábrica para usar su objeto, no oculta nada. Es mejor exponer una interfaz y / o mantener el constructor como interno si puede, esa es la mejor protección ya que el ensamblado es un código confiable.

Una opción es tener un constructor estático que registre una fábrica en algún lugar con algo como un contenedor IOC.


0

Aquí hay otra solución en la línea de "solo porque puedas no significa que debas" ...

Cumple los requisitos de mantener privado el constructor de objetos de negocio y colocar la lógica de fábrica en otra clase. Después de eso, se vuelve un poco vago.

La clase de fábrica tiene un método estático para crear objetos comerciales. Se deriva de la clase de objeto de negocio para acceder a un método de construcción protegido estático que invoca al constructor privado.

La fábrica es abstracta, por lo que no puede crear una instancia de ella (porque también sería un objeto comercial, por lo que sería extraño), y tiene un constructor privado, por lo que el código del cliente no puede derivarse de él.

Lo que no se impide es que el código de cliente también se derive de la clase de objeto de negocio y llame al método de construcción estático protegido (pero no validado). O peor aún, llamar al constructor predeterminado protegido que tuvimos que agregar para que la clase de fábrica se compilara en primer lugar. (Lo que, por cierto, es probable que sea un problema con cualquier patrón que separe la clase de fábrica de la clase de objeto comercial).

No estoy tratando de sugerir a nadie en su sano juicio que haga algo como esto, pero fue un ejercicio interesante. FWIW, mi solución preferida sería usar un constructor interno y el límite de ensamblaje como 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

Agradecería escuchar algunos pensamientos sobre esta solución. El único capaz de crear 'MyClassPrivilegeKey' es la fábrica. y 'MyClass' lo requiere en el constructor. Evitando así la reflexión sobre contratistas privados / "registro" a la fábrica.

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);
            }
        }
    }
}
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.