Padrão de fábrica em C #: como garantir que uma instância de objeto só possa ser criada por uma classe de fábrica?


93

Recentemente, tenho pensado em proteger parte do meu código. Estou curioso para saber como alguém poderia ter certeza de que um objeto nunca pode ser criado diretamente, mas apenas por meio de algum método de uma classe de fábrica. Digamos que eu tenha alguma classe de "objeto de negócios" e quero ter certeza de que qualquer instância dessa classe terá um estado interno válido. Para conseguir isso, vou precisar fazer algumas verificações antes de criar um objeto, provavelmente em seu construtor. Está tudo bem até eu decidir que quero fazer com que essa verificação faça parte da lógica de negócios. Portanto, como posso fazer com que um objeto de negócios seja criável apenas por meio de algum método em minha classe de lógica de negócios, mas nunca diretamente? O primeiro desejo natural de usar uma boa e velha palavra-chave "amiga" do C ++ ficará aquém do C #. Então, precisamos de outras opções ...

Vamos tentar alguns exemplos:

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

Está tudo bem até você se lembrar que ainda pode criar a instância MyBusinessObjectClass diretamente, sem verificar a entrada. Eu gostaria de excluir totalmente essa possibilidade técnica.

Então, o que a comunidade pensa sobre isso?


O método MyBoClass é incorreto?
pradeeptp

2
Estou confuso, por que você NÃO gostaria que seus objetos de negócios fossem os responsáveis ​​por proteger seus próprios invariantes? O objetivo do padrão de fábrica (como eu o entendo) é A) lidar com a criação de uma classe complexa com muitas dependências ou B) deixar a fábrica decidir qual tipo de runtime retornar, apenas expondo uma interface / abstrata classe para o cliente. Colocar suas regras de negócios em seus objetos de negócios é a própria ESSÊNCIA da OOP.
Sara

@kai Verdadeiro, o objeto de negócios é responsável por proteger sua invariante, mas o responsável pela chamada do construtor deve garantir que os argumentos sejam válidos antes de passá-los para o construtor ! O objetivo do CreateBusinessObjectmétodo é validar argumentos AND para (retornar um novo objeto válido OU retornar um erro) em uma única chamada de método quando um chamador não sabe imediatamente se os argumentos são válidos para passar para o construtor.
Lightman

2
Discordo. O construtor é responsável por fazer todas as verificações necessárias para os parâmetros fornecidos. Se eu chamar um construtor e nenhuma exceção for lançada, acredito que o objeto criado está em um estado válido. Não deveria ser fisicamente possível instanciar uma classe usando os argumentos "errados". Criar uma Distanceinstância com um argumento negativo deve lançar um, ArgumentOutOfRangeExceptionpor exemplo, você não ganharia nada criando um DistanceFactoryque faça a mesma verificação. Ainda não vejo o que você tem a ganhar aqui.
Sara

Respostas:


55

Parece que você apenas deseja executar alguma lógica de negócios antes de criar o objeto - então por que você simplesmente não cria um método estático dentro da "BusinessClass" que faz toda a verificação suja de "myProperty" funcionar e torna o construtor 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;
    }
}

Ligar seria muito simples:

BusinessClass objBusiness = BusinessClass.CreateObject(someProperty);

6
Ah, você também pode lançar uma exceção em vez de retornar nulo.
Ricardo Nolde

5
não faz sentido ter um construtor padrão privado se você declarou um personalizado. também, neste caso, não há literalmente nada a ganhar usando um "construtor" estático em vez de apenas fazer a validação no construtor real (já que é parte da classe de qualquer maneira). é ainda PIOR, já que agora você pode obter um valor de retorno nulo, abrindo para NREs e "o que diabos esse nulo significa?" questões.
Sara

Alguma boa maneira de exigir essa arquitetura por meio de uma interface / classe base abstrata? ou seja, uma interface / classe base abstrata que determina que os implementadores não devem expor um ctor.
Arash Motamedi

1
@Arash Não. As interfaces não podem definir construtores, e uma classe abstrata que define um construtor protegido ou interno não pode impedir que classes herdadas o exponham por meio de um construtor público próprio.
Ricardo Nolde

66

Você pode tornar o construtor privado e a fábrica um tipo aninhado:

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

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

Isso funciona porque os tipos aninhados têm acesso aos membros privados de seus tipos de inclusão. Eu sei que é um pouco restritivo, mas espero que ajude ...


Eu pensei sobre isso, mas ele efetivamente move essas verificações para o próprio objeto de negócios, o que estou tentando evitar.
Usuário de

@ so-tester: você ainda pode ter os cheques na fábrica em vez do tipo BusinessObject.
Jon Skeet

3
@JonSkeet Eu sei que esta questão é muito antiga, mas estou curioso para saber qual é a vantagem de colocar o CreateBusinessObjectmétodo dentro de uma Factoryclasse aninhada em vez de fazer com que o método estático seja um método diretamente da BusinessObjectclasse ... você pode se lembrar do seu motivação para fazer isso?
Kiley Naro

3
@KileyNaro: Bem, era isso que a pergunta estava pedindo :) Não necessariamente confere muitas vantagens, mas responde à pergunta ... pode haver momentos em que isso seja útil - o padrão do construtor vem à mente. (Nesse caso, o construtor seria a classe aninhada e teria um método de instância chamado Build.)
Jon Skeet

53

Ou, se você quiser ficar realmente sofisticado, inverta o controle: faça com que a classe devolva a fábrica e forneça à fábrica um delegado que possa criar a 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;
  }
}

:)


Um código muito bom. A lógica de criação de objeto está em uma classe separada e o objeto pode ser criado apenas usando a fábrica.
Nikolai Samteladze

Legal. Existe alguma maneira de limitar a criação do BusinessObjectFactory ao BusinessObject.GetFactory estático?
Felix Keil

1
Qual é a vantagem dessa inversão?
yatskovsky

1
@yatskovsky É apenas uma forma de garantir que o objeto só possa ser instanciado de uma forma controlada enquanto ainda é capaz de fatorar a lógica de criação em uma classe dedicada.
Fabian Schmied

15

Você poderia tornar o construtor em sua classe MyBusinessObjectClass interno e movê-lo junto com a fábrica para seu próprio assembly. Agora, apenas a fábrica deve ser capaz de construir uma instância da classe.


7

Além do que Jon sugeriu, você também pode fazer com que o método de fábrica (incluindo a verificação) seja um método estático de BusinessObject em primeiro lugar. Então, tenha o construtor privado e todos os outros serão forçados a usar o método estático.

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

Mas a verdadeira questão é - por que você tem esse requisito? É aceitável mover a fábrica ou o método de fábrica para a classe?


Isso dificilmente é um requisito. Eu só quero ter uma separação clara de objeto de negócios e lógica. Assim como é impróprio ter essas verificações no code-behind das páginas, considero impróprio ter essas verificações nos próprios objetos. Bem, talvez validação básica, mas não realmente regras de negócios.
Usuário

Se você deseja separar a lógica e a estrutura de uma classe apenas para fins de revisão, você sempre pode usar uma classe parcial e ter ambas em arquivos separados ...
Grx70

7

Depois de tantos anos, isso foi perguntado, e todas as respostas que vejo estão, infelizmente, dizendo a você como você deve fazer seu código em vez de dar uma resposta direta. A resposta real que você estava procurando é ter suas classes com um construtor privado, mas um instanciador público, o que significa que você só pode criar novas instâncias de outras instâncias existentes ... que estão disponíveis apenas na fábrica:

A interface para suas aulas:

public interface FactoryObject
{
    FactoryObject Instantiate();
}

Sua classe:

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

    private YourClass() {}

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

E, finalmente, a 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();
    }
}

Em seguida, você pode modificar facilmente esse código se precisar de parâmetros extras para a instanciação ou para pré-processar as instâncias criadas. E esse código permitirá que você force a instanciação pela fábrica, pois o construtor da classe é privado.


4

Outra opção (leve) é fazer um método de fábrica estático na classe BusinessObject e manter o construtor privado.

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

    private BusinessObject()
    {
    }
}

2

Então, parece que o que eu quero não pode ser feito de uma forma "pura". É sempre algum tipo de "chamada de volta" para a classe de lógica.

Talvez eu pudesse fazer de forma simples, basta fazer um método construtor na classe de objeto primeiro chamar a classe lógica para verificar a 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;
    }
}

Dessa forma, o objeto de negócios não pode ser criado diretamente e o método de verificação pública na lógica de negócios também não causará danos.


2

Em um caso de boa separação entre interfaces e implementações, o
padrão protected-constructor-public-initializer permite uma solução muito simples.

Dado um objeto de negócios:

public interface IBusinessObject { }

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

    protected BusinessObject() 
    { ... }
}

e uma fábrica de negócios:

public interface IBusinessFactory { }

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

    protected BusinessFactory() 
    { ... }
}

a seguinte mudança para BusinessObject.New() inicializador fornece a solução:

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

    ...
}

Aqui, uma referência à fábrica de negócios de concreto é necessária para chamar o BusinessObject.New() inicializador. Mas a única que tem a referência necessária é a própria fábrica de negócios.

Nós conseguimos o que queríamos: o único que pode criar BusinessObjecté BusinessFactory.


Portanto, você garante que o único construtor público funcional do objeto requer um Factory e que o parâmetro Factory necessário só pode ser instanciado chamando o método estático do Factory? Parece uma solução funcional, mas tenho a sensação de que trechos como public static IBusinessObject New(BusinessFactory factory) vão levantar muitas sobrancelhas se outra pessoa mantiver o código.
Dia

@Flater Concordado. Eu mudaria o nome do parâmetro factorypara asFriend. Em minha base de código, ele seria mostrado comopublic static IBusinessObject New(BusinessFactory asFriend)
Reuven Bass

Eu não entendo de jeito nenhum. Como funciona? A fábrica não pode criar novas instâncias de IBusinessObject nem tem intenção (métodos) de fazê-lo ...?
lapsus de

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

Por favor, tente minha abordagem, A 'fábrica' é o contêiner do manipulador e somente ela pode criar uma instância para si mesma. com algumas modificações, pode atender às suas necessidades.
lin

1
Em seu exemplo, o seguinte não seria possível (o que não faria sentido) ?: factory.DoWork ();
Whyser

1

Eu colocaria a fábrica no mesmo conjunto que a classe de domínio e marcaria o construtor da classe de domínio interno. Dessa forma, qualquer classe em seu domínio pode ser capaz de criar uma instância, mas você não confia em si mesmo, certo? Qualquer pessoa que escrever código fora da camada de domínio terá que usar sua fábrica.

public class Person
{
  internal Person()
  {
  }
}

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

No entanto, devo questionar sua abordagem :-)

Eu acho que se você quiser que sua classe Person seja válida na criação, você deve colocar o código no construtor.

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

1

Esta solução é baseada fora munificents idéia de usar um token no construtor. Feito nesta resposta, certifique-se de que o objeto foi criado apenas pela 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

Múltiplas abordagens com diferentes compensações foram mencionadas.

  • Aninhar a classe de fábrica na classe de construção privada permite que a fábrica construa apenas 1 classe. Nesse ponto, você estará melhor com um Createmétodo e um ctor privado.
  • Usar herança e um ctor protegido tem o mesmo problema.

Eu gostaria de propor a fábrica como uma classe parcial que contém classes aninhadas privadas com construtores públicos. Você está 100% escondendo o objeto que sua fábrica está construindo e apenas expondo o que você escolhe por meio de uma ou várias interfaces.

O caso de uso que ouvi para isso seria quando você deseja rastrear 100% das instâncias na fábrica. Este design garante que ninguém, mas a fábrica tem acesso à criação de instâncias de "produtos químicos" definidos na "fábrica" ​​e elimina a necessidade de uma montagem separada para isso.

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

Não entendo por que você deseja separar a "lógica de negócios" do "objeto de negócios". Isso soa como uma distorção da orientação do objeto, e você vai acabar se amarrando em nós ao fazer essa abordagem.


Eu também não entendo essa distinção. Alguém pode explicar por que isso está sendo feito e que código estaria no objeto de negócios?
pipTheGeek

É apenas uma abordagem que pode ser usada. Eu quero que os objetos de negócios sejam principalmente estruturas de dados, mas não com todos os campos abertos para leitura / gravação. Mas a questão realmente era ser capaz de instanciar um objeto apenas com a ajuda de um método de fábrica e não diretamente.
Usuário

@Jim: Pessoalmente, concordo com seu ponto, mas profissionalmente, isso é feito constantemente. Meu projeto atual é um serviço da Web que pega uma string HTML, limpa-a de acordo com algumas regras predefinidas e, em seguida, retorna a string limpa. Sou obrigado a passar por 7 camadas do meu próprio código "porque todos os nossos projetos devem ser construídos a partir do mesmo modelo de projeto". É lógico que a maioria das pessoas que fazem essas perguntas no SO não tem liberdade para alterar todo o fluxo de seu código.
Dia

0

Eu não acho que haja uma solução que não seja pior do que o problema, todos acima exigem uma fábrica estática pública que IMHO é um problema pior e não vai impedir as pessoas de chamarem a fábrica para usar seu objeto - ele não esconde nada. Melhor expor uma interface e / ou manter o construtor como interno, se possível, essa é a melhor proteção, já que o assembly é um código confiável.

Uma opção é ter um construtor estático que registra uma fábrica em algum lugar com algo como um contêiner IOC.


0

Aqui está outra solução no estilo "só porque você pode, não significa que você deve" ...

Ele atende aos requisitos de manter o construtor do objeto de negócios privado e colocar a lógica de fábrica em outra classe. Depois disso, fica um pouco incompleto.

A classe de fábrica possui um método estático para criar objetos de negócios. Ele deriva da classe de objeto de negócios para acessar um método de construção protegido estático que invoca o construtor privado.

A fábrica é abstrata, então você não pode realmente criar uma instância dela (porque também seria um objeto de negócios, o que seria estranho) e tem um construtor privado para que o código do cliente não possa derivar dele.

O que não é evitado é o código do cliente também derivando da classe de objeto de negócios e chamando o método de construção estático protegido (mas não validado). Ou pior, chamar o construtor padrão protegido que tivemos que adicionar para que a classe de fábrica fosse compilada em primeiro lugar. (O que, aliás, é provável que seja um problema com qualquer padrão que separe a classe de fábrica da classe de objeto de negócios.)

Não estou tentando sugerir que alguém em sã consciência deva fazer algo assim, mas foi um exercício interessante. FWIW, minha solução preferida seria usar um construtor interno e o limite da montagem como proteção.

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

Gostaria de ouvir algumas reflexões sobre esta solução. O único capaz de criar 'MyClassPrivilegeKey' é a fábrica. e 'MyClass' requer isso no construtor. Evitando assim, reflexão sobre empreiteiros privados / “registo” à 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);
            }
        }
    }
}
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.