Sign in to follow this  

Singleton Environments

This topic is 4304 days old which is more than the 365 day threshold we allow for new replies. Please post a new topic.

If you intended to correct an error in the post then please contact us.

Recommended Posts

A pattern that I've been using for many years now is that of "singleton environments". I will illustrate the concept with some code, but the code hasn't been compiled (thus, might have typos). The idea is that you might want to run more than one server, and more than one client, in a testing environment, but you want them all to run within the same process for easy debugging. This comes up especially when developing networked games. You can't just make everything a traditional singleton at that point, because the different executing instances (say, two zone servers and two client windows) would step on each other. Instead, the concept of an execution environment as a singleton factory can be used. For each service that you want to be able to expose to parts of your program (server, client, or both), set up a factory which knows how to create an instance of that service, and then vector through a map from service type to instance, and have the map create the service on first access. This way, you create one execution environment per logical program instance you want to run, and configure your program objects with this environment. The program will use this environment, and all services created out of that environment will, in turn, use the environment for their needed services. This keeps everything compartmentalized, letting you run multiple instances of "applications" within the single process. For some services, like file system and event scheduling, you might want to use the same instance for all the program instances; you can accomplish this by configuring the environment up-front with these instances. The pattern looks a bit like:
void main()
{
  IEnvironment * env = NewEnvironment();
  IEventScheduler * es = NewEventScheduler(env);
  env->setSingleton<IEventScheduler>( es );
  IFileSystem * fs = NewFilesystem(env);
  env->setSingleton<IFileSystem>( fs );

  LoginServer * ls = new LoginServer( env, LOGIN_SERVER_PORT );
  ZoneServer * zs1 = new ZoneServer( env, ZONE1_SERVER_PORT, "zone1.data" );
  ZoneServer * zs2 = new ZoneServer( env, ZONE2_SERVER_PORT, "zone2.data" );
  Client * c1 = new Client( env, "127.0.0.1", LOGIN_SERVER_PORT );
  Client * c2 = new Client( env, "127.0.0.1", LOGIN_SERVER_PORT );

  es->runEventLoop();
}
Let's assume that LoginServer needs an IEventScheduler, an INetwork and an IDatabase. Its constructor would look something like:
LoginServer::LoginServer( IEnvironment * env, short port )
{
  eventScheduler_ = env->getSingleton<IEventScheduler>();
  network_ = env->getSingleton<INetwork>();
  database_ = env->getSingleton<IDatabase>();
  network_->startListening( port, this ); // call me when you get new connections
  eventScheduler_->addTimedEvent( this, 0.1 ); // run 10 times a second
}
Similarly, the ZoneServer and Client instances would get the services they need out of their respective environments. The service instances (except for the manually created services) get automatically created on demand, and initialized with the environment that the instance belongs to. Thus, an IDatabase service might actually want to use an INetwork and an IFileSystem internally, and get that from the environment. To implement a service, you'd implement its interface, and have a constructor that can be called by the environment.
class DatabaseImpl : public IDatabase {
  public:
    DatabaseImpl( IEnvironment * env );
    static ServiceFactory< IDatabase, DatabaseImpl > factory_;
};

ServiceFactory< IDatabase, DatabaseImpl > DatabaseImpl::factory_;
So, the implementation of IEnvironment and of ServiceFactory is left. I think you can actually figure those out for yourselves, but I'll sketch them out:
class IServiceFactory {
  public:
    virtual void * manufacture( IEnvironment * env ) = 0;
};

template< class Intf, class Impl >
class ServiceFactory : public IServiceFactory {
  public:
    ServiceFactory() {
      IEnvironment::registerFactory( typeid(Intf).name(), this );
    }
    void * manufacture( IEnvironment * env ) {
      return new Impl( env );
    }
};
Note that the cast through void* looks scary, but the fact that the execution environment, in turn, also uses templates makes it type safe (enough).
class IEnvironment {
  public:
    template< class T >
    T * getSingleton() { return (T *)getSingleton( typeid(T).name() ); }
    template< class T >
    T * setSingleton( T * inst ) { setSingleton( typeid(T).name(), inst ); }
    void registerFactory( char const * name, IServiceFactory * fac );
  protected:
    virtual void * getSingleton( char const * name ) = 0;
    virtual void setSingleton( char const * name, void * inst ) = 0;
};

class Environment : public IEnvironment {
  public:
    void * getSingleton( char const * name ) {
      std::map< std::string, void * >::iterator ptr = map_.find( name );
      if( ptr == map_.end() ) {
        void * inst = makeInstance( name );
        map_[name] = inst;
        return inst;
      }
      return (*ptr).second;
    }
    void setSingleton( char const * name, void * inst ) {
      std::map< std::string, void * >::iterator ptr = map_.find( name );
      if( ptr != map_.end() ) {
        throw std::runtime_error( "attempt to set a singleton twice" );
      }
      map_[name] = inst;
    }
    void * makeInstance( char const * name ) {
      std::map< std::string, IServiceFactory * >::iterator ptr =
          factories_.find( name );
      if( ptr == factories_.end() ) {
        throw std::runtime_error( "attempt to manufacture a service that does not exist" );
      }
      return (*ptr).second->manufacture( this );
    }
    std::map< std::string, void * > map_;
    static std::map< std::string, IServiceFactory * > factories_;
};

std::map< std::string, IServiceFactory * > Environment::factories_

void IEnvironment::registerFactory( char const * name, IServiceFactory * fac )
{
  std::map< std::string, IServiceFactory * >::iterator ptr = 
      Environment::factories_.find( name );
  if( ptr != Environment::factories_.end() ) {
    throw std::runtime_error( "attempt to register two factories for the same interface" );
  }
  factories_[name] = fac;
}
The service factories get registered during static program initialization, before main() is executed, so you cannot use an environment until you get to main(). Typically, that's a pretty good rule to use anyway: doing too much work before main() leads to very hard to maintain and debug code. So, there you have it. It's really quite a powerful way of managing what environment each piece of code executes within, without having to resort to process-global singleton instances. The minor overhead of looking up a service can usually just be paid for when you don't need to do it all that often, and if you do it often enough to matter, you will get the value into a local member variable, which is more efficient than the global-singleton pattern anyway (because it's branch-less and function-call-less).

Share this post


Link to post
Share on other sites

This topic is 4304 days old which is more than the 365 day threshold we allow for new replies. Please post a new topic.

If you intended to correct an error in the post then please contact us.

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

Sign in to follow this