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 CreateXXX
mé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?
Respostas:
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:
Considere esta situação.
O conjunto A (-> significa depende):
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:
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
new
palavra - chave.fonte
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
CreateXXX
métodos que apenas chamam construtores. E, especialmente, não é chamada de Fábrica (nem Fábrica Abstrata).fonte
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".
fonte
Foo
especificar que um construtor público possa ser usado para a criação deFoo
instâ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 emnew
expressões versus invocação de construtores de subtipo, mas não conheço nenhuma linguagem que faça essa distinção.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 oAnimal
para alimentá-lo; não me importo se é umDog
ou umCat
).fonte
Feeder
pode usar nenhum e, em vez disso, chamarKennel
ogetHungryAnimal
método do objeto .Builder Pattern
como verdadeiro . Não é?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 ...
fonte
É 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.Foo
mapeie 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 classeFoo
que depende de uma classeMySet
, poderá escolher uma implementaçãoMySet
em tempo de compilação, mas ainda não poderá instanciarFoo
s usando duas implementações diferentes deMySet
.Essa infeliz decisão de design obriga as pessoas a usar
interface
s 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êunion
naSet
interface 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
interface
e mais tarde precisar de várias implementações, ficará preso entre uma rocha e um lugar difícil.fonte
new
simplesmente 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 implementaList
, deve ser possível que a interface dê uma, sem que o cliente precise conhecer alguma implementação específica (por exemploArrayList
).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".
fonte
int x
eIFooService fooService
. Você não quer estar circulando porfooService
toda parte, então cria uma fábrica com um métodoCreate(int x)
e injeta o serviço dentro da fábrica.IFactory
toda parte, em vez deIFooService
.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.
fonte
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 sejaO 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.
fonte