Creational Patterns

Table of contents:

By using creational patterns to manage object creation, we gain flexibility that makes it possible to choose or change the kinds of objects created or used at runtime, and also to manage object deletion automatically. Especially in large software systems, managing the creation of objects is important for flexibility in program design, maintaining a separation between layers of code and ensuring that objects are properly deleted when they are no longer needed.

In C++, a factory is a program component, generally a class, that is responsible for creating objects. The idea of a factory is to separate object creation from object usage.

A factory class generally has a function that obtains dynamic memory for the new object and returns a base class pointer to that object. This approach allows new derived types to be introduced without necessitating changes to the code that uses the created object.

We will discuss several design patterns that use factories and show examples of these design patterns.

When the responsibility for heap object creation is delegated to a virtual function, we call this a Factory method.

By making the factory method pure virtual and writing concrete derived factory classes, this becomes an abstract factory.

The Abstract Factory pattern provides an interface for defining factories that share abstract features but differ in concrete details. Client code can instantiate a particular subfactory and then use the abstract interface to create objects.

By imposing creation rules that prevent direct instantiation of a class, we can force clients to use factory methods to create all instances. When we combine creation rules with a factory method that always returns the same singleton object, this is an example of the Singleton pattern.

The Singleton pattern restricts a class so that only one instance can be created. This can be accomplished by making its constructor private or protected and providing an instance() function that creates a new instance if one does not already exist, but returns a pointer or reference to that instance if it does.

Consider two ways of creating a Customer:

Customer* c1 = new Customer(name);
Customer* c2 = CustomerFactory::instance()->newCustomer(name)

In the first case, we are hard-coding the class name and calling a constructor directly. The object will be created in memory using the default heap storage. Hard-coded class names in client code can limit the reusability of the code.

In the second case, we create a Customer object indirectly using a factory method called newCustomer(). In fact, newCustomer() is being called on a singleton CustomerFactory instance. CustomerFactory is a singleton because only one instance is available to us, via a static factory method, instance().

16.1.1. Abstract Factory

AbstractFactory, defined in Example 16.1, is a simple class.

Example 16.1. src/libs/dataobjects/abstractfactory.h

[ . . . . ]
class AbstractFactory {
 public:
 virtual DataObject* newObject (QString className) = 0;
 virtual ~AbstractFactory() {}
};
[ . . . . ]

The newObject() method is pure virtual, so it must be overridden in derived classes. Example 16.2 shows a concrete class derived from AbstractFactory.

Example 16.2. src/libs/dataobjects/objectfactory.h

[ . . . . ]
class ObjectFactory : public QObject, public AbstractFactory {
 Q_OBJECT
 public:
 static ObjectFactory* instance() ;
 virtual DataObject* newObject (QString className);
 Address* newAddress (Country::CountryType country =
 Country::Undefined);
 Address* newAddress (QString countryName = "USA");
 protected:
 ObjectFactory() {}
};
[ . . . . ]

ObjectFactory knows how to create a couple of concrete types. Since it is possible for any string to be supplied to newObject(), ObjectFactory handles the case when an unknown class is passed. This is where PropsMap comes into play. ObjectFactory creates a PropsMap if it does not recognize the given class name, as we see in Example 16.3. A PropsMap is a DataObject that holds property values in a hash table, Perl-style.

Example 16.3. src/libs/dataobjects/objectfactory.cpp

[ . . . . ]

DataObject* ObjectFactory::newObject(QString className) {
 DataObject* retval = 0;
 if (className == "UsAddress") {
 retval = newAddress(Country::USA);
 } else if (className == "CanadaAddress") {
 retval = newAddress(Country::Canada);
 } else {
 qDebug() << QString("Generic PropsMap created for new %1")
 .arg(className);
 retval = new PropsMap(className);
 retval->setParent(this); <-- 1
 }
 return retval;
}
 

(1)Initially set the parent of the new object to the factory.

newObject() returns the address of an object whose parent is initially set to an ObjectFactory singleton.[1] The parent can be changed later but this ensures that, by default, the object gets deleted with the factory if it's not managed by another object. This is an important measure to prevent memory leaks.

[1] newAddress() also sets the parent to the factory.

16.1.2. Abstract Factories and Libraries

We now discuss two libraries, libdataobjects and libcustomer, each with its own ObjectFactory, as shown in the UML diagram in Figure 16.1.

Figure 16.1. Libraries and factories

CustomerFactory, defined in Example 16.4, extends the functionality of ObjectFactory.

Example 16.4. src/libs/customer/customerfactory.h

[ . . . . ]
class CustomerFactory : public ObjectFactory {
 Q_OBJECT
 public:
 static CustomerFactory* instance();
 DataObject* newObject(QString classname);
 Customer* newCustomer(QString name = "",
 Country::CountryType country=Country::Undefined);
 protected:
 CustomerFactory() {};
};
[ . . . . ]

CustomerFactory inherits the ability to create Address objects from ObjectFactory. In addition, it knows how to create Customer objects. CustomerFactory overrides and extends the newObject(QString) method to support more types, as shown in Example 16.5.

Example 16.5. src/libs/customer/customerfactory.cpp

[ . . . . ]

DataObject* CustomerFactory::
newObject (QString className) {
 qDebug() << QString("CustomerFactory::newObject(%1)")
 .arg(className);
 if (className == "Customer")
 return newCustomer();
 if (className == "CustomerList") {
 DataObject* retval = new CustomerList();
 retval->setParent(this); <-- 1
 return retval;
 }
 return ObjectFactory::newObject(className);
}
 

(1)Guarantee the heap memory is freed eventually when this object factory is destroyed, unless it is reparented and killed sooner.

For each of the three returned objects the parent is set to be the factory. We see this explicitly in the case of a CustomerList object. If the newCustomer() function is called, it also returns an object that is parented by the factory, as we see in Example 16.6.

Example 16.6. src/libs/customer/customerfactory.cpp

[ . . . . ]

Customer* CustomerFactory::
newCustomer (QString name, Country::CountryType country) {
 Customer* cust = new Customer();
 cust->setName(name);
 cust->setParent(this);
 if (country != 0) {
 Address* defaultAddress = newAddress(country);
 defaultAddress->setParent(cust);
 cust->setAddress(defaultAddress);
 }
 return cust;
}

16.1.3. qApp and Singleton Pattern

As we discussed earlier, the Singleton pattern is a specialized factory that is used in situations where at most one instance of a class should be created. We use this pattern to manage a singleton ObjectFactory instance. The class definition in Example 16.7 has two distinctive features that characterize this singleton: a non-public constructor and a public static instance() function.

Example 16.7. src/libs/dataobjects/objectfactory.h

[ . . . . ]
class ObjectFactory : public QObject, public AbstractFactory {
 Q_OBJECT
 public:
 static ObjectFactory* instance() ;
 virtual DataObject* newObject (QString className);
 Address* newAddress (Country::CountryType country =
 Country::Undefined);
 Address* newAddress (QString countryName = "USA");
 protected:
 ObjectFactory() {}
};
[ . . . . ]

The instance() function, defined in Example 16.8, is our singleton factory. It creates an object if needed, but only the first time that the function is invoked. It always returns a pointer to the same object on subsequent calls.

Example 16.8. src/libs/dataobjects/objectfactory.cpp

[ . . . . ]

ObjectFactory* ObjectFactory::instance() { <-- 1
 static ObjectFactory* singleton = 0;
 if (singleton == 0) {
 singleton = new ObjectFactory();
 singleton->setParent(qApp); <-- 2
 }
 return singleton;
}
 

(1)static

(2)guarantees this object is deleted when the application is done

For the parent of this object, we set it to qApp. This is a pointer to a singleton instance of a QApplication, which was presumably created in main(). The QApplication instance exists precisely as long as the application is running.

Why not use static for our singleton?

If you make the singleton ObjectFactory a static object instead of a heap object, then children of the factory object will be destroyed after the QApplication is destroyed. Unless there is a compelling reason to the contrary, an application should not do anything after the QApplication has been destroyedincluding object cleanup. Static objects from different files of a multi-file application are destroyed in a linker-dependent order. That order of destruction may cause unintended side-effects (e.g., segmentation faults at termination).

To avoid this problem, you should follow this rule: Each QObject should either be allocated on the stack or be allocated on the heap with another QObject (or qApp) as its parent. This guarantees that there will not be any leftover pieces to be destroyed after the QApplication is destroyed.

If a QObject is allocated on the heap, and its parent is set to qApp, then it is deleted at the "last possible moment." Children of this singleton factory will be deleted just before the factory is deleted.

 

16.1.4. Creation Rules and friend Functions (What Friends Are Really For)

You can design a class to have a creation rule, such as: All new Customers must be created indirectly through a CustomerFactory. You can enforce this rule by defining only non-public constructors. In particular, make sure that you make the copy constructor and the assignment operator non-public. If you omit them from the class definition, the compiler will supply public versions of these two member functions, thus producing two "loopholes" in your creation rule. For example, this might permit clients to create Customer instances that are not children of your factory (which could lead to memory leaks). It is important to document this rule clearly in your class definition, as we did with the comment in Example 16.9.

Example 16.9. src/libs/customer/customer.h

[ . . . . ]

class Customer : public DataObject {
 Q_OBJECT
[ . . . . ]

 friend class CustomerFactory;
 protected:
 Customer(QString name=QString()) { <-- 1
 setObjectName(name);
 }

 Customer(QString name, QString id, CustomerType type);
 

(1)We declared the constructors of Customer as protected, so all Customer objects must be created indirectly through a CustomerFactory.

In Example 16.9, the constructors are protected.[2] CustomerFactory is declared to be a friend class inside Customer. This gives CustomerFactory permission to access the non-public members of Customer. In this way, we have made it impossible for client code to create Customer objects except through the CustomerFactory. In Example 16.10, we have a similar setup with the various Address classes. The base class, Address, is abstract.

[2] This permits us to write derived classes that reuse this constructor.

Example 16.10. src/libs/dataobjects/address.h

[ . . . . ]
class Address : public ConstrainedDataObject {
 Q_OBJECT
 public:
 [ . . . . ]

 protected:
 Address(QString addressName = QString()) { <-- 1
 setObjectName(addressName);
 }
 public:
 virtual Country::CountryType getCountry() = 0;
[ . . . . ]

private:
 QString m_Line1, m_Line2, m_City, m_Phone;
};
 

(1)protected constructor

In the derived classes, defined in Example 16.11, we have protected the constructors and given friend status to a factory, this time DataObjects:: ObjectFactory. This gives ObjectFactory permission to access the non-public members of UsAddress and CanadaAddress.

Example 16.11. src/libs/dataobjects/address.h

[ . . . . ]

class UsAddress : public Address {
 Q_OBJECT
 public:
 Q_PROPERTY( QString State READ getState WRITE setState );
 Q_PROPERTY( QString Zip READ getZip WRITE setZip );
 friend class ObjectFactory; <-- 1
 protected:
 UsAddress(QString name=QString()) : Address(name) {}
 static QString getPhoneFormat();
 public:
 static void initConstraints() ;
[ . . . . ]

 private:
 QString m_State, m_Zip;
};

class CanadaAddress : public Address {
 Q_OBJECT
 public:
 Q_PROPERTY( QString Province READ getProvince
 WRITE setProvince );
 Q_PROPERTY( QString PostalCode READ getPostalCode
 WRITE setPostalCode );
 friend class ObjectFactory; <-- 2
 protected:
 CanadaAddress(QString name=QString()): Address(name) {}
 static QString getPhoneFormat();
 public:
 static void initConstraints() ;
[ . . . . ]
 private:
 QString m_Province, m_PostalCode;
};
 

(1)All new UsAddress objects must be created indirectly through an ObjectFactory.

(2)All new CanadaAddress objects must be created indirectly through an ObjectFactory.

It is now impossible to create Address objects except through the factory or from the derived classes.

If you are writing a multi-threaded application that uses an ObjectFactory, you need to be careful about the ownership of objects.

Making a QObject the parent of a child in another thread will result in an error message such as the following one:

New parent must be in the same thread as the previous parent", file
kernel/qobject.cpp, line 1681
Aborted
 

To make our ObjectFactory work in a multi-threaded environment, we can:

  1. Explicitly pass the parent when we create each object (to prevent the default parenting behavior)
  2. Write the factory's singleton() function to return an object that belongs to the local thread

 

16.1.5. Benefits of Using Factories

One of the benefits of factory patterns is that we can ensure that the created objects are destroyed by assigning them parents before returning them. Any object created by ObjectFactory is deleted when the ObjectFactory singleton is destroyed (unless the object's parent is changed).

As processing continues, we normally expect the created objects to acquire more appropriate parents. In fact, we can regard created objects that retain the factory parent as "unclaimed" objects. We can clean up all of the heap memory used by unclaimed objects by iterating through the children of the factory and deleting them.[3]

[3] This is similar to the way that garbage-collected heaps work.

Indirect object creation also makes it possible to decide at runtime which class objects to create. This allows the "plugging in" of replacement classes without requiring changes in the client code. In Section 16.2, we will see an example of a method that makes use of factory objects to create trees of connected, client-defined objects based on the contents of an XML file.

Libraries and Plugins

When constructing a large system we group classes together in libraries when they share some common features or need to be used together. Substantial applications generally make use of components from several libraries, some supplied by the development team and some supplied by third-party developers (e.g., Qt from Trolltech). Only the public interface of a library class appears in the client code of reusers. Library designers should be able to change the implementation of any class without breaking client code. Many libraries permit the "plugging in" of outside classes by publishing interfaces and documenting how to implement them. Libraries can facilitate the creation of such plug-in classes by providing a factory base class from which specialized factory classes can be derived as needed.

Another benefit of the factory method (or indirect object creation in general) is that you can enforce post-constructor initialization of objects, including the invocation of virtual functions.

Polymorphism from Constructors

An object is not considered "fully constructed" until the constructor has finished executing. An object's vpointer does not point to the correct vtable until the end of the constructor's execution. Therefore, calls to methods of this from the constructor cannot use polymorphism!

Factory methods are required when any polymorphic behavior is needed during object initialization. Example 16.12 demonstrates this problem.

Example 16.12. src/ctorpoly/ctorpoly.cpp

#include 
using namespace std;

class A {
public:
 A() {
 cout << "in A ctor" << endl;
 foo();
 }
 virtual void foo() {
 cout << "A's foo()" << endl;
 }
};

class B: public A {
public:
 B() {
 cout << "in B ctor" << endl;
 }
 void foo() {
 cout << "B's foo()" << endl;
 }
};

class C: public B {
public:
 C() {
 cout << "in C ctor" << endl;
 }

 void foo() {
 cout << "C's foo()" << endl;
 }
};

int main() {
 C* cptr = new C;
 cout << "After construction is complete:" << endl;
 cptr->foo();
 return 0;
}

Its output is given in Example 16.13.

Example 16.13. src/ctorpoly/ctorpoly-output.txt

src/ctorpoly> ./a.out
in A ctor
A's foo()
in B ctor
in C ctor
After construction is complete:
C's foo()
src/ctorpoly>

Notice that the wrong version of foo() was called while the new C object was being constructed. You can find more discussion on vtables in Section 23.1.

Exercises: Creational Patterns

1.

Complete the implementation of the Address, Customer, and CustomerList classes.

Apply the ideas that we discussed in Section 10.6 to write a CustomerWriter class. Make sure that you use the Q_PROPERTY features of Customer and Address so that your CustomerWriter class will not need to be rewritten if you change the implementation of Customer.

Keep in mind that Address objects are stored as children of Customer objects. Here is one output format that you might consider using:

Customer {
 Id=83438
 DateEstablished=2004-02-01
 Type=Corporate
 objectName=Bilbo Baggins
 UsAddress {
 Line1=52 Shire Road
 Line2=Suite 6
 City=Brighton
 Phone=1234567890
 State=MA
 Zip=02201
 addressName=home
 }
}
 

Another possibility arises if you make use of the Dataobject::toString() function.

2.

Write a CustomerReader class that creates all of its new objects by reusing the CustomerFactory class that we supplied.

Write a CustomerListWriter and a CustomerListReader class that serialize and deserialize lists of Customer objects. How much of the CustomerWriter/Reader code can you reuse here?

Write client code to test your classes.


Part I: Introduction to C++ and Qt 4

C++ Introduction

Classes

Introduction to Qt

Lists

Functions

Inheritance and Polymorphism

Part II: Higher-Level Programming

Libraries

Introduction to Design Patterns

QObject

Generics and Containers

Qt GUI Widgets

Concurrency

Validation and Regular Expressions

Parsing XML

Meta Objects, Properties, and Reflective Programming

More Design Patterns

Models and Views

Qt SQL Classes

Part III: C++ Language Reference

Types and Expressions

Scope and Storage Class

Statements and Control Structures

Memory Access

Chapter Summary

Inheritance in Detail

Miscellaneous Topics

Part IV: Programming Assignments

MP3 Jukebox Assignments

Part V: Appendices

MP3 Jukebox Assignments

Bibliography

MP3 Jukebox Assignments



An Introduction to Design Patterns in C++ with Qt 4
An Introduction to Design Patterns in C++ with Qt 4
ISBN: 0131879057
EAN: 2147483647
Year: 2004
Pages: 268

Flylib.com © 2008-2020.
If you may any questions please contact us: flylib@qtcs.net