Inversion of Control

“Inversion of control” is a design pattern used to allow users of a framework or library (often called clients) to customize the behavior of the framework.

Our Example

Examples in this document will be given by extending or modifying this example API, which is hopefully self-explanatory:

class StringKVStore {
 public:
  StringKVStore();
  virtual ~StringKVStore();

  using KeyPredicate = base::RepeatingCallback<bool(const string&)>;

  void Put(const string& key, const string& value);
  void Remove(const string& key);
  void Clear();

  string Get(const string& key) const;
  set<string> GetKeys() const;
  set<string> GetKeysMatching(const KeyPredicate& predicate) const;

  void SaveToPersistentStore();
};

What is inversion of control?

Normally, client code calls into the library to do operations, so control flows from a high-level class to a low-level class:

void YourFunction() {
  // GetKeys() calls into the StringKVStore library
  for (const auto& key : kv_store_.GetKeys()) {
    ...
  }
}

In “inverted” control flow, the library calls back into your code after you call into it, so control flows back from a low-level class to a high-level class:

bool IsKeyInteresting(const string& key) { ... }

void YourFunction() {
  StringKVStore::KeyPredicate predicate =
      base::BindRepeating(&IsKeyInteresting);
  // GetKeysMatching() calls into the StringKVStore library, but it calls
  // back into IsKeyInteresting defined in this file!
  for (const auto& key : kv_store_.GetKeysMatching(predicate)) {
    ...
  }
}

It is also often inverted in the Chromium dependency sense. For example, in Chromium, code in //content can't call, link against, or generally be aware of code in //chrome - the normal flow of data and control is only in one direction, from //chrome “down” to //content. When //content calls back into //chrome, that is an inversion of control.

Abstractly, inversion of control is defined by a low-level class defining an interface that a high-level class supplies an implementation of. In the example fragment given above, StringKVStore defines an interface called StringKVStore::KeyPredicate, and YourFunction supplies an implementation of that interface - namely the bound instance of IsKeyInteresting. This allows the low-level class to use functionality of the high-level class without being aware of the specific high-level class's existence, or a high-level class to plug logic into a low-level class.

There are a few main ways this is done in Chromium:

  • Callbacks
  • Observers
  • Listeners
  • Delegates

Inversion of control should not be your first resort. It is sometimes useful for solving specific problems, but in general it is overused in Chromium.

Callbacks

Callbacks are one of the simplest ways to do inversion of control, and often are all you need. Callbacks can be used to split out part of the framework's logic into the client, like so:

void StringKVStore::GetKeysMatching(const KeyPredicate& predicate) {
  set<string> keys;
  for (const auto& key : internal_keys()) {
    if (predicate.Run(key))
      keys.insert(key);
  }
  return keys;
}

where predicate was supplied by the client of StringKVStore::GetKeysMatching. They can also be used for the framework library to notify clients of events, like so:

void StringKVStore::Put(const string& key, const string& value) {
  ...
  // In real code you would use CallbackList instead, but for explanatory
  // purposes:
  for (const auto& callback : key_changed_callbacks_)
    callback.Run(...);
}

making use of Subscription.

Callbacks can also be used to supply an implementation of something deliberately omitted, like so:

class StringKVStore {
  using SaveCallback = base::RepeatingCallback<void(string, string)>;
  void SaveToPersistentStore(const SaveCallback& callback);
};

Observers

An “observer” receives notifications of events happening on an object. For example, an interface like this might exist:

class StringKVStore::Observer {
 public:
  virtual void OnKeyChanged(StringKVStore* store,
                            const string& key,
                            const string& from_value,
                            const string& to_value) {}
  virtual void OnKeyRemoved(StringKVStore* store,
                            const string& key,
                            const string& old_value) {}
  ...
}

and then on the StringKVStore class:

class StringKVStore {
 public:
  ...
  void AddObserver(Observer* observer);
  void RemoveObserver(Observer* observer);
}

So an example of a StringKVStore::Observer might be:

class HelloKeyWatcher : public StringKVStore::Observer {
 public:
  void OnKeyChanged(StringKVStore* store,
                    const string& key,
                    const string& from_value,
                    const string& to_value) override {
    if (key == "hello")
      ++hello_changes_;
  }
  void OnKeyRemoved(StringKVStore* store,
                    const string& key,
                    const string& old_value) override {
    if (key == "hello")
      hello_changes_ = 0;
  }
}

where the StringKVStore arranges to call the relevant method on each StringKVStore::Observer that has been added to it whenever a matching event happens.

Use an observer when:

  • More than one client may care to listen to events happening
  • Clients passively observe, but do not modify, the state of the framework object being observed

Listeners

A listener is an observer that only observes a single type of event. These were very common in C++ and Java before the introduction of lambdas, but these days are not as commonly seen, and you probably should not introduce new listeners - instead, use a plain Callback.

Here's an example:

class StringKVStore::ClearListener {
 public:
  virtual void OnCleared(StringKVStore* store) = 0;
}

Use a listener when:

  • There is only a single client listener instance at most per framework object
  • There is only a single event being listened for

Delegates

A delegate is responsible for implementing part of the framework that is deliberately missing. While observers and listeners are generally passive with respect to the framework object they are attached to, delegates are generally active.

One very common use of delegates is to allow clients to make policy decisions, like so:

class StringKVStore::Delegate {
 public:
  virtual bool ShouldPersistKey(StringKVStore* store, const string& key);
  virtual bool IsValidValueForKey(StringKVStore* store,
                                  const string& key,
                                  const string& proposed_value);
};

Another common use is to allow clients to inject their own subclasses of framework objects that need to be constructed by the framework, by putting a factory method on the delegate:

class StringKVStore::Delegate {
 public:
  virtual unique_ptr<StringKVStoreBackend>
      CreateBackend(StringKVStore* store);
}

And then these might exist:

class MemoryBackedStringKVStoreDelegate : public StringKVStore::Delegate;
class DiskBackedStringKVStoreDelegate : public StringKVStore::Delegate;
...

Use a delegate when:

  • There needs to be logic that happens synchronously with what's happening in the framework
  • It does not make sense to have a decision made statically per instance of a framework object

Observer vs Listener vs Delegate

If every call to the client could be made asynchronous and the API would still work fine for your use case, you have an observer or listener, not a delegate.

If there might be multiple interested client objects instead of one, you have an observer, not a listener or delegate.

If any method on your interface has any return type other than void, you have a delegate, not an observer or listener.

You can think of it this way: an observer or listener interface notifies the observer or listener of a change to a framework object, while a delegate usually helps cause the change to the framework object.

Callbacks vs Observers/Listeners/Delegates

Callbacks have advantages:

  • No separate interface is needed
  • Header files for client classes are not cluttered with the interfaces or methods from them
  • Client methods don‘t need to use specific names, so the name-collision problems above aren’t present
  • Client methods can be bound (using Bind) with any needed state, including which object they are attached to, so there is no need to pass the framework object of interest back into them
  • The handler for an event is placed in object setup, rather than being implicit in the presence of a separate method
  • They sometimes save creation of “trampoline” methods that simply discard or add extra parameters before invoking the real handling logic for an event
  • Forwarding event handlers is a lot easier, since callbacks can easily be passed around by themselves
  • They avoid multiple inheritance

They also have disadvantages:

  • They can lead to deeply-nested setup code
  • Callback objects are heavyweight (performance and memory wise) compared to virtual method calls

Design Tips

  1. Observers should have empty method bodies in the header, rather than having their methods as pure virtuals. This has two benefits: client classes can implement only the methods for events they care to observe, and it is obvious from the header that the base observer methods do not need to be called.

  2. Similarly, delegates should have sensible base implementations of every method whenever this is feasible, so that client classes (subclasses of the delegate class) can concern themselves only with the parts that are relevant to their use case.

  3. When inverting control, always pass the framework object of interest back to the observer/listener/delegate; that allows the client, if it wants to, to reuse the same object as the observer/listener/delegate for multiple framework objects. For example, if ButtonListener (given above) didn't pass the button in, the same ButtonListener instance could not be used to listen to two buttons simultaneously, since there would be no way to tell which button received the click.

  4. Large inversion-of-control interfaces should be split into smaller interfaces when it makes sense to do so. One notorious Chromium example is WebContentsObserver, which observes dozens of different events. Whenever any of these events happens, every registered WebContentsObserver has to be notified, even though virtually none of them might care about this specific event. Using smaller interfaces helps with this problem and makes the intent of installing a specific observer clearer.

  5. The framework class should not take ownership of observers or listeners. For delegates the decision is less clear, but in general, err on the side of not taking ownership of delegates either. It is common to hold raw pointers to observers and listeners, and raw or weak pointers to delegates, with lifetime issues managed via AddObserver/RemoveObserver or the helper classes discussed below.

  6. Depending on your application and how widely-used you expect your observer, listener, or delegate to be, you should probably use names that are longer and more specific than you might otherwise. This is because client classes may be implementing multiple inversion-of-control interfaces, so it is important that their method names not collide with each other. For example, instead of having PageObserver::OnLoadStarted, you might have PageObserver::OnPageLoadStarted to reduce the odds of an unpleasant collision with NetworkRequestObserver::OnLoadStarted (or similar). Note that callbacks entirely avoid this problem.

  7. A callback is probably a better fit for what you're trying to do than one of the other patterns given above!

Inversion of Control in Chromium

Some key classes in //base:

And some production examples:

When Not To Use This Pattern

Inverted control can be harder to reason about, and more expensive at runtime, than other approaches. In particular, beware of using delegates when static data would be appropriate. For example, consider this hypothetical interface:

class StringKVStore::Delegate {
  virtual bool ShouldSaveAtDestruction() { return true; }
}

It should be clear from the naming that this method will only be called once per StringKVStore instance and that its value cannot meaningfully change within the lifetime of a given instance; in this case, “should save at destruction” should instead be a parameter given to StringKVStore directly.

A good rule of thumb is that any method on a delegate that:

  • Will only be called once for a given framework object, or
  • Has a value that can't meaningfully change for a given framework object, and
  • Serves primarily to return that value, rather than doing some other work like constructing a helper object

should be a property on the framework object instead of a delegate method.