Por que devo usar uma classe de fábrica em vez da construção direta de objetos?


162

Vi o histórico de vários projetos de biblioteca de classes С # e Java no GitHub e CodePlex, e vejo uma tendência de mudar para classes de fábrica em vez de instanciar objetos diretamente.

Por que devo usar as classes de fábrica extensivamente? Eu tenho uma biblioteca muito boa, onde os objetos são criados à moda antiga - invocando construtores públicos de classes. Nos últimos commits, os autores mudaram rapidamente todos os construtores públicos de milhares de classes para internos, e também criaram uma enorme classe de fábrica com milhares de CreateXXXmétodos estáticos que apenas retornam novos objetos chamando os construtores internos das classes. A API do projeto externo está quebrada, bem feita.

Por que essa mudança seria útil? Qual é o sentido de refatorar dessa maneira? Quais são os benefícios de substituir chamadas para construtores de classe pública por chamadas de método de fábrica estática?

Quando devo usar construtores públicos e quando devo usar fábricas?



1
O que é uma classe / método de malha?
Doval

Respostas:


56

As classes de fábrica geralmente são implementadas porque permitem que o projeto siga os princípios do SOLID mais de perto. Em particular, os princípios de segregação de interface e inversão de dependência.

Fábricas e interfaces permitem muito mais flexibilidade a longo prazo. Permite um design mais dissociado - e, portanto, mais testável. Aqui está uma lista não exaustiva do motivo pelo qual você pode seguir este caminho:

  • Permite introduzir facilmente um contêiner de Inversão de Controle (IoC)
  • Torna seu código mais testável, pois você pode zombar de interfaces
  • Dá muito mais flexibilidade na hora de alterar o aplicativo (ou seja, você pode criar novas implementações sem alterar o código dependente)

Considere esta situação.

O conjunto A (-> significa depende):

Class A -> Class B
Class A -> Class C
Class B -> Class D

Quero mover a Classe B para o Assembly B, que depende do Assembly A. Com essas dependências concretas, tenho que mover a maior parte de toda a minha hierarquia de classes. Se eu usar interfaces, posso evitar muita dor.

Montagem A:

Class A -> Interface IB
Class A -> Interface IC
Class B -> Interface IB
Class C -> Interface IC
Class B -> Interface ID
Class D -> Interface ID

Agora posso mover a classe B para a montagem B sem nenhuma dor. Ainda depende das interfaces no conjunto A.

O uso de um contêiner de IoC para resolver suas dependências permite ainda mais flexibilidade. Não há necessidade de atualizar cada chamada para o construtor sempre que você alterar as dependências da classe.

Seguir o princípio de segregação de interface e o princípio de inversão de dependência nos permite criar aplicativos dissociados e altamente flexíveis. Depois de trabalhar em um desses tipos de aplicativos, você nunca mais voltará a usar a newpalavra - chave.


40
As fábricas da OMI são a coisa menos importante para o SOLID. Você pode fazer o SOLID muito bem sem fábricas.
Euphoric

26
Uma coisa que nunca fez sentido para mim é que, se você usa fábricas para criar novos objetos, precisa fazer a fábrica em primeiro lugar. Então, o que exatamente isso te leva? Supõe-se que alguém lhe dará a fábrica em vez de você instancia-la por conta própria ou algo mais? Isso deve ser mencionado na resposta, caso contrário, não está claro como as fábricas realmente resolvem qualquer problema.
Mehrdad

8
@ BЈовић - Exceto que toda vez que você adiciona uma nova implementação, agora você abre a fábrica e a modifica para dar conta da nova implementação - violando explicitamente o OCP.
Telastyn

6
@Telastyn Sim, mas o código usando os objetos criados não muda. Isso é mais importante do que as mudanças na fábrica.
BЈовић

8
As fábricas são úteis em certas áreas abordadas pela resposta. Eles não são apropriados para uso em qualquer lugar . Por exemplo, o uso de fábricas para criar strings ou listas levaria muito longe. Mesmo para UDTs, eles nem sempre são necessários. A chave é usar uma fábrica quando a implementação exata de uma interface precisar ser dissociada.

126

Como o whatsisname disse, acredito que seja o caso do design de software para cultos de carga . Fábricas, especialmente do tipo abstrato, só podem ser usadas quando seu módulo cria várias instâncias de uma classe e você deseja dar ao usuário deste módulo a capacidade de especificar qual tipo criar. Esse requisito é realmente muito raro, porque na maioria das vezes você só precisa de uma instância e pode transmiti-la diretamente, em vez de criar uma fábrica explícita.

O fato é que as fábricas (e singletons) são extremamente fáceis de implementar e, portanto, as pessoas as usam muito, mesmo em locais onde não são necessárias. Então, quando o programador pensa "Que padrões de design devo usar neste código?" a fábrica é a primeira que vem à sua mente.

Muitas fábricas são criadas porque "Talvez um dia eu precise criar essas classes de maneira diferente" em mente. O que é uma clara violação do YAGNI .

E as fábricas ficam obsoletas quando você introduz a estrutura de IoC, porque a IoC é apenas um tipo de fábrica. E muitas estruturas de IoC são capazes de criar implementações de fábricas específicas.

Além disso, não há um padrão de design que diga criar enormes classes estáticas com CreateXXXmétodos que apenas chamam construtores. E, especialmente, não é chamada de Fábrica (nem Fábrica Abstrata).


6
Concordo com a maioria dos seus pontos, exceto "IoC é o tipo de fábrica" : os contêineres de IoC não são fábricas . Eles são um método conveniente de obter injeção de dependência. Sim, algumas são capazes de construir fábricas automáticas, mas não são fábricas próprias e não devem ser tratadas como tal. Eu também sustentaria o ponto YAGNI. É útil poder substituir um teste duas vezes no seu teste de unidade. Refatorar tudo para fornecer isso depois do fato é um pé no saco . Planeje com antecedência e não cair para a desculpa "YAGNI"
AlexFoxGill

11
@AlexG - eh ... na prática, praticamente todos os contêineres IoC funcionam como fábricas.
Telastyn

8
@AlexG O foco principal da IoC é construir objetos concretos para passar para outros objetos com base na configuração / convenção. É a mesma coisa que usar fábrica. E você não precisa de fábrica para poder criar uma simulação para teste. Você apenas instancia e passa o mock diretamente. Como eu disse no primeiro parágrafo. Fábricas são úteis apenas quando você deseja passar a criação de uma instância para o usuário do módulo, que chama a fábrica.
Euphoric

5
Há uma distinção a ser feita. O padrão de fábrica é usado por um consumidor para criar entidades durante o tempo de execução de um programa. Um contêiner de IoC é usado para criar o gráfico de objeto do programa durante a inicialização. Você pode fazer isso manualmente, o contêiner é apenas uma conveniência. O consumidor de uma fábrica não deve estar ciente do contêiner de IoC.
AlexFoxGill

5
Re: "E você não precisa de fábrica para poder criar uma simulação para teste. Você apenas instancia e passa a simulação diretamente" - novamente, circunstâncias diferentes. Você usa uma fábrica para solicitar uma instância - o consumidor está no controle dessa interação. Fornecer uma instância por meio do construtor ou de um método não funciona, é um cenário diferente.
AlexFoxGill

79

A moda do padrão Factory deriva de uma crença quase dogmática entre codificadores em linguagens "C-style" (C / C ++, C #, Java) de que o uso da palavra-chave "new" é ruim e deve ser evitado a todo custo (ou pelo menos menos centralizado). Por sua vez, isso provém de uma interpretação ultra-estrita do Princípio de Responsabilidade Única (o "S" do SOLID) e também do Princípio de Inversão da Dependência (o "D"). Em termos simples, o SRP diz que, idealmente, um objeto de código deve ter um "motivo para mudar" e apenas um; esse "motivo para mudar" é o objetivo central desse objeto, sua "responsabilidade" na base de código e qualquer outra coisa que exija uma alteração no código não deve exigir a abertura desse arquivo de classe. O DIP é ainda mais simples; um objeto de código nunca deve depender de outro objeto concreto,

Caso em questão, usando "new" e um construtor público, você está acoplando o código de chamada a um método de construção específico de uma classe concreta específica. Seu código agora precisa saber que existe uma classe MyFooObject e possui um construtor que aceita uma string e um int. Se esse construtor precisar de mais informações, todos os usos do construtor precisam ser atualizados para passar essas informações, incluindo a que você está escrevendo agora, e, portanto, eles precisam ter algo válido para passar e, portanto, devem ter ou seja alterado para obtê-lo (adicionando mais responsabilidades aos objetos de consumo). Além disso, se o MyFooObject for substituído na Base de Código pelo BetterFooObject, todos os usos da classe antiga precisarão ser alterados para construir o novo objeto em vez do antigo.

Portanto, todos os consumidores do MyFooObject devem depender diretamente do "IFooObject", que define o comportamento da implementação de classes, incluindo o MyFooObject. Agora, os consumidores do IFooObjects não podem apenas construir um IFooObject (sem ter o conhecimento de que uma classe concreta específica é um IFooObject, da qual eles não precisam); portanto, eles devem receber uma instância de uma classe ou método de implementação do IFooObject do lado de fora, por outro objeto que tem a responsabilidade de saber como criar o IFooObject correto para a circunstância, que em nossa linguagem é geralmente conhecida como Fábrica.

Agora, é aqui que a teoria encontra a realidade; um objeto nunca pode ser fechado para todos os tipos de alterações o tempo todo. Caso em questão, o IFooObject agora é um objeto de código adicional na base de código, que deve mudar sempre que a interface exigida pelos consumidores ou as implementações do IFooObjects mudam. Isso introduz um novo nível de complexidade envolvido na alteração da maneira como os objetos interagem entre si nessa abstração. Além disso, os consumidores ainda terão que mudar, e mais profundamente, se a própria interface for substituída por uma nova.

Um bom codificador sabe como equilibrar o YAGNI ("Você não precisará dele") com o SOLID, analisando o design e encontrando locais que provavelmente terão que mudar de uma maneira específica, e refatorando-os para serem mais tolerantes a que tipo de mudança, porque, nesse caso, "você está indo precisar dele".


21
Adore esta resposta, em especial depois de ler todas as outras. Posso acrescentar que quase todos (bons) novos codificadores são dogmáticos demais sobre os princípios pelo simples fato de que realmente querem ser bons, mas ainda não aprenderam o valor de manter as coisas simples e não exagerar.
jean

1
Outro problema com os construtores públicos é que não há uma maneira legal de uma classe Fooespecificar que um construtor público possa ser usado para a criação de Fooinstâncias ou a criação de outros tipos dentro do mesmo pacote / montagem , mas não deve ser usado para a criação de tipos derivados em outros lugares. Não conheço nenhum motivo particularmente atraente que uma linguagem / estrutura não possa definir construtores separados para uso em newexpressões versus invocação de construtores de subtipo, mas não conheço nenhuma linguagem que faça essa distinção.
Supercat 11/02

1
Um construtor protegido e / ou interno seria esse sinal; esse construtor só estaria disponível para consumir código, seja em uma subclasse ou dentro do mesmo assembly. O C # não possui uma combinação de palavras-chave para "protegido e interno", significando que apenas os subtipos dentro do assembly poderiam usá-lo, mas o MSIL possui um identificador de escopo para esse tipo de visibilidade, portanto é possível que a especificação do C # possa ser estendida para fornecer uma maneira de utilizá-lo. . Mas isso realmente não tem muito a ver com o uso de fábricas (a menos que você use a restrição de visibilidade para reforçar o uso de uma fábrica).
Keiths

2
Resposta perfeita. Bem no ponto com a parte "teoria encontra a realidade". Basta pensar em quantos milhares de desenvolvedores e tempo humano gastaram elogios ao culto de carga, justificando, implementando, usando-os para depois cair na mesma situação que você descreveu, é simplesmente enlouquecedor. Após YAGNI, Ive nunca identificou a necessidade de implementar uma fábrica
Breno Salgado

Estou programando em uma base de código onde, porque a implementação do OOP não permite sobrecarregar o construtor, o uso de construtores públicos é efetivamente banido. Esse já é um problema menor nos idiomas que o permitem. Como criar temperatura. Fahrenheit e Celsius são carros alegóricos. No entanto, você pode encaixotá-las e resolver o problema.
jgmjgm

33

Os construtores são bons quando contêm código curto e simples.

Quando a inicialização se torna mais do que atribuir algumas variáveis ​​aos campos, uma fábrica faz sentido. Aqui estão alguns dos benefícios:

  • Código longo e complicado faz mais sentido em uma classe dedicada (uma fábrica). Se o mesmo código for colocado em um construtor que chama vários métodos estáticos, isso poluirá a classe principal.

  • Em alguns idiomas e em alguns casos, lançar exceções em construtores é uma péssima idéia , pois pode introduzir bugs.

  • Quando você invoca um construtor, você, o responsável pela chamada, precisa saber o tipo exato da instância que deseja criar. Nem sempre é esse o caso (como um Feeder, eu só preciso construir o Animalpara alimentá-lo; não me importo se é um Dogou um Cat).


2
As opções não são apenas "fábrica" ​​ou "construtor". Um Feederpode usar nenhum e, em vez disso, chamar Kennelo getHungryAnimalmétodo do objeto .
DougM 14/08/14

4
+1 Acho que aderir a uma regra de absolutamente nenhuma lógica em um construtor não é uma má idéia. Um construtor deve ser usado apenas para definir o estado inicial do objeto, atribuindo seus valores de argumentos a variáveis ​​de instância. Se algo mais complicado for necessário, faça pelo menos um método de fábrica (classe) para criar a instância.
KaptajnKold

Esta é a única resposta satisfatória que eu vi aqui, me salvou de ter que escrever a minha. As outras respostas estão tratando apenas de conceitos abstratos.
TheCatWhisperer 17/03/2019

Mas esse argumento também pode ser válido Builder Patterncomo verdadeiro . Não é?
Soufrk 26/09

Regra dos Construtores
Breno Salgado

10

Se você trabalha com interfaces, pode permanecer independente da implementação real. Uma fábrica pode ser configurada (por meio de propriedades, parâmetros ou algum outro método) para instanciar uma dentre várias implementações diferentes.

Um exemplo simples: você deseja se comunicar com um dispositivo, mas não sabe se será via Ethernet, COM ou USB. Você define uma interface e três implementações. Em tempo de execução, você pode selecionar qual método deseja e a fábrica fornecerá a implementação apropriada.

Use-o frequentemente ...


5
Eu acrescentaria que é ótimo usar quando há várias implementações de uma interface e o código de chamada não sabe ou não deve saber qual escolher. Mas quando um método de fábrica é simplesmente um invólucro estático em torno de um único construtor, como na pergunta, esse é o antipadrão. Não tem que ser múltiplas implementações a partir do qual a escolher ou a fábrica está ficando no caminho e adicionando complexidade desnecessária.

Agora você tem a flexibilidade adicional e, em teoria, seu aplicativo pode usar Ethernet, COM, USB e serial, está pronto para o fireloop ou o que seja, em teoria. Na realidade, seu aplicativo só se comunicará pela Ethernet ... sempre.
Pieter B

7

É um sintoma de uma limitação nos sistemas de módulos do Java / C #.

Em princípio, não há razão para que você não consiga trocar uma implementação de uma classe por outra com as mesmas assinaturas de construtor e método. Não são linguagens que permitem isso. No entanto, Java e C # insistem em que cada classe tenha um identificador exclusivo (o nome completo) e o código do cliente acaba com uma dependência embutida nele.

Você pode resolver isso ao mexer nas opções do sistema de arquivos e do compilador para que com.example.Foomapeie para um arquivo diferente, mas isso é surpreendente e não intuitivo. Mesmo se você fizer isso, seu código ainda estará vinculado a apenas uma implementação da classe. Ou seja, se você escrever uma classe Fooque depende de uma classe MySet, poderá escolher uma implementação MySetem tempo de compilação, mas ainda não poderá instanciar Foos usando duas implementações diferentes de MySet.

Essa infeliz decisão de design obriga as pessoas a usar interfaces desnecessariamente para proteger seu código no futuro contra a possibilidade de que mais tarde precisem de uma implementação diferente de algo ou para facilitar o teste de unidade. Isso nem sempre é viável; se você tiver algum método que observe os campos particulares de duas instâncias da classe, não poderá implementá-lo em uma interface. É por isso que, por exemplo, você não vê unionna Setinterface do Java . Ainda assim, fora dos tipos e coleções numéricos, os métodos binários não são comuns; portanto, você geralmente pode se safar.

Obviamente, se você ligar, new Foo(...)ainda terá uma dependência da classe ; portanto, precisará de uma fábrica se desejar que uma classe possa instanciar uma interface diretamente. No entanto, geralmente é uma idéia melhor aceitar a instância no construtor e permitir que outra pessoa decida qual implementação usar.

Cabe a você decidir se vale a pena inchar sua base de código com interfaces e fábricas. Por um lado, se a classe em questão for interna da sua base de código, refatorar o código para que ele use uma classe ou interface diferente no futuro é trivial; você pode chamar YAGNI e refatorar mais tarde, se a situação surgir. Mas se a classe fizer parte da API pública de uma biblioteca que você publicou, você não terá a opção de corrigir o código do cliente. Se você não usar um interfacee mais tarde precisar de várias implementações, ficará preso entre uma rocha e um lugar difícil.


2
Eu gostaria que Java e derivados como o .NET tivessem uma sintaxe especial para uma classe criando instâncias de si mesmos e, de outra forma, newsimplesmente fossem açúcar sintático para chamar um método estático com nome especial (que seria gerado automaticamente se um tipo tivesse um "construtor público "mas não incluiu explicitamente o método). IMHO, se o código deseja apenas uma coisa padrão chata que implementa List, deve ser possível que a interface dê uma, sem que o cliente precise conhecer alguma implementação específica (por exemplo ArrayList).
Supercat

2

Na minha opinião, eles estão apenas usando o Simple Factory, que não é um padrão de design adequado e não deve ser confundido com o Abstract Factory ou o Factory Method.

E como eles criaram uma "enorme classe de malhas com milhares de métodos estáticos CreateXXX", isso soa como um antipadrão (talvez uma classe de Deus?).

Eu acho que os métodos Simple Factory e estático criador (que não exigem uma classe externa) podem ser úteis em alguns casos. Por exemplo, quando a construção de um objeto requer várias etapas, como instanciar outros objetos (por exemplo, favorecendo a composição).

Eu nem chamaria isso de Factory, mas apenas um monte de métodos encapsulados em alguma classe aleatória com o sufixo "Factory".


1
A Simple Factory tem seu lugar. Imagine que uma entidade use dois parâmetros construtores int xe IFooService fooService. Você não quer estar circulando por fooServicetoda parte, então cria uma fábrica com um método Create(int x)e injeta o serviço dentro da fábrica.
AlexFoxGill

4
@ AlexG E então, você tem que passar por IFactorytoda parte, em vez de IFooService.
Euphoric

3
Eu concordo com eufórico; na minha experiência, os objetos que são injetados em um gráfico a partir do topo tendem a ser do tipo de que todos os objetos de nível inferior precisam; portanto, passar o IFooServices por aí não é grande coisa. Substituir uma abstração por outra não realiza nada, mas ofusca ainda mais a base de código.
Keiths

isso parece totalmente irrelevante para a pergunta: "Por que devo usar uma classe factory em vez da construção direta de objetos? Quando devo usar construtores públicos e quando devo usar fábricas?" Veja como responder
gnat

1
Esse é apenas o título da pergunta, acho que você perdeu o resto. Veja o último ponto do link fornecido;).
21416 FranMowinckel

1

Como usuário de uma biblioteca, se a biblioteca tiver métodos de fábrica, você deverá usá-los. Você presumiria que o método factory fornece ao autor da biblioteca a flexibilidade de fazer determinadas alterações sem afetar seu código. Eles podem, por exemplo, retornar uma instância de alguma subclasse em um método de fábrica, que não funcionaria com um construtor simples.

Como criador de uma biblioteca, você usaria métodos de fábrica se quiser usar essa flexibilidade por conta própria.

No caso descrito, você parece ter a impressão de que substituir os construtores por métodos de fábrica era inútil. Certamente foi uma dor para todos os envolvidos; uma biblioteca não deve remover nada de sua API sem uma boa razão. Portanto, se eu tivesse adicionado métodos de fábrica, deixaria os construtores existentes disponíveis, talvez preteridos, até que um método de fábrica não chamasse mais esse construtor e o código usando o construtor simples funcionasse menos do que deveria. Sua impressão pode muito bem estar certa.


Observe também; Se o desenvolvedor precisar fornecer uma classe derivada com funcionalidade extra (propriedades extras, inicialização), o desenvolvedor poderá fazê-lo. (supondo que os métodos de fábrica sejam substituíveis). O autor da API também pode fornecer uma solução alternativa para necessidades especiais do cliente.
Eric Schneider

-4

Isso parece obsoleto na era do Scala e na programação funcional. Uma base sólida de funções substitui um gazilhão de classes.

Observe também que o duplo do Java {{não funciona mais ao usar uma fábrica, ou seja

someFunction(new someObject() {{
    setSomeParam(...);
    etc..
}})

O que pode permitir que você crie uma classe anônima e a personalize.

No dilema do espaço temporal, o fator tempo é agora muito reduzido, graças às CPUs rápidas, que permitem a programação funcional diminuir o espaço, ou seja, o tamanho do código.


2
este parece mais com um comentário tangencial (veja Como responder ), e ele não parece oferecer nada substancial sobre pontos apresentados e explicados na prévia 9 respostas
mosquito
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.