Let's say you have a domain, a game for instance, that includes well over 100 different classes in a class hierarchy (this is not an unreasonable number, but it's also an arbitrary number). You need a way of creating all these different objects. Fair enough, just create them. The problem that arises, sooner rather than later, is that you have will eventually have countless of different places where you include different parts of this hierarchy. In the specific case of C++ this bloats your include diagrams into an unreadable form in the worst cases, but more generally it creates a lot of dependencies. For example your player class needs to include the different bullet classes in order to create them. Moreover, whenever you add a new type of bullet to your game, this needs to also be included.
Would it not be nice, then, to have a single class whose only responsibility is to create these objects. It, solely, includes every class it needs to create, and it then provides a method which create specific objects and returns them. This is a factory.
There are different ways of implementing a factory, but (part of) the method described in Design Patterns (by Gamma et. al) is something akin to this:
Creator.h
1: namespace Creator
2: {
3: Enum ObjectID {player, enemy};
4: object* Creator::create(ObjectID id);
5: };
Creator.cpp
1: #include "Player.h"
2: #include "Enemy.h"
3: object* Creator::create(ObjectID id)
4: {
5: if (id == player) return new Player();
6: else if (id == enemy) return new Enemy();
7: /* and so on*/
8: else return NULL; /* could be useful if the enum contains an
9: entry for which we have no class yet */
10: }
Note: the actual pattern as it's described in the book is more flexible and fleshed out, but this is the gist of it: a method that takes an ID as a parameter, and based on that ID returns an object. Here the ID is in the form of an enum, but it can as easily be a string (which has its own advantages and disadvantages).
The thing to note here is that any class that uses the creator to create its objects have no idea how to create these objects, all it has is a list of IDs that magically turns into an object through the create method. Also note that whenever a new object is included in the creator, only Creator.c has to be recompiled. This is a big step forward from the naive approach.
One disadvantage of this approach is that - most obviously - a new if statement must be added for every class we add to the factory. A better approach would be to use a lookup table somehow:
1: #include <map>
2: #include <string>
3: #include <functional>
4: class Factory
5: {
6: public:
7: Factory()
8: {
9: creators.insert(std::make_pair
10: ("Player",
11: []() -> Object* {return new Player();}));
12: creators.insert(std::make_pair
13: ("Enemy",
14: []() -> Object* {return new Enemy();}));
15: }
16: Object* create(const std::string& type)
17: {
18: return creators[type]();
19: }
20: private:
21: std::map<std::string, std::function<Object* ()>> creators;
22: };
Here we instead us a mapping from a string to an object creator-function, supported by std::map and std::functional. For brevity I defined all functions inside the header; outside of this example they should be defined in a separate *.cpp-file to limit dependencies, as we did in the previous code snippet (if we don't we might as well not use a factory to begin with!).
The disadvantages of using a string for an ID is obvious, it's very easy to mistype and the error will not be caught during compilation. That's a huge problem. In the example above we don't even check whether the string is mapped or not, we naively assume the caller will do the right thing. In this case we have undefined behaviour if the string isn't mapped. This is easy to fix of course (std::map::count()).
The advantages are this:
- It's very flexible.
- There is no enum we need to know about.
- We can clearly see what we are creating (as opposed to create(1) for enemy and create(0) for player.
- We can create objects directly by reading from a file or similar.
In my opinion the last point is the most enticing. The simplest way to demonstrate this would be something akin to this:
1: void UserCreatesObjects()
2: {
3: std::string input;
4: std::vector<Object*> objects;
5: std::cout << "What should I create? \n";
6: std::getline(std::cin, input);
7: while (!input.empty());
8: {
9: objects.push_back(factory.create(input));
10: std::cout << "What should I create next \n";
11: std::getline(std::cin, input);
12: }
13: for (auto object : objects)
14: {
15: std::cout << object->what() << "\n";
16: }
17: }
No pesky if statements, just create what the user says. This makes loading a game state from a file a breeze, for example. Note that the only include you need to make this method work is to include the factory-header, which in turn doesn't include any of the concrete classes itself. It's safe to say that the user still have no idea how to create these objects, even when he, literally, does! Here we also use a creator, which is a function a factory uses to create an object. The factory itself doesn't actually create the object, it calls a creator which creates the object for the factory.
Of course, we need to handle bad user input.
A question that might arise is how do we allow parameters? Some objects doesn't make sense to create without parameters. This problem CAN be solved, but it will bloat the code up a lot (at least my solution), and I will not include it here. A problem with allowing parameters is that it makes the coupling between the user of the factory and the objects he creates, he needs to know more about how the object is created, and it's not simply just a string anymore. One can argue that allowing parameters defeats the purpose of the factory. There are cases where you might allow parameters: for example the Unity engine requires every object to have a transform component, in the same way your game engine might require every object to have a position. In this case it makes sense that every object be created with a position, and in then you might have the create method for the factory take a position-parameter, knowing that every object has a constructor for it.
The following code is a template version of the type of factory I've been describing (it's a TMP factory!): Factory.h
1: #pragma once
2: #include <memory>
3: #include <map>
4: #include <string>
5: #include <algorithm>
6: #include <functional>
7: template <class T, class ... Ps>
8: class Factory
9: {
10: public:
11: std::shared_ptr<T> create(const std::string& str, Ps... ps)
12: {
13: auto found = creators.find(str);
14: if (found != creators.end())
15: return found->second(ps...);
16: else
17: return nullptr;
18: }
19: template <class U>
20: void addCreator(std::string str)
21: {
22: creators.insert(std::make_pair(str,
23: [&] (Ps... ps) -> std::shared_ptr<U>
24: {return std::make_shared<U>(ps...);}));
25: }
26: private:
27: std::map<std::string,std::function<std::shared_ptr<T>(Ps...)>> creators;
28: };
A factory object is set up with a type, which is the type the create()-function will return. This type will be the base class of whatever objects we create. We also provide a variadic number of parameters; every object we create should (must) provide a constructor which takes these parameters (in that order). Using parameters with the factory is, of course, completely optional. I should add, this code has been tested, and works, in Visual Studios Express 2013 Preview.
The interface for the class is rather simple. The function addCreator essentially maps a string to a class, by creating a lamda which returns an object (actually a shared_ptr) of that class, mapping that lamda to the string-value. The create-function uses this mapping to return a new object of that class; it also takes some parameters, which are the parameters we specified as the template arguments when creating the Factory object. It handles faulty user input by returning a nullptr if the string is not found in the map.
Example of use:
As an example of use I will use fruits (yum). It should hopefully convey what my implementation of a factory can do:
Fruit.h
1: #pragma once
2: #include <string>
3: #include "Factory.h"
4: struct Fruit
5: {
6: static void addCreators(Factory<Fruit, int>& factory);
7: virtual ~Fruit() {};
8: virtual int ripeness() = 0;
9: virtual std::string what() = 0;
10: };
Fruit.cpp
1: #include "Fruit.h"
2: struct Apple;
3: struct Banana;
4: struct Orange;
5: void Fruit::addCreators(Factory<Fruit, int>& factory)
6: {
7: factory.addCreator<Banana>("Banana");
8: factory.addCreator<Orange>("Orange");
9: factory.addCreator<Apple>("Apple");
10: }
11: struct Apple : public Fruit
12: {
13: Apple(int ripeness) : ripeness_(ripeness) {};
14: std::string what()
15: {
16: return "I am an apple!";
17: }
18: int ripeness()
19: {
20: return ripeness_;
21: }
22: private:
23: int ripeness_;
24: };
25: struct Banana : public Fruit
26: {
27: Banana(int ripeness) : ripeness_(ripeness) {};
28: std::string what()
29: {
30: return "I am a banana!";
31: }
32: int ripeness()
33: {
34: return ripeness_;
35: }
36: private:
37: int ripeness_;
38: };
39: struct Orange : public Fruit
40: {
41: Orange(int ripeness) : ripeness_(ripeness) {};
42: std::string what()
43: {
44: return "I am an Orange!";
45: }
46: int ripeness()
47: {
48: return ripeness_;
49: }
50: private:
51: int ripeness_;
52: };
Note that, whomever includes fruit.h has no idea what an Orange, an Apple, or a Banana is, let alone how to create them. By the time the fruit class creates the creators for these object, neither does the fruit class! (I'm actually curious as to how that really works). We could add any number of different fruits, and this would change nothing for whoever uses the factory to create the fruits. Below is the code for main(), which uses the factory to create fruits:
main.cpp
1: #include "Factory.h"
2: #include <vector>
3: #include <memory>
4: #include <iostream>
5: #include "Fruit.h"
6: int main()
7: {
8: std::vector<std::shared_ptr<Fruit>> fruits;
9: Factory<Fruit, int> factory;
10: Fruit::addCreators(factory);
11: fruits.push_back(factory.create("Banana", 1));
12: fruits.push_back(factory.create("Apple", 9000));
13: fruits.push_back(factory.create("Orange", 42));
14: fruits.push_back(factory.create("Apple", -14));
15: for (auto& fruit : fruits)
16: {
17: std::cout << fruit->what() << " - " << fruit->ripeness() << "\n";
18: }
19: }
Output:
Banana! - 1
Apple! - 9000
Orange - 42
Apple - -14
As we expected.
Again, note that main does not knows how to create the different fruits!
If you have any input at all, feel free to comment. I am sure there are better ways of doing things, and I'm also sure at least half of what I wrote was partly wrong! If you would like to use the above code, feel free to do so.
Inga kommentarer:
Skicka en kommentar