Object Factories

Introdução

Os processos de abstracção e modularidade em programação orientada a objectos, em particular em C++, são facilmente conseguidos através dos conceitos de herança, polimorfismo e métodos virtuais. Na verdade o sistema em runtime é capaz de “despachar” métodos virtuais para os correctos objectos derivados, conseguindo assim executar o código que pretendemos em cada um dos instantes. A literatura referente à programação orientada a objectos é basta em exemplos.

Geralmente quando utilizamos este tipo de técnicas encontramos-nos num estado em que os objectos já estão criados, e dessa forma mantemos referências ou ponteiros que nos servirão para invocar o(s) método(s) desejados.

No entanto um problema poderá existir quando queremos usufruir das vantagens oferecidas pela herança e polimorfismo durante a criação de objectos. Este problema leva-nos geralmente ao paradoxo de “construtores virtuais”!

Para melhor explicar o descrito, tome-se o seguinte pedaço de código:

class Base { ... };
class Derived : public Base { ... };
class AnotherDerived : public Base { ... };
...
// criação de um objecto do tipo Derived, atribuindo-o a um ponteiro do //tipo Base
Base* pB = new Derived;

Repare-se que se quisermos criar um objecto do tipo AnotherDerived, teríamos que alterar a última linha e colocar new AnotherDerived. É impossível ter mais dinamismo com o operador new. Para o conseguirmos teríamos de lhe passar o tipo que queremos construir sendo que tinha de ser conhecido no momento da compilação da aplicação, sendo impossível defini-lo no momento em que o programa se encontra a ser executado.

Assim, verificamos que a criação de um objecto é um processo completamente diferente do processo de invocar um método virtual num objecto previamente construído. O problema descrito tem particular interesse quando o nosso programa quer construir objectos em tempo de execução dos quais nós não sabemos no momento da compilação qual será o seu tipo.

O que desejávamos, para resolver o problema anterior, era que o seguinte código pudesse ser escrito em C++ da seguinte forma (impossível com todos sabemos!).

Class theClass = Read(fileName);  
 
Document* pDoc = new theClass;  

Uma possível solução para este problema é a utilização de Object Factories, uma técnica que abordarei de forma resumida no restante artigo.

Object Factories

Para explicar o funcionamento de uma Object Factory vamos utilizar o típico exemplo das figuras geométricas, assim:

class Shape
{
public:
   virtual void Draw() const = 0;
   virtual void Rotate(double angle) = 0;
   virtual void Zoom(double zoomFactor) = 0;
   ...
}; 

Em conjunto com a classe Shape, podemos depois ter uma classe Drawing que contém uma lista de Shapes e que servirá para os manipular. A classe Drawing terá entre outros métodos o Drawing::Save e o Drawing::Load.

class Drawing
{
public:
   void Save(std::ofstream& outFile);
   void Load(std::ifstream& inFile);
   ...
};
void Drawing::Save(std::ofstream& outFile)
{
   write drawing header
   for (each element in the drawing)
   {
       (current element)->Save(outFile);
   }
}

Até este ponto não há qualquer problema. Quando pretendemos guardar um Circle podemos simplesmente colocar no ficheiro onde guardamos a informação de que se trata de um círculo. O problema existe no método de carregar um ficheiro e criar o Shape certo, isto é, no método Drawing::Load. Como é que eu crio o objecto dinamicamente? Apenas sei que é um Circle …

Uma solução simples, mas pouco elegante, é escrever o método Drawing::Load da seguinte forma:

// um ID único por tipo de shape
namespace DrawingType
{
//os ficheiros guardados têm um header onde é indicado o tipo de figura geométrica que representam
const int
   LINE = 1,
   POLYGON = 2,
   CIRCLE = 3
};
void Drawing::Load(std::ifstream& inFile)
{
   // error handling omitted for simplicity
   while (inFile)
   {
      // read object type
      int drawingType;
      inFile >> drawingType;
      // create a new empty object
      Shape* pCurrentObject;
      switch (drawingType)
      {
         using namespace DrawingType;
      case LINE:
         pCurrentObject = new Line;
         break;
      case POLYGON:
         pCurrentObject = new Polygon;
      ...

A implementação apresentada tem um problema grave. Cada vez que se queira introduzir um novo tipo de Shape, por exemplo um Circulo, obriga-nos a alterar o método Drawing::Load. Repare-se que apenas suportamos linhas e polígonos!

Quando se trata de componentes de software complexos é muito provável que a alteração, por pequena que seja, de código possa introduzir erros. Para elementos genéricos a utilização do código apresentado acima afigura-se como um grave problema!

A ideia para resolver este problema é transformar o switch numa única linha:

Shape* CreateConcreteShape();

Sendo que o método CreateConcreteShape saberá como criar o objecto pretendido de forma automática.

A solução será a nossa Factory manter uma associação entre o tipo de objectos (que poderá ser o seu ID) e um ponteiro para um método com a assinatura do apresentado acima, que tem como objectivo a criação do objecto específico que desejamos criar a determinada altura. Assim, é o próprio código do objecto que sabe como se deve “auto-criar” deixando a porta aberta para uma mais simples integração de novas funcionalidades.

A associação poderá ser conseguida com recurso a um std::map, em que a chave é o ID que identifica o tipo de objecto que desejamos construir. Na realidade o std:map fornece-nos a flexibilidade do switch com a possibilidade de ser aumentado durante a execução do programa. (Um map é uma estrutura definida na Standard Template Library)

O pedaço de código seguinte mostra o desenho da classe ShapeFactory que terá a responsabilidade de criar e gerir todos os objectos derivados de Shape:

class ShapeFactory
{
public:
   typedef Shape* (*CreateShapeCallback)();
private:
   typedef std::map<int, CreateShapeCallback> CallbackMap;
public:
   // retorna 'true' se o registo ocorreu sem problema
   bool RegisterShape(int ShapeId, CreateShapeCallback CreateFn); 
   bool UnregisterShape(int ShapeId);
   Shape* CreateShape(int ShapeId) {
   CallbackMap::const_iterator i = callbacks_.find(shapeId);
     if (i == callbacks_.end())
     {
          // Não foi encontrado
            throw std::runtime_error("Unknown Shape ID");
     }
   // Invocar o método de criação para o objecto ShapeId
     return (i->second)();
   } 
   static policy_factory* instance() {
        if(!my_instance)
           my_instance = new ShapeFactory;
         return my_instance;
   }
private:
   CallbackMap callbacks_;
   static policy_factory *my_instance ;
}; 

A classe guarda um mapa chamado callbacks_ e disponibiliza um método para registo do ID para cada objecto e do ponteiro para o método que deverá ser chamado no caso de queremos criar um objecto do tipo identificado pelo ID.

Há ainda um pormenor no que respeita ao desenho da classe factory. É de interesse mantermos a factory única para posteriormente cada objecto poder registar o ponteiro para o membro de criação. Esta característica é conseguida através do método static policy_factory* instance() que será utilizado para aceder à única instância existente da nossa factory.

Com esta abordagem estamos no fundo a dividir responsabilidades, uma vez que cada objecto (derivado de Shape) tem a responsabilidade de saber como deve ser criado e de efectivamente se criar.

Cada novo objecto derivado de Shape precisa apenas de criar um método que permita ser criado. Um exemplo é apresentado de seguida:

Shape* CreateLine()
{
   return new Line;
}  

Claro que o apresentado é apenas um exemplo e como tal um objecto pode ter um processo de criação muito mais complexo que o apresentado.

namespace
{
   Shape* CreateLine()
   {
      return new Line;
   }
   // O ID da classe Line
   const int LINE = 1;
   //registo do ID, como chave, e do ponteiro para o método de criação        //do objecto
   const bool registered = ShapeFactory::Instance().RegisterShape(       LINE, CreateLine);
}  

Para a criação de um objecto “on demand” bastará:

Shape* sh = ShapeFactory::instance()->CreateShape(LINE);

E um objecto do tipo Shape::Line é criado automaticamente.

Conclusão

Apresentou-se, neste artigo, uma pequena introdução a uma técnica de programação denominada por Object Factories. Com este tipo de técnica é dada mais liberdade e abstracção, facilitando a criação de objectos de tipos não conhecidos no momento de compilação, mas que sabemos derivarem de tipos bem conhecidos.

Resta referir que o apresentado, embora funcional, é apenas um pequeno exemplo e que para o bom funcionamento de uma Object Factory seria necessário o desenvolvimento de mais algum código de controlo.

Todo o código apresentado foi adaptado do livro Modern C++ Design: Generic Programming and Design Patterns Applied de Andrei Alexandrescu (2001).