Example 5

This example shows how to adapt existing class to bind with remote interface and implement remote factory pattern. It introduces the items below:

  • remote::pool_node class.
  • retrive target pointer from remote pointer using target_cast method.
  • how to create an adaptor class to adapt existing class to remote interface.
  • how to create remote service on demand.

Let's say we already have a class model that implements a factory that produces different type of media player based on the parameter of it's create method. In this example, we want to extend this model to a remote factory where client side can use this factory through remote interface to create different type of player remotely. Then client can use the player remote interface to control the created media player. Below is the original class model we are going to extend through remote interface.

// model.hpp
class player
{
public:
    virtual ~player(){};
    virtual void play(std::string) = 0;
    virtual void stop() = 0;
};

class audio_player : public player
{
public:
    void play(std::string song)
    {
        std::cout << "audio player playing " << song << std::endl;
    }

    void stop()
    {
        std::cout << "audio player stopped" << std::endl;
    }
};

class video_player : public player
{
public:
    void play(std::string video)
    {
        std::cout << "video player playing " << video << std::endl;
    }

    void stop()
    {
        std::cout << "video player stopped" << std::endl;
    }
};

class player_factory
{
    std::set<player*> m_players;
public:
    player* create(std::string type)
    {
        // create player based on type
        player* p = 0;
        if(type == "audio")
            p = new audio_player;
        else if(type == "video")
            p = new video_player;
        else
            return 0;

        // insert the created player
        m_players.insert(p);
        std::cout << "create " << type << std::endl;

        return p;
    }

    void destroy(player* p)
    {
        // check if this player is created here
        std::set<player*>::iterator iter = m_players.find(p);
        if(iter == m_players.end())
            return;

        std::cout << "destroy a player" << std::endl;

        // reclaim resource
        m_players.erase(iter);
        delete p;
    }

    std::set<player*> all_player() const
    {
        return m_players;
    }
};

Based on the existing model, we need to create two remote interfaces. One to bind with the player class and another one to bind with the player_factory class. First, we create a remote_player remote interface below to bind with the player class.

Note: Only the class definition code fragment is shown here.

// remote_player.hpp
REMOTE_CLASS_BEGIN(remote_player)           \
REMOTE_METHOD_M1(void, play, std::string)   \
REMOTE_METHOD_M0(void, stop)                \
REMOTE_CLASS_END

Then we create a remote_factory class below that accept remote_player remote interface as parameter.

//remote_factory.hpp
REMOTE_CLASS_BEGIN(remote_factory)                      \
REMOTE_METHOD_M1(remote_player*, create, std::string)   \
REMOTE_METHOD_M1(void, destroy, remote_player*)         \
REMOTE_METHOD_C0(std::set<remote_player*>, all_player)  \
REMOTE_CLASS_END

You might have noticed that the member functions signatures of remote_factory and player_factory are not match. One return a remote_player* type and the other one return a player* type. In this case, we can't bind the player_factory as remote_factory service!

In this case, we need an adaptor class that adapt the player_factory interface to remote_factory interface. In this adaptor, we need to convert the member function parameter from remote_player* to player*. The constructor of factory_adaptor class below take reference to a pool_node object and reference to a player_factory object that is to be adapted. To convert a remote_player* to/from player*, we use the target_cast and remote_cast member functions of pool_node object.

// factory_adaptor.hpp
#include "model.hpp"
#include "remote_player.hpp"
#include <remote/pool_node.hpp>

class factory_adaptor : public boost::noncopyable
{
    remote::pool_node& m_node;
    player_factory& m_factory;
public:
    factory_adaptor(remote::pool_node& node, player_factory& factory)
    : m_node(node), m_factory(factory)
    {}

    remote_player* create(std::string type)
    {
        // using pool_node to do remote_cast to bind the player* as remote_player*
        return m_node.remote_cast<remote_player>(m_factory.create(type));
    }

    void destroy(remote_player* p)
    {
        // using pool_node to do target_cast
        m_factory.destroy(m_node.target_cast<player>(p));

        // using pool_node to unbind remote service
        m_node.unbind(p);
    }

    std::set<remote_player*> all_player() const
    {
        std::set<remote_player*> rp;
        std::set<player*> p = m_factory.all_player();

        std::transform(p.begin(), p.end(), std::inserter(rp, rp.begin()),
                boost::bind(&remote_cast, boost::ref(m_node), _1));
        return rp;
    }

    static remote_player* remote_cast(remote::pool_node& node, player* p)
    {
        return node.remote_cast<remote_player>(p);
    }
};

The factory_adaptor class has member functions signatures that match with the remote_factory interface. Now, we can bind player_factory to remote_factory through this adaptor class but where do we find a pool_node object? remote::server is also a pool_node object. Both remote::server and remote::session are inherited from pool_node class. We use pool_node in this adaptor to make this adaptor more generic. If want to use this adaptor with session in the future, we can reuse this class.

Like I always say, once we have the remote interface classes, creating the server and client applications are simple.

// server.cpp
int main()
{
    player_factory factory;
    remote::server server;

    factory_adaptor adaptor(server, factory);
    server.bind<remote_factory>(&adaptor, "factory");

    server.start(remote::make_tcp_binding(8888));

    std::cin.get();
    return 0;
}
// client.cpp
int main()
{
    remote::session session;
    session.start(remote::make_tcp_binding("127.0.0.1", 8888));
    if(session.wait_for_ready() != remote::session::started)
        return -1;

    // get a remote factory pointer
    boost::shared_ptr<remote_factory> factory
        = session.get<remote_factory>("factory");

    // create media player services using the remote factory pointer
    remote_player* audio_player = factory->create("audio");
    remote_player* video_player = factory->create("video");
    remote_player* null_player = factory->create("other");

    // use the remote player interface
    BOOST_VERIFY(!null_player);
    audio_player->play("nice song");
    video_player->play("nice movie");

    // stop all created player
    std::set<remote_player*> players = factory->all_player();
    std::for_each(players.begin(), players.end(),
                boost::bind(&remote_player::stop, _1));

    // destroy the player
    factory->destroy(audio_player);
    factory->destroy(video_player);
    
    // remember to release all the raw pointers
    session.release(audio_player);
    session.release(video_player);
    
    return 0;
}

In this example, we have extended an existing factory class model to a remote factory model in a non-intrusive manner. It also shows how to use pool_node to create adaptor to adapt existing class to remote interface. The adaptor pattern is useful not only to adapt one class pointer to remote pointer, but also for us to handle resources like when to unbind or release a remote pointer.

We have created a server that can create or destroy different type of services on demand using this remote factory pattern. This can be compared to the server object activation that is normally implemented as a feature in most middleware frameworks. User normally get this feature by following the framework way of doing it. In this library, users create this feature by expressing it in their design.

Next Example