After having some breakthroughs, you realize that the game development is actually sort of a large system development and you had better have full control over the flow. The more you code (especially if you are trying to come up with a common rendering system or a generic library-kinda-thing for your whole future projects) the more you see the importance of the separate interface and implementation.
That’s exactly what template method pattern provides. It lets you define the backbone of your algorithm and gives you some placeholders to specialize them in the derived classes. Thus, the interface of the base class (and the structure of the algorithm) remains steady, the base class has full control over its interface and policy and now you can enforce interface preconditions and postconditions, insert instrumentation, and perform any similar work all in a single convenient place.
Template method pattern provides you a better separation for the interface and implementation and your base class can handle the future changes and refactors easier since any changes in the implementation won’t bother the common interface at all.
For example consider this example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 |
// Base GameObject class class GameObject { public: GameObject() { } virtual ~GameObject() { } virtual void callOnFrame(const float pDt) = 0; virtual void checkBorders() = 0; protected: std::unique_ptr<Collider> mCollider; std::unique_ptr<Character> mCharacter; }; // Derived AsteroidController class class AsteroidController : public GameObject { public: AsteroidController() : GameObject() { mCollider = std::make_unique<Collider>(); mCharacter = std::make_unique<Asteroid>(); } virtual ~AsteroidController() { } void callOnFrame(const float pDt) override { mCollider->clearCollided(); mCharacter.callOnFrame(pDt); // Some specific code checkBorders(); } void checkBorders() override { // Some specific code } }; // Derived Player class class Player : public GameObject { public: Player() : GameObject() { mCollider = std::make_unique<Collider>(); mCharacter = std::make_unique<Spaceship>(); } virtual ~Player() { } void callOnFrame(const float pDt) override { mCollider->clearCollided(); mCharacter.callOnFrame(pDt); // Some specific code checkBorders(); } void checkBorders() override { // Some specific code } }; // Derived MissileController class class MissileController : public GameObject { public: MissileController() : GameObject() { mCollider = std::make_unique<Collider>(); mCharacter = std::make_unique<Missile>(); } virtual ~MissileController() { } void callOnFrame(const float pDt) override { mCollider->clearCollided(); mCharacter.callOnFrame(pDt); // Some specific code checkBorders(); } void checkBorders() override { // Some specific code } }; |
This is probably what you have been doing for a really long time. And as you can see, this approach causes you the code repetition. Also, it is error prone and adapting to the changes is harder for the common interface.
Remember that virtual methods should be considered as class members. So, they are not supposed to be public unless we really need to have them public. Also consider what Herb Sutter says on his article about virtuality:
-
Guideline #1: Prefer to make interfaces nonvirtual, using Template Method.
-
Guideline #2: Prefer to make virtual functions private. (My note -28/09/2018: If you are working with C# (or should I say Unity?), C# won’t let you to define virtual methods as private. In this case, you will need to go with protected)
-
Guideline #3: Only if derived classes need to invoke the base implementation of a virtual function, make the virtual function protected.
-
Guideline #4: A base class destructor should be either public and virtual, or protected and nonvirtual.
Thus, in our case, it is better to implement a non-virtual public method that defines the common algorithm and call the customizable nonpublic virtual placeholder methods at the related part of the algorithm.
So, we will replace our class implementations as below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 |
// Base GameObject class class GameObject { public: GameObject() { } virtual ~GameObject() { } void callOnFrame(const float pDt) { mCollider->clearCollided(); mCharacter.callOnFrame(pDt); // Placeholder call for specific code performCallOnFrame(pDt); checkBorders(); } protected: std::unique_ptr<Collider> mCollider; std::unique_ptr<Character> mCharacter; private: // Placeholder virtual void performCallOnFrame(const float pDt) = 0; virtual void checkBorders() = 0; }; // Derived AsteroidController class class AsteroidController : public GameObject { public: AsteroidController() : GameObject() { mCollider = std::make_unique<Collider>(); mCharacter = std::make_unique<Asteroid>(); } virtual ~AsteroidController() { } private: void performCallOnFrame(const float pDt) override { // Some specific code } void checkBorders() override { // Some specific code } }; // Derived Player class class Player : public GameObject { public: Player() : GameObject() { mCollider = std::make_unique<Collider>(); mCharacter = std::make_unique<Spaceship>(); } virtual ~Player() { } private: void performCallOnFrame(const float pDt) override { // Some specific code } void checkBorders() override { // Some specific code } }; // Derived MissileController class class MissileController : public GameObject { public: MissileController() : GameObject() { mCollider = std::make_unique<Collider>(); mCharacter = std::make_unique<Missile>(); } virtual ~MissileController() { } private: void performCallOnFrame(const float pDt) override { // Some specific code } void checkBorders() override { // Some specific code } }; |
References: