开发者

Collections of objects that are aware of each other

开发者 https://www.devze.com 2023-03-29 13:32 出处:网络
This is kind of a broad question, but for my own understanding I decided I would throw it out there. Can anyone recommend designs, or maybe even generally accepted design patterns for situations wher

This is kind of a broad question, but for my own understanding I decided I would throw it out there.

Can anyone recommend designs, or maybe even generally accepted design patterns for situations where one would want various objects that are aware of each other?

To use a people/community simulation as an analogy, what would be the best pattern to apply when you have X people objects where X can grow/shrink dynamically, and have each person object hold an opinion of or relationship with the other objects?

Thinking outside of the programming syntax, I could just have a X by X grid that represents each person's relationship with each other (likes, dislikes, hasn't met, etc). I thought of basically implementing that in code 开发者_StackOverflow社区as a separate object from each person object that was updated every time a new one was created, but it seemed like a completely un-elegant solution. Does anyone have any advice here?

Secondly, if I have collections of people each with "inventories" or items that they are carrying, I thought that I could create each person's inventory as a List member of the class; this linked-list would grow and shrink as the objects that the person has grow and shrink.

The people objects could easily be queried for their objects they are carrying, but I can't figure out an efficient way to make vice-versa possible as well; that is, I want to be able to query an item and find which people have that item.

I did read this Q-A: Game Objects Talking To Each Other ..but I'm not sure if it is completely applicable to my case.

Can anyone offer any advice?

Thanks!

--R


What you're looking for is a relatively simple association.

class Person {
    struct Relationship {
        // whatever
    };
    std::map<Person*, Relationship> relationships;
public:
    Person() {}
    void Meet(Person* other) {
        // think about it
        relationships[other] = ...;
    }
    ~Person() {
        // Loop through the map and ensure no dud relationships
        for(auto& pair : relationships) {
            pair.first->relationships.erase(this);  
        }
    }
};

As for determining the person which has certain items, I'm thinking that a (relatively) simple manager solution ought to work well, although I hate calling classes "Manager".

class PersonManager {
    struct Item { ... };
    std::set<Item, std::set<Person*>> items; // set for no dupes
public:
    void AddToInventory(Item i, Person* p) {
        items[i].insert(p);
    }
    std::vector<Person*> PeopleWithItem(Item i) {
        return std::vector<Person*>(items[i].begin(), items[i].end());
    }
};

If you have to model a lot of relationships this way, I'd suggest going to a database. However, I doubt that any database can match the efficiency of this code- you can even go to hash containers and get O(1) for most of these actions.


Relational databases were made exactly for this purpose. If you are not familiar with it, read about it!

You could use a database with several tables. One table would be "person" which holds basic properties of people plus an id.

To model relationships like "met", create a table called met with 2 columns, which hold the ids of two people who met each other.

Databases are heavily optimized for queries like "find all persons that met John". If you don't want to install a client/server database, you could use a file based database like SQLite


If the relationship is merely communication, and not actually lifetime ownership, and you don't want everyone to be able to talk with everyone automatically, then interface proxies are usually a good start. These are simply intermediate classes that own a way to communicate with the colleague and provide notifications when the lifetime of either side changes. These are usually unidirectional, but when you pair them and add a little more glue code, they become bidirectional.

So, you have a class

class ColleagueProxy
{
private:
    // a pointer to the Colleague
    // it can be a naked pointer since it does not deal with lifetime
    Colleague friend_;

    // a callback for when death comes to the colleague
    typedef std::function<void (ColleagueProxy *)> DeathHandler;
    DeathHandler deathCallback_;

    // an identifier for friends to know me by
    std::string id_;

public:
    // ctor takes a colleague and a callback for when colleague dies
    // ctor notifies friend of new proxy following - in case that is useful info
    ColleagueProxy(Colleague * friend, DeathHandler callback, std::string const& myName) :
      friend_(friend),
      deathCallback_(callback)
    {
      if (friend)
        friend_->proxyConnecting(this);
    }

    // dtor may notify friend as well
    ~ColleagueProxy()
    {
      if (friend)
        friend_->proxyLeaving(this);
    }

    // the communication interface
    void sayHi() 
    { 
      if (friend)
        friend_->sayHi(this); 
    }
    // ...

    // my name badge
    std::string id() { return id_; }

    // a way for the friend to say goodbye
    void Goodbye()
    {
       deathCallback_(this);
    }
};

and then the colleagues would store these communication proxies

class Colleague
{
private:
    std::map<std::string, std::shared_ptr<ColleagueProxy>> friends_;
    std::vector<std::shared_ptr<ColleagueProxy>> colleaguesWhoConsiderMeAFriend_;

    void GoodbyeCallback(ColleagueProxy * that)
    {
      // search through friends_ and remove that
    }
}

You can automate some of the cleanup part through a manager class, so you don't duplicate the code if you have different dynamic types. If you have a single Colleague type, though, that's not really necessary. Another useful reason a manager class may be helpful is that it can do the creation of colleagues, which may apply restrictions or other visibility rules.

The point of this kind of design is to allow a communication mechanism that can be customised per connection (every proxy object may hold connection-specific state). This could be applied to opinions (as in the original question) and many other things. You don't have to worry about cyclic dependencies because lifetime management is not a part of the connection (like if you just stored a struct with a shared_ptr to the friend, where cyclic dependencies may cause issues of orphaned cycles).

If you actually wanted to let everyone see everyone else without restriction, then storage of Proxies inside the Colleague class is not necessary and a manager class is preferred. Then all communications occur through the manager, which acts like a proxy collection. In those cases, probably the same manager that manages the lifetime of the Colleagues can play the role as the proxy manager as well, to prevent duplication of data and optimise access.


The canonical example in the GOF book is to use the Mediator pattern. The example they give is that a GUI window/dialog (the Mediator) keeping track of its controls (sometimes called Colleagues), and notifying the controls of relevant events, such as window resizing, etc. I believe the example extends to allowing the controls to query the mediator for information about other controls.

If you want your objects to be able to track particular instances of objects, the Observer pattern (as indicated by cbamber85) is useful. But for general information about events occurring in a population of objects, you might want to consider using a Mediator.

Regarding your specific problem, a Mediator might help track changes in the general population (growing or shrinking).

0

精彩评论

暂无评论...
验证码 换一张
取 消