Sign in to follow this  
staticVoid2

Proxy pattern problem

Recommended Posts

Hi,

I'm currently stuck in a bit of a rut trying to solve a problem, this is something I've been working on for the past few weeks and I still can't seem to come up with a good solution.

I have a system which is currently split into four main parts:

    1. API                   (Subject)
    2. Core System    (Real Subject)
    3. Network Proxy  (Proxy)
    4. User Interface

And structured like so:

Diagram1.png

   

 

 

As you can see, this follows the Proxy pattern (https://en.wikipedia.org/wiki/Proxy_pattern) where I have a 'proxy' implementation of the API which allows the system to be run over a network; the proxy objects live on the client and communicate with the server via HTTP.

The API consists of a couple of interface classes representing objects the developer can access and manipulate. These objects generally have a lot of accessor methods for various properties and some of them have a one-to-many relationship with other objects. As an example:

// Subject/API
class IObjectA
{
public:
    virtual int getValue1() const = 0;
    virtual int getValue2() const = 0;
    virtual int getValue3() const = 0;
    // ... lots more accessors

    virtual void createObjectB() = 0;

    // IObjectA should have a collection of IObjectB
    virtual std::vector<IObjectB*> getObjectBs() const = 0;
};

// Real Subject
class ObjectA : public IObjectA
{
public:
    virtual int getValue1() const override { return value1; }
    virtual int getValue2() const override { return value2; }
    virtual int getValue3() const override { return value3; }

    virtual void createObjectB() override
    {
        objectBs.push_back(new ObjectB()); // ObjectB implements IObjectB
    }

    virtual std::vector<IObjectB*> getObjectBs() const override { return objectBs; }
private:
    int value1, value2, value3;
    std::vector<ObjectB*> objectBs;
};

// Proxy
class ProxyObjectA : public IObjectA
{
public:
    virtual int getValue1() const override
    {
        JSONObject obj = SendHTTPRequest("GET", "/objectA");
        return obj["value1"].asInt();
    }
    virtual int getValue2() const override { /* Similar to above */ }
    virtual int getValue3() const override { /* Similar to above */ }

    virtual void createObjectB() override
    {
        // Create a new object on the server
        SendHTTPRequest("POST", "/objectAs/objectBs", "" /*empty object*/);
    }

    virtual std::vector<IObjectB*> getObjectBs() const override
    {
        JSONObject obj = SendHTTPRequest("GET", "/objectA/objectBs");
        std::vector<ObjectB*> objectBs;
        for(JSONObject objB : obj["objects"])
        {
            objectBs.push_back(new ObjectB(objB));
        }
        return objectBs;
    }
};

The user interface currently interacts directly with the API, calling into the above accessor methods every ~0.5 seconds to refresh the view data. This is fine when the underlying implementation is 'ObjectA' however it slows the system down to a crawl when using the 'ProxyObjectA' implementation due to the server response times.

Attempt 1:

The first solution that came to mind was to incorporate some sort of callback system into the accessor methods, fundamentally changing the API so you would have calls that looked like:

IObjectA* obj = ...;
obj->getValue1([](int value1){ /* Do something */ });

This made it clear that the implementation could potentially take a long time but it also seemed like it might spiral into callback hell (from an API design point of view), especially with the number of accessor methods present, and after reading this article: http://www.drdobbs.com/parallel/prefer-futures-to-baked-in-async-apis/222301165 I decided that this was probably not the way to go.

Attempt 2:

So the next approach I took, following the advice in the article, was to simply revert back to what I had originally and allow the API implementations to take as long as they needed to retrieve the data their interface had promised. This drastically simplified the code and prevented synchronization issues when it came to creating objects (createObjectB) and accessing them (getObjectBs) (see below). It also just made a lot more sense, looking at the method implementations you could clearly see what these methods were doing (or at least what their main intention was). However, performance was still a real issue when it came to the UI so I had to make another attempt.

Attempt 3:

The next approach I tried was to spawn a separate thread which would constantly retrieve all the data it needed from the server every second or so for the duration of the application. So the 'ProxyObjectA' class now looked a bit like so:

// Proxy
class ProxyObjectA : public IObjectA
{
public:
    virtual int getValue1() const override
    {
        std::scoped_lock lock(mutex);
        return value1;
    }
    virtual int getValue2() const override { /* Similar to above */ }
    virtual int getValue3() const override { /* Similar to above */ }

    // This method still blocks...
    virtual void createObjectB() override
    {
        // Create a new object on the server
        SendHTTPRequest("POST", "/objectAs/objectBs", "" /*empty object*/);
    }

    virtual std::vector<IObjectB*> getObjectBs() const override
    {
        std::scoped_lock lock(mutex);
        return objectBs;
    }

    // This methods gets called every second from a separate
    // thread with new information from the server
    void update(int value1, int value2, int value3, const std::vector<ObjectBs>& objectBs)
    {
        std::scoped_lock lock(mutex);
        this->value1 = value1;
        this->value2 = value2;
        this->value3 = value3;
        this->objectBs = objectBs;
    }

private:
    // Cached values
    int value1, value2, value3;
    std::vector<ObjectB*> objectBs;
    mutable std::mutex mutex;
};

This worked fairly well, but I still had a few reservations about it:

1. For a start, having a separate thread constantly retrieve data from a server seems like a terrible solution, considering this would be running for the duration of the application and most of the time would not be getting accessed at all.

2. There could be cases where I would add a new 'B' object via the 'createObjectB' method and then a subsequent call to 'getObjectBs' would return an empty vector because it had not been updated from the server yet.

3. Constantly locking and unlocking mutexes also seemed like a terrible thing to do and there was still some noticeable flicker in the UI, presumably caused by this.

4. Values will generally be out of date, and this is not made apparent from the interface.

 

However, this was the only solution which provided some form of batching the data into as few requests as possible, the previous attempts would all have to send individual HTTP requests for every accessor method call.

This is basically my problem in a nutshell, I've had three attempts at fixing this to no avail so I thought I would ask for some advice. I'm probably going about it the wrong way or there is some obvious solution staring me in the face but I can't seem to see it. It feels like a check-mate situation :(

I would be grateful if anyone could point me in the right direction, especially if you know of any existing systems that are similar to this and have managed to solve these problems.

 

Thanks







  

Edited by staticVoid2

Share this post


Link to post
Share on other sites

You'll probably want to have each client maintain its own local copy of the model, and write-through any changes to the server behind the scenes in a separate thread. All local access remains instantaneous, while the server is updated on any changes.

Clients then need a way to determine if their local cache has been invalidated. This is going to look a lot like your attempt 3, with a separate thread responsible for polling the server. However you'll want to check out techniques like long polling so that your HTTP connections stick around longer and can be reused. These approaches will also often allow your server to notify clients sooner that data has been updated, since the connection may already be established and the server can "push" the updates right away.

Keep in mind though that you'll need to be careful with state shared among multiple clients. I might go so far as to explicitly disallow multiple writers on any given object (or for certain operations), but that's just because I like to start with the most restrictive solution and open it up as/if necessary.

Share this post


Link to post
Share on other sites

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