Existe uma coisa no C ++ que me deixa desconfortável há muito tempo, porque sinceramente não sei como fazê-lo, mesmo que pareça simples:
Como implementar corretamente o método de fábrica em C ++?
Objetivo: possibilitar ao cliente instanciar algum objeto usando métodos de fábrica em vez dos construtores do objeto, sem consequências inaceitáveis e um impacto no desempenho.
Por "Padrão de método de fábrica", refiro-me aos métodos de fábrica estáticos dentro de um objeto ou métodos definidos em outra classe, ou funções globais. Geralmente "o conceito de redirecionar a maneira normal de instanciação da classe X para qualquer outro lugar que não o construtor".
Deixe-me examinar algumas respostas possíveis em que pensei.
0) Não faça fábricas, faça construtores.
Parece bom (e de fato geralmente a melhor solução), mas não é um remédio geral. Primeiro, há casos em que a construção de objetos é uma tarefa complexa o suficiente para justificar sua extração para outra classe. Mas mesmo colocando esse fato de lado, mesmo para objetos simples, usando apenas construtores, muitas vezes não servem.
O exemplo mais simples que conheço é uma classe vetorial 2-D. Tão simples, mas complicado. Quero ser capaz de construí-lo a partir de coordenadas cartesianas e polares. Obviamente, não posso fazer:
struct Vec2 {
Vec2(float x, float y);
Vec2(float angle, float magnitude); // not a valid overload!
// ...
};
Minha maneira natural de pensar é então:
struct Vec2 {
static Vec2 fromLinear(float x, float y);
static Vec2 fromPolar(float angle, float magnitude);
// ...
};
O que, em vez de construtores, me leva ao uso de métodos estáticos de fábrica ... o que essencialmente significa que estou implementando o padrão de fábrica de alguma forma ("a classe se torna sua própria fábrica"). Isso parece bom (e seria adequado para esse caso em particular), mas falha em alguns casos, que vou descrever no ponto 2. Continue lendo.
outro caso: tentar sobrecarregar por dois typedefs opacos de alguma API (como GUIDs de domínios não relacionados, ou um GUID e um campo de bits), tipos semanticamente totalmente diferentes (sobrecargas válidas, na teoria - válidas), mas que na verdade acabam sendo as mesma coisa - como entradas não assinadas ou ponteiros nulos.
1) O Caminho Java
Java é simples, pois temos apenas objetos alocados dinamicamente. Fazer uma fábrica é tão trivial quanto:
class FooFactory {
public Foo createFooInSomeWay() {
// can be a static method as well,
// if we don't need the factory to provide its own object semantics
// and just serve as a group of methods
return new Foo(some, args);
}
}
No C ++, isso se traduz em:
class FooFactory {
public:
Foo* createFooInSomeWay() {
return new Foo(some, args);
}
};
Legal? Muitas vezes, de fato. Mas então - isso força o usuário a usar apenas alocação dinâmica. A alocação estática é o que torna o C ++ complexo, mas também é o que frequentemente o torna poderoso. Além disso, acredito que existem alguns destinos (palavra-chave: incorporado) que não permitem alocação dinâmica. E isso não implica que os usuários dessas plataformas gostem de escrever OOP limpo.
Enfim, deixe a filosofia de lado: no caso geral, não quero forçar os usuários da fábrica a serem restringidos à alocação dinâmica.
2) Retorno por valor
OK, então sabemos que 1) é legal quando queremos alocação dinâmica. Por que não adicionamos alocação estática além disso?
class FooFactory {
public:
Foo* createFooInSomeWay() {
return new Foo(some, args);
}
Foo createFooInSomeWay() {
return Foo(some, args);
}
};
O que? Não podemos sobrecarregar pelo tipo de retorno? Ah, claro que não podemos. Então, vamos mudar os nomes dos métodos para refletir isso. E sim, escrevi o exemplo de código inválido acima apenas para enfatizar o quanto não gosto da necessidade de alterar o nome do método, por exemplo, porque não podemos implementar um projeto de fábrica independente de idioma corretamente agora, pois precisamos alterar os nomes - e todo usuário desse código precisará se lembrar dessa diferença de implementação da especificação.
class FooFactory {
public:
Foo* createDynamicFooInSomeWay() {
return new Foo(some, args);
}
Foo createFooObjectInSomeWay() {
return Foo(some, args);
}
};
OK ... aí está. É feio, pois precisamos alterar o nome do método. É imperfeito, pois precisamos escrever o mesmo código duas vezes. Mas uma vez feito, ele funciona. Certo?
Bem, geralmente. Mas às vezes isso não acontece. Ao criar o Foo, na verdade, dependemos do compilador para fazer a otimização do valor de retorno para nós, porque o padrão C ++ é benevolente o suficiente para que os fornecedores do compilador não especifiquem quando o objeto será criado no local e quando será copiado ao retornar um objeto temporário por valor em C ++. Portanto, se é caro copiar o Foo, essa abordagem é arriscada.
E se Foo não for copiável? Bem, doh. ( Observe que no C ++ 17 com elision de cópia garantida, não ser copiável não é mais problema para o código acima )
Conclusão: Fazer uma fábrica retornando um objeto é realmente uma solução para alguns casos (como o vetor 2-D mencionado anteriormente), mas ainda não é um substituto geral para os construtores.
3) Construção bifásica
Outra coisa que alguém provavelmente sugeriria é separar a questão da alocação de objetos e sua inicialização. Isso geralmente resulta em código como este:
class Foo {
public:
Foo() {
// empty or almost empty
}
// ...
};
class FooFactory {
public:
void createFooInSomeWay(Foo& foo, some, args);
};
void clientCode() {
Foo staticFoo;
auto_ptr<Foo> dynamicFoo = new Foo();
FooFactory factory;
factory.createFooInSomeWay(&staticFoo);
factory.createFooInSomeWay(&dynamicFoo.get());
// ...
}
Pode-se pensar que funciona como um encanto. O único preço que pagamos em nosso código ...
Desde que eu escrevi tudo isso e deixei isso como o último, também devo não gostar. :) Por quê?
Primeiro de tudo ... Eu sinceramente não gosto do conceito de construção em duas fases e me sinto culpado quando o uso. Se eu projetar meus objetos com a asserção de que "se existe, está em estado válido", sinto que meu código é mais seguro e menos propenso a erros. Eu gosto assim.
Ter que abandonar essa convenção E alterar o design do meu objeto apenas com o objetivo de torná-lo fábrica é ... bem, pesado.
Sei que o exposto acima não convencerá muitas pessoas, então, deixe-me dar alguns argumentos mais sólidos. Usando construção em duas fases, você não pode:
- inicializar
const
ou referenciar variáveis de membro, - passar argumentos para construtores de classe base e construtores de objetos membros.
E provavelmente poderia haver mais algumas desvantagens nas quais não consigo pensar agora, e nem me sinto particularmente obrigado, pois os pontos acima mencionados já me convencem.
Portanto: nem mesmo perto de uma boa solução geral para implementar uma fábrica.
Conclusões:
Queremos ter uma maneira de instanciação de objetos que:
- permitir instanciação uniforme, independentemente da alocação,
- atribuir nomes diferentes e significativos aos métodos de construção (sem depender da sobrecarga por argumentos),
- não introduza um impacto significativo no desempenho e, de preferência, um impacto significativo no código, especialmente no lado do cliente,
- seja geral, como em: possível de ser introduzido para qualquer classe.
Acredito ter provado que as maneiras mencionadas não atendem a esses requisitos.
Alguma dica? Por favor, me forneça uma solução, não quero pensar que essa linguagem não me permita implementar adequadamente um conceito tão trivial.
delete
isso. Esse tipo de método é perfeitamente adequado, desde que seja "documentado" (o código-fonte é a documentação ;-)) que o chamador se apropria do ponteiro (leia-se: é responsável por excluí-lo quando apropriado).unique_ptr<T>
vez deT*
.Respostas:
Eu acredito que este ponto está incorreto. A complexidade realmente não importa. A relevância é o que faz. Se um objeto puder ser construído em uma etapa (não como no padrão do construtor), o construtor é o lugar certo para fazê-lo. Se você realmente precisar de outra classe para executar o trabalho, deve ser uma classe auxiliar usada pelo construtor de qualquer maneira.
Existe uma solução fácil para isso:
A única desvantagem é que parece um pouco detalhada:
Mas o bom é que você pode ver imediatamente o tipo de coordenada que está usando e, ao mesmo tempo, não precisa se preocupar em copiar. Se você deseja copiar, e é caro (como comprovado pela criação de perfil, é claro), convém usar algo como as classes compartilhadas do Qt para evitar a sobrecarga de cópia.
Quanto ao tipo de alocação, o principal motivo para usar o padrão de fábrica é geralmente o polimorfismo. Os construtores não podem ser virtuais e, mesmo que pudessem, não faria muito sentido. Ao usar a alocação estática ou de pilha, não é possível criar objetos de forma polimórfica, pois o compilador precisa saber o tamanho exato. Portanto, ele funciona apenas com ponteiros e referências. E retornar uma referência de uma fábrica também não funciona, porque, embora um objeto tecnicamente possa ser excluído por referência, ele pode ser bastante confuso e propenso a erros, consulte A prática de retornar uma variável de referência C ++ é ruim?por exemplo. Portanto, ponteiros são a única coisa que resta, e isso inclui ponteiros inteligentes. Em outras palavras, as fábricas são mais úteis quando usadas com alocação dinâmica, para que você possa fazer coisas como estas:
Em outros casos, as fábricas apenas ajudam a resolver problemas menores, como aqueles com sobrecargas que você mencionou. Seria bom se fosse possível usá-los de maneira uniforme, mas não dói muito o que provavelmente é impossível.
fonte
Exemplo simples de fábrica:
fonte
unique_ptr
nesse exemplo não tem sobrecarga de desempenho. O gerenciamento de recursos, incluindo memória, é uma das vantagens supremas do C ++ sobre qualquer outra linguagem, porque você pode fazê-lo sem penalidade de desempenho e deterministicamente, sem perder o controle, mas diz exatamente o contrário. Algumas pessoas não gostam do que o C ++ implica implicitamente, como o gerenciamento de memória por meio de ponteiros inteligentes, mas se o que você deseja é que tudo seja obrigatoriamente explícito, use C; o tradeoff é ordens de magnitude menos problemas. Acho injusto você rejeitar uma boa recomendação.boost::ptr_vector<>
é um pouco mais eficiente, pois entende que possui o ponteiro, em vez de delegar o trabalho a uma subclasse. Mas a principal vantagemboost::ptr_vector<>
é que ela expõe seus membros por referência (não por ponteiro), portanto, é realmente fácil de usar com algoritmos na biblioteca padrão.Você já pensou em não usar uma fábrica e fazer bom uso do sistema de tipos? Eu posso pensar em duas abordagens diferentes que fazem esse tipo de coisa:
Opção 1:
O que permite escrever coisas como:
Opção 2:
você pode usar "tags" como o STL faz com os iteradores e outros. Por exemplo:
Essa segunda abordagem permite escrever um código parecido com este:
o que também é bom e expressivo, permitindo que você tenha protótipos exclusivos para cada construtor.
fonte
Você pode ler uma solução muito boa em: http://www.codeproject.com/Articles/363338/Factory-Pattern-in-Cplusplus
A melhor solução é nos "comentários e discussões", consulte "Não há necessidade de métodos estáticos de criação".
A partir dessa idéia, eu fiz uma fábrica. Observe que estou usando o Qt, mas você pode alterar o QMap e o QString por equivalentes std.
Uso da amostra:
fonte
Concordo principalmente com a resposta aceita, mas há uma opção C ++ 11 que não foi abordada nas respostas existentes:
Exemplo:
Então você pode construir objetos na pilha:
Como subobjetos de outras coisas:
Ou alocado dinamicamente:
Quando devo usar isso?
Se, em um construtor público, não for possível fornecer inicializadores significativos para todos os membros da classe sem algum cálculo preliminar, eu poderia converter esse construtor em um método estático. O método estático executa os cálculos preliminares e, em seguida, retorna um resultado de valor por meio de um construtor privado, que apenas faz uma inicialização por membro.
Eu digo ' poder ' porque depende de qual abordagem fornece o código mais claro sem ser desnecessariamente ineficiente.
fonte
Loki tem um método de fábrica e uma fábrica abstrata . Ambos estão documentados (extensivamente) em Modern C ++ Design , por Andei Alexandrescu. O método de fábrica provavelmente está mais próximo do que você procura, embora ainda seja um pouco diferente (pelo menos se a memória servir, é necessário registrar um tipo antes que a fábrica possa criar objetos desse tipo).
fonte
Function
e as manipulações de tipo podem ser substituídas porstd::function
e<type_traits>
enquanto lambdas, threading, rvalue refs têm implicações que podem exigir alguns ajustes menores, não há substituição padrão para singletons de fábricas como ele os descreve.Não tento responder a todas as minhas perguntas, pois acredito que é muito amplo. Apenas algumas notas:
Essa classe é de fato um construtor , e não uma fábrica.
Então você poderá encapsular sua fábrica em um ponteiro inteligente. Eu acredito que assim você pode comer o seu bolo e comê-lo também.
Isso também elimina os problemas relacionados ao retorno por valor.
De fato. Todos os padrões de design têm suas limitações e desvantagens (específicas do idioma). Recomenda-se usá-los apenas quando eles ajudarem a resolver seu problema, não por eles mesmos.
Se você está buscando a implementação "perfeita" da fábrica, boa sorte.
fonte
Esta é a minha solução de estilo c ++ 11. O parâmetro 'base' é para a classe base de todas as subclasses. criadores, são objetos std :: function para criar instâncias de subclasses, podem ser uma ligação à sua subclasse 'static member function' create (some args) '. Isso talvez não seja perfeito, mas funciona para mim. E é meio que uma solução 'geral'.
Um exemplo de uso.
fonte
Padrão de fábrica
E se o compilador não oferecer suporte à Otimização do Valor de Retorno, evite-o, provavelmente não conterá muita otimização ...
fonte
Factory
é que é bastante genérico e cobre muito terreno; uma fábrica pode adicionar argumentos (dependendo do ambiente / configuração) ou fornecer algum cache (relacionado ao Flyweight / Pools), por exemplo, mas esses casos só fazem sentido em algumas situações.Sei que esta pergunta foi respondida há 3 anos, mas isso pode ser o que você estava procurando.
O Google lançou há duas semanas uma biblioteca que permite alocações dinâmicas fáceis e flexíveis de objetos. Aqui está: http://google-opensource.blogspot.fr/2014/01/introducing-infact-library.html
fonte