Skip to content

Latest commit

 

History

History
218 lines (171 loc) · 8.6 KB

associated-data.md

File metadata and controls

218 lines (171 loc) · 8.6 KB

The Associated Data Pattern

The associated data pattern allows a class C that is used by multiple other classes outside its own module (which don't control or know the lifetime of C) to store data that is logically "per instance of C". This document will call those other classes the "consumer classes" of C.

Imagine that you have a class Pokemon, which is used by two other classes, PokemonGroomer and PokemonNurse. A PokemonGroomer wishes to store the last time a given Pokemon got a bath, and a PokemonNurse wishes to store the last time a given Pokemon got a checkup.

You might implement that this way:

class Pokemon {
  ...
  Time last_bath_;
  Time last_checkup_;
  ...
};

bool PokemonNurse::NeedsCheckup(Pokemon* p) {
  return Time::Now() - p->last_checkup_ > kCheckupInterval;
}

but then Pokemon ends up containing (and being responsible for enforcing invariants on) data that is only for the use of other objects, which might live in entirely separate modules.

You might instead do this:

class PokemonNurse {
  ...
  map<Pokemon*, Time> last_checkup_;
  ...
};

bool PokemonNurse::NeedsCheckup(Pokemon* p) {
  if (!last_checkup_.contains(p))
    last_checkup_[p] = 0;
  return Time::Now() - last_checkup_[p] > kCheckupInterval;
}

but another problem appears: PokemonNurse has to know when Pokemon are released into the wild so it can clean up the map, which adds extra bookkeeping.

The associated data pattern would look like this:

class Pokemon {
  ...
  struct Data {
    virtual ~Data();
  };
  map<Key, unique_ptr<Data>> user_data_;
  ...
};

class PokemonNurse::PokemonData : public Pokemon::Data {
  Time last_checkup_;
};

bool PokemonNurse::NeedsCheckup(Pokemon* p) {
  if (!p->user_data_[PokemonNurse::kDataKey])
    p->user_data_[PokemonNurse::kDataKey] = make_unique<PokemonData>();
  return Time::Now() - p->user_data_[PokemonNurse::kDataKey].last_checkup_
      > kCheckupInterval;
}

This way, Pokemon manages the lifetime of the per-Pokemon data, but PokemonNurse manages the invariants of the data, and only PokemonNurse is aware of the per-Pokemon data belonging to PokemonNurse.

Use this pattern when:

  • You have a central class C, and many places in your code want to store some data for each instance of C.
  • C can't directly contain your data for layering or code structure reasons.
  • The data being stored is ephemeral and your consumer classes don't care about its lifetime.

Don't use this pattern when:

  • The data being stored must expose any behavior other than a destructor to the central class; in that case, you need a richer pattern than this one.
  • The data being stored has a complex lifetime.
  • Deleting the data being stored has side effects.
  • There might be a very large amount (thousands) of associated data entries for a particular client class; in that case, a domain-specific data model will provide better efficiency.
  • The central class C and the consumer classes are close enough that it would make sense for C to store the data directly.

Alternatives / See also:

  • Storing data for consumer classes directly on the object of class C.
  • Having the consumer store a map between objects of class C and the consumer's C-specific data.

How to use this pattern in Chromium:

The two most commonly-used instances of this pattern in Chromium are SupportsUserData (especially WebContentsUserData and WebStateUserData) and KeyedService (usually via BrowserContextKeyedServiceFactory and BrowserStateKeyedServiceFactory).

SupportsUserData

SupportsUserData is a mixin-type class that allows consumer classes to stash data on the class with the mixin. It exposes a very small interface SupportsUserData::Data which stored data items implement (in fact, any class with a virtual destructor already implements it), and adds a handful of new methods to the class with the mixin:

Data* GetUserData(const void* key);
void SetUserData(const void* key, std::unique_ptr<Data> data);
void RemoveUserData(const void* key);

For example, in //net, URLRequest inherits from SupportsUserData, so if you were running a RequestNicenessService that wanted to annotate URLRequests with a niceness value at one point and use that value later, you might do:

class RequestNicenessService {
  static char kRequestDataKey;

  void SetRequestNiceness(net::URLRequest* request, int niceness);
  int GetRequestNiceness(net::URLRequest* request);
};

struct NicenessData : public SupportsUserData::Data {
  int niceness;
};

void RequestNicenessService::SetRequestNiceness(...) {
  request->SetUserData(&kRequestDataKey,
                       make_unique<NicenessData>(niceness));
}

int RequestNicenessService::GetRequestNiceness(...) {
  SupportsUserData::Data* data = request->GetUserData(&kRequestDataKey);
  if (data)
    return static_cast<NicenessData*>(data)->niceness;
  return -1;
}

Or, if there might be multiple RequestNicenessService instances, you could use the address of the RequestNicenessService instance itself as the key for the UserData on the URLRequest. That's actually a more common pattern, but for a singleton instance using the address of a static is also fine.

WebContentsUserData & TabHelper

For the specific very common case of wanting to attach a SupportsUserData::Data to a WebContents, the helper class WebContentsUserData exists. This templated class handles casting to and from your concrete type for you. It is commonly used with the related TabHelper pattern.

KeyedService & BrowserContextKeyedServiceFactory

KeyedService is an abstract interface that implements this pattern, but with an additional notion of dependencies between different pieces of associated data. It is usually concretely used via BrowserContextKeyedServiceFactory, by subclassing BrowserContextKeyedServiceFactory and overriding the BuildServiceInstanceFor method. Since a Profile is a BrowserContext, this provides a way to store data associated with a Profile. For example, if you were creating a UserNicenessController, you might do:

class UserNicenessData : public KeyedService {
 public:
  static UserNicenessData* FromProfile(Profile* profile);
  int niceness;

  // Maybe also:
  //   void Shutdown() override;
  // if you need to participate in KeyedService's two-phase destruction
  // protocol.
};

class UserNicenessController :
      public BrowserContextKeyedServiceFactory {
  ...
};

// static
UserNicenessData*
UserNicenessData::FromProfile(Profile* profile) {
  return static_cast<UserNicenessData*>(
      UserNicenessController::GetInstance()->
          GetServiceForBrowserContext(profile, true));
}

// in code somewhere:
UserNicenessData::FromProfile(profile)->niceness++;

Any code that needs the UserNicenessData for a given Profile can call UserNicenessData::FromProfile, and KeyedService and BrowserContextKeyedServiceFactory will handle creation and lifetime as needed, as well as handling any dependencies UserNicenessData might declare on other services.

Note that the naming of KeyedService implies that it is intended for services, as does the presence of a dependency mechanism, but the same approach can be used to store plain data, as in this example.

Another note: Since Profile already SupportsUserData, if you simply want to store data on the profile, it is easier to do it that way rather than use KeyedService.

A third note: on iOS, use BrowserStateKeyedServiceFactory instead, which attaches a KeyedService to a BrowserState.