O objetivo é implementar uma infra-estrutura de servidor de aplicação, onde, mediante a requisição de um cliente, crie-se um objeto que trate os parâmetros e devolva uma resposta padronizada.
A requisição do cliente virá numa forma textual (XML ou coisa que o valha, então precisa-se interpretar essa requisição e criar o objeto correspondente. Como todos sabem, linguagens fortemente tipadas como C++, Object Pascal e outras do gênero não oferecem nenhum recurso para se criar classes mediante um nome.
O objetivo final é chegar a um front-end de servidor de aplicação simples e limpo como
tratador *ObtemTratador(int numero)
{
tratador *t = 0;
fabrica *c = 0;
c = ListaDeFabricas[numero];
if (c) {
t = c->fabricarTratador();
} else {
cerr >>nome << " não é uma regra de negócio válida." << endl;
}
return t;
}
Mesmo sendo um objetivo final, muitas coisas já podem ser inferidas:
Cada tratador é uma classe (poderão haver milhares de tratadores em uma aplicação complexa), mas todos serão descendentes da classe "tratador", para que as funções de front-end sejam as mais simples possíveis.
A interface de todos os tratadores deve seguir um padrão rígido (receber parâmetros e devolver resultados do mesmo modo) para ratificar o ponto acima.
As funções que usam e manipulam tratadores, como o exemplo acima, têm de usar ponteiros para "tratador", ao invés da classe em si. Por que ?
Essa é talvez a diferença mais gritante entre C++ e Object Pascal ou Java. Nas duas últimas, absolutamente toda variável que contenha um objeto, na verdade contém apenas um ponteiro - o objeto em si está no heap. (O pessoal do Java prefere chamar de "referência", mas dá no mesmo).
Já no C++, você pode ter objetos e ponteiros para objetos. Por exemplo, na simples função abaixo
void inutil()
{
string a;
string *b;
}
o objeto "a" será criado no início da função e destruído ao final, automaticamente. O compilador garante isso. Já o objeto "b" é bem ao estilo do Object Pascal - só existirá se for criado explicitamente com
b = new string("bla");
e também precisa ser explicitamente destruído com
delete b;
O problema é que o objeto "a", na maioria das implementações, será criado em pilha - ou seja, seu tamanho é 100% conhecido em tempo de compilação. O objeto "a" não poderá conter um descendente de string. Já no caso do ponteiro "b", o objeto real pode ter qualquer tamanho, e pode ser um descendente de string. Desde que os métodos de "string" reimplementados pelos descendentes sejam definidos como virtuais, o comportamento polimórfico será honrado.
Outra conseqüência muito interessante dos fatos acima, para quem vem do Delphi, é que objetos em C++ podem ser passados por VALOR ou REFERÊNCIA. Em Delphi ou Java, é sempre por referência. (Na linguagem C, tudo é passado por valor, mesmo estruturas. Originalmente, as estruturas eram passadas por referência, mas o ANSI C removeu essa exceção.)
Podíamos ir longe nessa discussão, mas vamos voltar ao nosso problema original.
- O objeto ListaDeFabricas é definido assim:
map<int, fabrica*> ListaDeFabricas;
O "map" é uma "template class" - classe-modelo - da STL. Em si, ele não é uma classe, e sim um modelo onde alguns tipos de dado não estão definidos. Quando o programador pede algo como
map<int, fabrica*>
ele está dizendo "quero um mapa onde o 'vetor' seja um inteor, e o valor armazenado seja um ponteiro para fabrica". Nesse momento, foi criada uma classe real (não mais um modelo) de mapa que usa esses tipos. Se você tiver paciência de analisar o arquivo /usr/include/g++/stl_map.h, verá como um modelo de classe pode ser implementado sem que se saiba de antemão alguns tipos.
Templates podem parecer algo realmente inútil para quem vem do mundo Delphi, mas eles ajudam muito em certas situações. É notório, por exemplo, o problema do TList do Delphi. Quem usa TList com objetos, tem duas opções:
a) usar "casts" a todo instante para transformar os ponteiros de TList em objetos; b) criar uma classe, derivada de TList, para CADA tipo de objeto, de forma que o compilador possa verificar o tipo, sem os malditos casts.
No C++, existe a classe-modelo List<class T>, que você instancia assim:
List<string> a;
Daí para frente, o objeto "a" aceitará e retornará apenas strings. Você não precisa mais usar casts e seu código sairá muito mais depurado da compilação.
"Mas se eu tiver 1000 classes e criar listas para todas elas, isso vai criar um monte de classes e um monte de código!". É verdade - templates podem causar muito code bloat. Mas, mas, normalmente listas e mapas armazenam PONTEIROS para objetos (tal como TList do Delphi) - e como ponteiros têm sempre o mesmo tamanho, a STL encarrega-se de fazer todas as classes List<classe*> funcionar como List<VOID*> na hora de realmente gerar o binário. Assim, não se perde em segurança de tipo e não se perde nada em performance. A STL está cheia dessas "especializações parciais" em suas classes-modelo.
Voltando ao fragmento de código original, vemos que "fabrica" também é um tipo-base, e que por certo haverá tantas fábricas quantos forem os tratadores. O motivo disso é que, pelo menos na maioria das linguagens fortemente tipadas, os construtores das classes não podem ser virtuais. Então, precisamos de uma classe intermediaria (a fábrica) que essa sim será polimórfica.
Bem, em algum ponto do programa terá de haver o seguinte fragmento de código:
ListaDeFabricas["tratador_1"] = new fabrica_do_tratador_1;
Idealmente, cada tratador ocupará um arquivo-fonte, e a instrução acima seria consignada em cada um desses arquivos - para evitar a necessidade de uma "lista geral de tratadores".
Em Object Pascal isso seria particularmente fácil, porque cada "Unit" pode ter uma sessão "initialization" e outra "finalization". Lá nós colocamos código que deve ser rodado antes do início e depois do final de cada programa. Mas isso não existe em C++, infelizmente ;((((
Para contornar essa limitação, utilizaremos um recurso que o C++ tem a mais que o Delphi: objetos globais criados pelo compilador. Nós já temos um objeto nessa situação (ListaDeFabricas). Observe:
class fabrica
{
fabrica(int numero) {
ListaDeFabricas[numero] = this;
}
virtual tratador *fabricarTratador() = 0;
};
...
class tratador_1: public tratador { ... };
class fabrica_do_tratador_1: public fabrica
{
public:
fabrica_do_tratador_1(int numero): fabrica(numero) {};
tratador *fabricarTratador() {
return new tratador_1();
}
};
static fabrica_do_tratador_1 f;
A última linha é a mais importante, pois indica ao compilador que, antes de o programa iniciar, "f" terá de ser criado. Ora, o processo de criação de "fabrica_do_tratador_1" implica na chamada do construtor do pai (que é "fabrica"). Olhe lá o que ele faz: adiciona a si mesmo na ListaDeFabricas !
Como "this" é um ponteiro e ListaDeFabricas armazena ponteiros também, o comportamento polimórfico é preservado - ou seja, quando o método fabricarTratador() for invocado, o que será chamado é fabrica_do_tratador_1::fabricarTratador().
Ok, resolvemos o problema do registro automático da classe junto à uma lista, mas restam um problema grave e uma pedra no sapato:
1) ListaDeFabricas e o objeto "f" são construídos pelo compliador. Mas em que ordem ? Se "f" for construído primeiro, a lista não estará pronta ainda, e o programa acabará mal.
Infelizmente, NÃO HÁ COMO definir a ordem de criação dos objetos globais. A saída é tornar ListaDeFabricas um ponteiro:
map<int, fabrica*> *ListaDeFabricas = 0;
e modificar ligeiramente o código de "fabrica":
fabrica(int numero) {
if (ListaDeFabricas == NULL) {
ListaDeFabricas = new map<int, tratador*>;
}
(*ListaDeFabricas)[nome] = this;
}
E a exemplo das funções acima, referências a ListaDeFabricas enquanto objeto devem ser trocadas para (*ListaDeFabricas), que dereferencia o ponteiro.
2) A mão-de-obra em se definir uma fábrica para cada tratador é incômoda.
A saída óbvia parece ser templates, porém há uma pegadinha - a ListaDeFabricas vai querer que todas as fábricas sejam descendentes de uma classe única (no caso, "fabrica"). E os templates NÃO TÊM RELAÇÃO DE PARENTESCO entre si.
Eis a saída. Mantém-se a classe-pai exatamente como está, e cria-se um template dessa forma:
template class<class T, int Int> Tfabrica: public fabrica
{
public:
Tfabrica(): fabrica(Int) {};
T *fabricarTratador() {
return new T();
}
};
A classe fabrica_para_tratador_1 deixa de existir. A variável estática global "f", definímo-la assim:
Tfabrica<tratador_1, 1> f;
O compilador se encarrega de gerar a classe a partir do template. Como ela é descendente de fabrica, o registro junto à ListaDeFabricas processa-se como dantes.
Talvez possa lhe parecer estranho usar um inteiro como parte da especificação de um template (Int) - e ainda passá-lo como parâmetro ao construtor. Os templates permitem essas coisas interessantes.
Sua única limitação no "standard" atual do C++ é não aceitar strings literais. Isso impediria, por exemplo, o uso do nosso truque acima se as fábricas fossem extraídas da ListaDeFabricas mediante uma string (sim, a classe-modelo map<> permite isso. Quem usa Perl vai gostar ;)