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 de T*
.