9.11. Factorías: encapsular la creación de objetos

Cuando se descubre que se necesitan añadir nuevos tipos a un sistema, el primer paso más sensato es usar polimorfismo para crear una interfaz común para esos nuevos tipos. Así, se separa el resto del código en el sistema del conocimiento de los tipos específicos que se están añadiendo. Los tipos nuevos pueden añadirse sin "molestar" al código existente, o eso parece. A primera vista, podría parecer que hace falta cambiar el código únicamente en los lugares donde se hereda un tipo nuevo, pero esto no es del todo cierto. Todavía hay que crear un objeto de este nuevo tipo, y en el momento de la creación hay que especificar qué constructor usar. Por lo tanto, si el codigo que crea objetos está distribuido por toda la aplicación, se obtiene el mismo problema que cuando se añaden tipos -hay que localizar todos los puntos del código donde el tipo tiene importancia. Lo que imoporta es la creación del tipo, más que el uso del mismo (de eso se encarga el polimorfismo), pero el efecto es el mismo: añadir un nuevo tipo puede causar problemas.

La solución es forzar a que la creación de objetos se lleve a cabo a través de una factoría común, en lugar de permitir que el código creacional se disperse por el sistema. Si todo el código del programa debe ir a esta factoría cada vez que necesita crear uno de esos objetos, todo lo que hay que hacer para añadir un objeto es modificar la factoría. Este diseño es una variación del patrón conocido comúnmente como Factory Method. Dado que todo programa orientado a objetos crea objetos, y como es probable que haya que extender el programa añadiendo nuevos tipos, las factorías pueden ser el más útil de todos los patrones de diseño.

Como ejemplo, considere el ampliamente usado ejemplo de figura (Shape). Una aproximación para implementar una factoría es definir una función miembro estática en la clase base:

//: C10:ShapeFactory1.cpp
#include <iostream>
#include <stdexcept>
#include <cstddef>
#include <string>
#include <vector>
#include "../purge.h"
using namespace std;

class Shape {
public:
  virtual void draw() = 0;
  virtual void erase() = 0;
  virtual ~Shape() {}
  class BadShapeCreation : public logic_error {
  public:
    BadShapeCreation(string type)
    : logic_error("Cannot create type " + type) {}
  };
  static Shape* factory(const string& type)
    throw(BadShapeCreation);
};

class Circle : public Shape {
  Circle() {} // Private constructor
  friend class Shape;
public:
  void draw() { cout << "Circle::draw" << endl; }
  void erase() { cout << "Circle::erase" << endl; }
  ~Circle() { cout << "Circle::~Circle" << endl; }
};

class Square : public Shape {
  Square() {}
  friend class Shape;
public:
  void draw() { cout << "Square::draw" << endl; }
  void erase() { cout << "Square::erase" << endl; }
  ~Square() { cout << "Square::~Square" << endl; }
};

Shape* Shape::factory(const string& type)
  throw(Shape::BadShapeCreation) {
  if(type == "Circle") return new Circle;
  if(type == "Square") return new Square;
  throw BadShapeCreation(type);
}

char* sl[] = { "Circle", "Square", "Square",
  "Circle", "Circle", "Circle", "Square" };

int main() {
  vector<Shape*> shapes;
  try {
    for(size_t i = 0; i < sizeof sl / sizeof sl[0]; i++)
      shapes.push_back(Shape::factory(sl[i]));
  } catch(Shape::BadShapeCreation e) {
    cout << e.what() << endl;
    purge(shapes);
    return EXIT_FAILURE;
  }
  for(size_t i = 0; i < shapes.size(); i++) {
    shapes[i]->draw();
    shapes[i]->erase();
  }
  purge(shapes);
} ///:~

Listado 9.23. C10/ShapeFactory1.cpp


La función factory() toma un argumento que le permite determinar qué tipo de figura crear. Aquí, el argumento es una cadena, pero podría ser cualquier conjunto de datos. El método factory() es el único código del sistema que hay que cambiar cuando se añade un nuevo tipo de figura. (Los datos de inicialización para los objetos vendrán supuestamente de algún sitio fuera del sistema y no serán un FIXME: hard-coded array como en el ejemplo.)

Para asegurar que la creación sólo puede realizarse en factory(), los constructores de cada tipo específico de figura se hacen privados, y Shape se declara como friend de forma que factory() tiene acceso a los mismos. (También se podría declarar sólamente Shape::factory() como friend, pero parece razonablemente inocuo declarar la clase base entera.) Hay otra implicación importante de este diseño -la clase base, Shape, debe conocer ahora los detalles de todas las clases derivadas -una propiedad que el diseño orientado a objetos intenta evitar. Para frameworks o cualquier librería de clases que deban poder extenderse, esto hace que se convierta rápidamente en algo difícil de manejar, ya que la clase base debe actualizarse en cuanto se añada un tipo nuevo a la jerarquía. Las factorías polimórficas, descritas en la siguiente subsección, se pueden usar para evitar esta dependencia circular tan poco deseada.

9.11.1. Factorías polimórficas

La función estática factory() en el ejemplo anterior fuerza que las operaciones de creación se centren en un punto, de forma que sea el único sitio en el que haya que cambiar código. Esto es, sin duda, una solución razonable, ya que encapsula amablemente el proceso de crear objetos. Sin embargo, el GoF enfatiza que la razón de ser del patrón Factory Method es que diferentes tipos de factorías se puedan derivar de la factoría básica. Factory Method es, de hecho, un tipo especial de factoría polimórfica. Esto es ShapeFactory1.cpp modificado para que los Factory Methods estén en una clase aparte como funciones virtuales.

//: C10:ShapeFactory2.cpp
// Polymorphic Factory Methods.
#include <iostream>
#include <map>
#include <string>
#include <vector>
#include <stdexcept>
#include <cstddef>
#include "../purge.h"
using namespace std;

class Shape {
public:
  virtual void draw() = 0;
  virtual void erase() = 0;
  virtual ~Shape() {}
};

class ShapeFactory {
  virtual Shape* create() = 0;
  static map<string, ShapeFactory*> factories;
public:
  virtual ~ShapeFactory() {}
  friend class ShapeFactoryInitializer;
  class BadShapeCreation : public logic_error {
  public:
    BadShapeCreation(string type)
    : logic_error("Cannot create type " + type) {}
  };
  static Shape*
  createShape(const string& id) throw(BadShapeCreation) {
    if(factories.find(id) != factories.end())
      return factories[id]->create();
    else
      throw BadShapeCreation(id);
  }
};

// Define the static object:
map<string, ShapeFactory*> ShapeFactory::factories;

class Circle : public Shape {
  Circle() {} // Private constructor
  friend class ShapeFactoryInitializer;
  class Factory;
  friend class Factory;
  class Factory : public ShapeFactory {
  public:
    Shape* create() { return new Circle; }
    friend class ShapeFactoryInitializer;
  };
public:
  void draw() { cout << "Circle::draw" << endl; }
  void erase() { cout << "Circle::erase" << endl; }
  ~Circle() { cout << "Circle::~Circle" << endl; }
};

class Square : public Shape {
  Square() {}
  friend class ShapeFactoryInitializer;
  class Factory;
  friend class Factory;
  class Factory : public ShapeFactory {
  public:
    Shape* create() { return new Square; }
    friend class ShapeFactoryInitializer;
  };
public:
  void draw() { cout << "Square::draw" << endl; }
  void erase() { cout << "Square::erase" << endl; }
  ~Square() { cout << "Square::~Square" << endl; }
};

// Singleton to initialize the ShapeFactory:
class ShapeFactoryInitializer {
  static ShapeFactoryInitializer si;
  ShapeFactoryInitializer() {
    ShapeFactory::factories["Circle"]= new Circle::Factory;
    ShapeFactory::factories["Square"]= new Square::Factory;
  }
  ~ShapeFactoryInitializer() {
    map<string, ShapeFactory*>::iterator it =
      ShapeFactory::factories.begin();
    while(it != ShapeFactory::factories.end())
      delete it++->second;
  }
};

// Static member definition:
ShapeFactoryInitializer ShapeFactoryInitializer::si;

char* sl[] = { "Circle", "Square", "Square",
  "Circle", "Circle", "Circle", "Square" };

int main() {
  vector<Shape*> shapes;
  try {
    for(size_t i = 0; i < sizeof sl / sizeof sl[0]; i++)
      shapes.push_back(ShapeFactory::createShape(sl[i]));
  } catch(ShapeFactory::BadShapeCreation e) {
    cout << e.what() << endl;
    return EXIT_FAILURE;
  }
  for(size_t i = 0; i < shapes.size(); i++) {
    shapes[i]->draw();
    shapes[i]->erase();
  }
  purge(shapes);
} ///:~

Listado 9.24. C10/ShapeFactory2.cpp


Ahora, Factory Method aparece en su propia clase, ShapeFactory, como virtual create(). Es una función miembro privada, lo que significa que no puede ser llamada directametne, pero puede ser sobreescrita. Las subclases de Shape deben crear cada una su propias subclases de ShapeFactory y sobreescribir el método create para crear un objeto de su propio tipe. Estas factorías son privadas, de forma que sólo pueden ser accedidas desde el Factory Method principal. De esta forma, todo el código cliente debe pasar a través del Factory Method para crear objetos.

La verdadera creación de figuras se realiza llamando a ShapeFactory::createShape( ), que es una función estática que usa el mapa en ShapeFactory para encontrar la objeto factoría apropiado basándose en el identificador que se le pasa. La factoría crea el objeto figura directamente, pero podría imaginarse un problema más complejo en el que el objeto factoría apropiado se devuelve y luego lo usa quien lo ha llamado para crear un objeto de una manera más sofisticada. Sin embargo, parece que la mayoría del tiempo no hacen falta las complejidades del Factory Method polimórfico, y bastará con una única función estática en la clase base (como se muestra en ShapeFactory1.cpp).

Observe que el ShapeFactory debe ser inicializado cargando su mapa con objetos factory, lo que tiene lugar en el Singleton ShapeFactoryInitializer. Así que para añadir un nuevo tipo a este diseño debe definir el tipo, crear una factoría, y modificar ShapeFactoryInitializer para que se inserte una instancia de su factoría en el mapa. Esta complejidad extra, sugiere de nuevo el uso de un Factory Method estático si no necesita crear objetos factoría individuales.