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

142

Eu vi a história de vários projetos de bibliotecas de classes С e Java no GitHub e no CodePlex, e vejo uma tendência de mudar para classes de fábrica em oposição à instanciação direta de objetos.

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

Por que tal mudança seria útil? Qual é o ponto 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?

    
por rufanov 14.08.2014 / 05:27
fonte

9 respostas

52

As classes de fábrica geralmente são implementadas porque permitem que o projeto siga os princípios 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 flexibilidade a longo prazo. Permite um design mais dissociado - e, portanto, mais testável. Aqui está uma lista não exaustiva de por que você pode seguir esse caminho:

  • Ele permite que você insira um contêiner Inversion of Control (IoC) facilmente
  • Torna seu código mais testável, já que você pode simular interfaces
  • Ele oferece muito mais flexibilidade na hora de alterar o aplicativo (por exemplo, você pode criar novas implementações sem alterar o código dependente)

Considere esta situação.

Montagem A (- > significa depende de):

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

Eu quero mover a Classe B para a Montagem B, que é dependente da Montagem A. Com essas dependências concretas, preciso mover toda a hierarquia de classes inteira. 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 dor alguma. Ainda depende das interfaces no assembly A.

Usar um contêiner 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.

Seguindo o princípio de segregação de interface e o princípio de inversão de dependência, podemos construir aplicativos altamente flexíveis e desacoplados. Depois de trabalhar em um desses tipos de aplicativos, você nunca mais desejará voltar a usar a palavra-chave new .

    
por 14.08.2014 / 05:51
fonte
111

Como whatsisname disse, creio que este é o caso do projeto de software cult . Fábricas, especialmente o tipo abstrato, são utilizáveis apenas quando o módulo cria várias instâncias de uma classe e você deseja fornecer ao usuário a capacidade desse módulo de especificar o tipo a ser criado. Esse requisito é realmente muito raro, porque na maioria das vezes você precisa apenas de uma instância e pode simplesmente passar essa instância 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 por isso as pessoas usam muito, mesmo em lugares onde não são necessárias. Então, quando o programador pensa "Quais 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 se tornam obsoletas quando você introduz o framework IoC, porque o IoC é apenas um tipo de fábrica. E muitas estruturas 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 para criar grandes classes estáticas com métodos CreateXXX que só chamam construtores. E especialmente não é chamado Fábrica (nem Fábrica Abstrata).

    
por 14.08.2014 / 08:45
fonte
66

A moda do padrão Factory deriva de uma crença quase dogmática entre codificadores em linguagens "C-style" (C / C ++, C #, Java) que o uso da "nova" palavra-chave é ruim e deve ser evitado a todo custo (ou pelo menos centralizado). Isto, por sua vez, vem de uma interpretação ultra-rigorosa do Princípio da Responsabilidade Única (o "S" do SOLID), e também do Princípio da 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 um só; essa "razão 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, mas sim de uma abstração.

Por exemplo, 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 tem que saber que existe uma classe MyFooObject, e tem um construtor que leva uma string e um int. Se esse construtor precisar de mais informações, todas as utilizações do construtor precisam ser atualizadas para passar essas informações, incluindo aquelas que você está escrevendo agora, e, portanto, elas precisam ter algo válido para serem transmitidas e, portanto, elas devem ter ou ser alterado para obtê-lo (adicionando mais responsabilidades aos objetos consumidores). Além disso, se MyFooObject for substituído na base de código por BetterFooObject, todos os usos da classe antiga terão que ser alterados para construir o novo objeto em vez do antigo.

Assim, ao invés disso, todos os consumidores de MyFooObject devem ser diretamente dependentes de "IFooObject", que define o comportamento de implementar classes incluindo MyFooObject. Agora, os consumidores de IFooObjects não podem apenas construir um IFooObject (sem ter o conhecimento de que uma determinada classe concreta é um IFooObject, que eles não precisam), então eles devem receber uma instância de um método ou classe de implementação IFooObject. de fora, por outro objeto que tem a responsabilidade de saber como criar o IFooObject correto para a circunstância, que em nosso jargão é geralmente conhecido como Factory.

Agora, aqui é onde a teoria encontra a realidade; um objeto pode nunca ser fechado para todos os tipos de mudança o tempo todo. Caso em questã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 implementações de IFooObjects for alterada. Isso introduz um novo nível de complexidade envolvido na mudança do modo como os objetos interagem entre si através dessa 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 balancear o YAGNI ("Você não vai precisar disso") com o SOLID, analisando o design e localizando lugares que provavelmente terão que mudar de uma maneira particular, e refatorando-os para serem mais tolerante a esse tipo de mudança, porque nesse caso "você é vai precisar dele".

    
por 14.08.2014 / 18:13
fonte
29

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

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 chame vários métodos estáticos, isso irá poluir a classe principal.

  • Em alguns idiomas e alguns casos, lançar exceções em construtores é uma ideia muito ruim , já que pode introduzir erros.

  • Quando você chama um construtor, você, o chamador, precisa saber o tipo exato da instância que deseja criar. Isso nem sempre é o caso (como Feeder , eu só preciso construir o Animal para alimentá-lo; não importa se é um Dog ou um Cat ).

por 14.08.2014 / 05:40
fonte
9

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

Um exemplo simples: você quer 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 o método desejado e a fábrica fornecerá a implementação apropriada.

Use-o com frequência ...

    
por 14.08.2014 / 07:41
fonte
6

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

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

Você pode classificar contornar isso mexendo nas opções do sistema de arquivos e do compilador para que com.example.Foo seja mapeado 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 Foo que depende de uma classe MySet , poderá escolher uma implementação de MySet no tempo de compilação, mas ainda não é possível instanciar Foo s usando duas implementações diferentes de MySet . / p>

Esta infeliz decisão de projeto obriga as pessoas a usar interface s desnecessariamente para testar seu código com a possibilidade futura de precisarem de uma implementação diferente de algo ou de facilitar o teste de unidade. Isso nem sempre é viável; Se você tiver algum método que examine os campos particulares de duas instâncias da classe, não poderá implementá-los em uma interface. É por isso que, por exemplo, você não vê union na interface Set do Java. Ainda assim, fora dos tipos e colecções numéricos, os métodos binários não são comuns, pelo que normalmente pode sair impune.

Claro, se você chamar new Foo(...) você ainda tem uma dependência na classe , então você precisa de uma fábrica se quiser que uma classe instancie uma interface diretamente. No entanto, geralmente é uma ideia melhor aceitar a instância no construtor e deixar 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 é interna à sua base de código, refatorar o código para que ele use uma classe diferente ou uma interface no futuro é trivial; você poderia invocar YAGNI e refatorar mais tarde se a situação surgisse. Mas se a classe fizer parte da API pública de uma biblioteca que você publicou, não terá a opção de corrigir o código do cliente. Se você não usar um interface e depois precisar de várias implementações, ficará preso entre uma rocha e um local difícil.

    
por 14.08.2014 / 13:54
fonte
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 malha 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 static creator (que não requerem 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 não chamaria nem mesmo chamar isso de Factory, mas apenas um monte de métodos encapsulados em alguma classe aleatória com o sufixo "Factory".

    
por 14.08.2014 / 14:09
fonte
1

Como usuário de uma biblioteca, se a biblioteca tiver métodos de fábrica, você deverá usá-los. Você poderia supor que o método de fábrica fornece ao autor da biblioteca a flexibilidade de fazer certas 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, o 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 que você descreve, parece ter a impressão de que substituir os construtores por métodos de fábrica era inútil. Foi certamente uma dor para todos os envolvidos; uma biblioteca não deve remover nada da API sem um bom motivo. Então, se eu tivesse adicionado métodos de fábrica, teria deixado os construtores existentes disponíveis, talvez preteridos, até que um método de fábrica não chame mais esse construtor e o código usando o construtor simples funcione menos do que deveria . Sua impressão pode muito bem estar certa.

    
por 29.02.2016 / 09:53
fonte
-4

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

Observe também que o dobro {{ do Java não funciona mais quando se usa uma fábrica, por exemplo

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

Que permite criar uma classe anônima e personalizá-la.

No dilema do espaço de tempo, o fator tempo agora está muito reduzido graças às CPUs rápidas que a programação funcional que permite o encolhimento de espaço, como o tamanho do código, é agora prática.

    
por 07.03.2016 / 21:41
fonte