Ferramentas de Site


dev_geral:cpp:object_factories

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 run-time é 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 encontramonos 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 Factories 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 circulo. 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 conseguída 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 the 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 é conseguída 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. Lembrar apenas que o registo na factory para o objecto Line já se encontra garantido como mostra o conteúdo do ficheiro [line.cpp]

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).

Autoria

Este artigo foi originalmente escrito por Ricardo Azevedo para a 12ª Edição da Revista PROGRAMAR

dev_geral/cpp/object_factories.txt · Última modificação em: 2021/12/11 23:43 (edição externa)