Creating the Dynamic Factory class

So far, we have only created our builders, which are equivalent to the classic Factory method pattern. However, we haven't seen the Factory part of the Dynamic Factory. Let's look at how the Mach5 Engine implements the StageFactory class:

class M5StageFactory 
{
public:
~M5StageFactory(void);
void AddBuilder(M5StageTypes name, M5StageBuilder* builder);
void RemoveBuilder(M5StageTypes type);
void ClearBuilders(void);
M5Stage* Build(M5StageTypes name);
private:
typedef std::unordered_map<M5StageTypes,
M5StageBuilder*> BuilderMap;
typedef BuilderMap::iterator MapItor;

BuilderMap m_builderMap;
};

As you can see, M5StageFactory isn't a very complicated class. Once you understand the design behind the patterns, implementing them is usually not very difficult. As for this class, it is only five methods and one member. The private section looks a little complicated because Mach5 prefers using a typedef for templated containers. Since the container is used in all of the private functions, let's look at the member before exploring the five methods.

Let's first look the typedefs:

typedef std::unordered_map<M5StageTypes, M5StageBuilder*> BuilderMap; 

Since we want a container of M5StageBuilders, we have a few choices. We could use an STL vector or list, but those containers are not ideal for searching because of the potential lack of efficiency if we have many builders. However, this is exactly what the STL map and unordered_map are perfect for. They allow us to save key/value pairs and later use the key to efficiently find the value, even if we had thousands of builders. We will use the M5StageTypesenum as our key, and use a derived M5StageBuilder* as our value.

An STL map is implemented as a tree, while unordered_map is implemented as a hash table. In general, this means that the map will use less memory, but will be a little slower to search. unordered_map will use more memory but search much faster. In our games, we aren't likely to create thousands of stages, so the difference in speed isn't going to matter that much, especially since we won't be searching very often either. We choose the hash table because, on a PC, I am less concerned about memory and more concerned about speed. If you are interested in learning more, check out http://www.cplusplus.com/reference/ for lots of information about the standard library.

We should also prefer to have as readable code as possible. Using a typedef will help others understand our code, because we only need to write the long std::unordered_map< M5StageTypes, M5StageBuilder*> code one time. After that, we can use the shorted name, in this case BuilderMap. This also gives us the ability to easily change containers if we later decide to use a map instead:

typedef BuilderMap::iterator MapItor; 

The next typedef gives us a shorted name for our BuilderMap iterators.

This is unnecessary with the C++ 11 auto keyword, but doesn't make our code less readable so we have chosen to use the typedef.

Finally, the actual member:

BuilderMap m_builderMap; 

This will be our container that maps M5StageTypes to M5StageBuilder*. We should make it private because we want all builders to be added and removed with the class methods so the data can be validated.

Now for the class methods. Let's start with the most important method of the factory:

M5Stage* M5StageFactory::Build(M5StageTypes type) 
{
ArcheTypeItor itor = m_builderMap.find(type);
if (itor == m_builderMap.end())
return nullptr;
else
return itor->second->Build();
}

The Build method is where the magic happens, at least for the user. They pass in a stage type and we build the correct stage for them. Of course, we use the find method first to make sure the type has been added. If it can't be found, we use a debug assert to let the user know that this type wasn't added. In general, the find method is safer to use than the operator[] that exists in the map and unordered map. Using the operator[] will create and return a null value if the key doesn't exist. If that happened while trying to build, we would get a null pointer exception, which would crash the program without giving the user an explanation of why.

We have the choice of adding some default stage to the map, and building that if the correct type can't be found. However, there is a chance that the programmer won't notice that a mistake has been made. Instead, we have chosen to return a null pointer. The requires the user to check whether the builder is valid before using it, but also means that the code will crash if they don't fix the problem:

bool M5StageFactory::AddBuilder(M5StageTypes name, 
M5StageBuilder* pBuilder)
{
std::pair<MapItor, bool> itor = m_builderMap.insert(
std::make_pair(name, pBuilder));

return itor.second;
}

The AddBuilder method allows our user to associate an M5StageTypes value with a derived M5StageBuilder. In this case, our code doesn't know or care whether pBuilder is pointing to a templated class or not. All that matters is that it derives from M5StageBuilder.

Just as before, we should write our code to help the user find and fix bugs if they occur. We do that by testing the return value of the insert method. The insert method returns a pair in which the second element will tell us whether the insert was successful or not. Since a map and an unordered_map do not allow duplicates, we can test to make sure the user isn't associating an M5StageTypes value with two different builders. If the user tries to use an enum value twice, the second builder will not be inserted and false will be returned.

The STL versions of map and unordered_map do not allow duplicate items. If you wish to have duplicates, you can replace the container with multimap or unordered_multimap, which do allow duplicates. It wouldn't be useful to use the multi versions in this class, but they are good tools to know about.
void M5StageFactory::RemoveBuilder(M5StageTypes name) 
{
BuilderMap::iterator itor = m_builderMap.find(name);

if (itor == m_builderMap.end())
return;

//First delete the builder
delete itor->second;
itor->second = 0;//See the note below

//then erase the element
m_builderMap.erase(itor);
}

By now, the pattern should feel routine. First we write code to make sure there are no errors, then we write the actual function code. In this function, we first check to make sure the user is removing a previously added builder. After we make sure the user didn't make a mistake, we then delete the builder and erase the iterator from the container.

Since we are immediately erasing the iterator after deleting the builder, it is unnecessary to set the pointer to 0. However, I always set the pointer to 0. This helps find bugs. For example, if I forgot to erase the iterator and tried to use this builder again, the program would crash, resulting from using the null pointer. If I didn't set the pointer to 0 but still tried to use it, I would instead get undefined behavior.
void M5StageFactory::ClearBuilders(void) 
{
MapItor itor = m_builderMap.begin();
MapItor end = m_builderMap.end();

//Make sure to delete all builder pointers first
while (itor != end)
{
delete itor->second;
itor->second = 0;
++itor;
}

//Then clear the hash table
m_builderMap.clear();
}

Just as with the RemoveAllComponents from the M5Object, the purpose of ClearBuilders is to help the destructor of the class. Since this code needs to be written anyway (it would go in the destructor), we think it is better to factor it into a separate function that the user can call if they need:

M5StageFactory::~M5StageFactory(void) 
{
ClearBuilders();
}

Finally, we have our factory destructor. This just ensures that we don't have any memory leaks by calling the ClearBuilders function.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.116.15.161