Saturday, October 31, 2015

c++ - When several classes need to access the same data, where should the data be declared?



I have a basic 2D tower defense game in C++.


Each map is a separate class which inherits from GameState. The map delegates the logic and drawing code to each object in the game and sets data such as the map path. In pseudo-code the logic section might look something like this:


update():
for each creep in creeps:
creep.update()
for each tower in towers:
tower.update()
for each missile in missiles:
missile.update()


The objects (creeps, towers and missiles) are stored in vector-of-pointers. The towers must have access to the vector-of-creeps and the vector-of-missiles to create new missiles and identify targets.


The question is: where do I declare the vectors? Should they be members of the Map class, and passed as arguments to the tower.update() function? Or declared globally? Or are there other solutions I'm missing entirely?


When several classes need to access the same data, where should the data be declared?



Answer



When you need a single instance of a class throughout your program, we call that class a service. There are several standard methods of implementing services in programs:



  • Global variables. These are the easiest to implement, but the worst design. If you use too many global variables, you will quickly find yourself writing modules that rely on each other too much (strong-coupling), making the flow of logic very difficult to follow. Global variables are not multithreading-friendly. Global variables make tracking the lifetime of objects more difficult, and clutter the namespace. They are, however, the most performant option, so there are times when they can and should be used, but use them spareingly.


  • Singletons. About 10-15 years ago, singletons were the big design-pattern to know about. However, nowadays they are looked down upon. They are much easier to multi-thread, but you must limit their use to one thread at a time, which is not always what you want. Tracking lifetimes is just as difficult as with global variables.
    A typical singleton class will look something like this:



    class MyClass
    {
    private:
    static MyClass* _instance;
    MyClass() {} //private constructor

    public:
    static MyClass* getInstance();
    void method();
    };


    ...

    MyClass* MyClass::_instance = NULL;
    MyClass* MyClass::getInstance()
    {
    if(_instance == NULL)
    _instance = new MyClass(); //Not thread-safe version
    return _instance;


    //Note that _instance is *never* deleted -
    //it exists for the entire lifetime of the program!
    }


  • Dependency Injection (DI). This just means passing the service in as a constructor parameter. A service must already exist in order to pass it into a class, so there's no way for two services to rely on each other; in 98% of the cases, this is what you want (and for the other 2%, you can always create a setWhatever() method and pass in the service later). Because of this, DI doesn't have the same coupling problems as the other options. It can be used with multithreading, because each thread can simply have its own instance of every service (and share only those it absolutely needs to). It also makes code unit-testable, if you care about that.


    The problem with dependency injection is that it takes up more memory; now every instance of a class needs references to every service it will use. Also, it gets annoying to use when you have too many services; there are frameworks that mitigate this problem in other languages, but because of C++'s lack of reflection, DI frameworks in C++ tend to be even more work than just doing it manually.


    //Example of dependency injection
    class Tower
    {

    private:
    MissileCreationService* _missileCreator;
    CreepLocatorService* _creepLocator;
    public:
    Tower(MissileCreationService*, CreepLocatorService*);
    }

    //In order to create a tower, the creating-class must also have instances of
    // MissileCreationService and CreepLocatorService; thus, if we want to
    // add a new service to the Tower constructor, we must add it to the

    // constructor of every class which creates a Tower as well!
    //This is not a problem in languages like C# and Java, where you can use
    // a framework to create an instance and inject automatically.

    See this page (from the documentation for Ninject, a C# DI framework) for another example.


    Dependency injection is the usual solution for this problem, and is the answer you will see most highly upvoted to questions like this on StackOverflow.com. DI is a type of Inversion of Control (IoC).




  • Service Locator. Basically, just a class that holds an instance of every service. You can do it using reflection, or you can just add a new instance to it every time you want to create a new service. You still have the same problem as before - How do classes access this locator? - which can be solved in any of the above ways, but now you only need to do it for your ServiceLocator class, rather than for dozens of services. This method is also unit-testable, if you care about that sort of thing.


    Service Locators are another form of Inversion of Control (IoC). Usually, frameworks that do automatic dependency injection will also have a service locator.



    XNA (Microsoft's C# game programming framework) includes a service locator; to learn more about it, see this answer.






By the way, IMHO the towers should not know about the creeps. Unless you are planning on simply looping over the list of creeps for every tower, you'll probably want to implement some nontrivial space partitioning; and that sort of logic doesn't belong in the towers class.


No comments:

Post a Comment

Simple past, Present perfect Past perfect

Can you tell me which form of the following sentences is the correct one please? Imagine two friends discussing the gym... I was in a good s...