Browser Components Cookbook

being the story of how a fresh new component is born, or how a veteran feature is rescued from a tangled web of ugly dependencies and reborn as a shiny component.

Overview

A talk was recently given on some of the same material as this page, and it may serve as a good high-level overview before diving into the documentation. See the "Life of a Browser Component" file linked to at the bottom of this page for just the slides, or watch the video of the presentation.

Motivation and Layering

We’ve started having multiple top-level applications.  For one of these (Chrome for Android), the necessary approach was to #ifdef in //chrome, but we want to avoid this for future top-level applications.


The //components layer is a place for browser features that embedders of //content may want to add.  Most of the components currently there are features that previously lived in //chrome but were extracted so that they could be reused by other top-level applications such as //android_webview.


New features that may get used by more than one top-level application should be written as components.  For top-level applications other than Chrome for Android, features they wish to reuse should first be extracted into //components.  This document is a cookbook -type guide for how to create a new component, and how to extract an existing feature from //chrome into a component.




Creating a new Browser Component

Creating a new component is straightforward.  Follow the rules in the design document, and the examples already in the //components directory. TL;DR version:

  • A component named xyz lives in //components/xyz.

  • Code in xyz should be enclosed in namespace xyz.

  • There should be a strict //components/xyz/DEPS to enforce that the component depends only on lower layers.

    • //content and below

    • Other //components, in a strictly tree-shaped graph - no circular dependencies.

  • A component may depend concretely on lower layers and other components.  For stuff it needs its embedder to fulfill, it should define delegate interfaces that the embedder will implement and provide.

  • A component used only by the browser process contains code directly in its directory.

  • A component used by multiple process types has a directory per process, e.g.

    • //components/xyz/browser

    • //components/xyz/renderer

    • //components/xyz/common

  • A component is a separate dynamic library in the components build.

  • Create, own and configure the component in the embedder layer, e.g. in //chrome/browser.

Extracting an existing feature to a Browser Component

These are the typical steps to extract a feature:

  1. Identify a feature you need to extract

  2. Work from "leaf nodes"

    • The feature may map to more than one component, or may depend on other bits of //chrome that need to be componentized first. You need to start by componentizing the bits that don’t have major dependencies on the rest of //chrome.

  3. For each component-to-be that makes up the feature, write a strict DEPS file as if it were already a component

    • First, write a strict DEPS file for the directory the feature is in, e.g. //chrome/browser/xyz, that disallows use of the rest of //chrome.

    • This DEPS file will initially cause checkdeps to fail.

    • Run tools/checkdeps/checkdeps.py --generate-temp-rules chrome/browser/xyz to generate a list of temporary exceptions, that you can paste into your DEPS file to let checkdeps pass.

    • Temporary exceptions (!-prefixed rules) cause a presubmit warning if people add more instances of the includes in question, so they discourage others from working against your goal.

  4. Reduce the set of temporarily-allowed dependencies to zero

    • This is by far the biggest part of the work; see the section below on approaches to breaking dependencies.

  5. Move the code to its //components/xyz directory

    • We have tools to automate this. You can use tools/git/move_source_file.py to move individual files or files matching a wildcard, or tools/git/mass-rename.sh if you have already invoked git mv (but not committed) to fix up the files you moved.

  6. Fix up .gypi files and add export declarations to build it as a component

    • See examples in other components, e.g. //components/webdata/common/webdata_export.h and its uses.

Breaking Dependencies - Here Comes the Cookbook Part

Please feel free to add new sections, add clarifications and examples to this section, etc. If you don't have edit rights to the site, feel free to add comments at the bottom of the page with additional recipes.

Dependency Inversion

Dependency inversion is the most fundamental approach to breaking dependencies. This is the pattern where you switch from object Foo depending concretely on object Bar, and instead object Foo defines an interface FooDelegate that it needs fulfilled by any embedder; this interface is then implemented by Bar, which passes a FooDelegate pointer to itself or its delegate implementation to Foo.


By using this pattern, it is easy to push knowledge of minor dependencies out of your component, and possible to remove knowledge of some larger dependencies (but see the next section).


Examples:

Large Dependencies: Do them First

Sometimes you’ll find that the feature you want to extract has a fairly big dependency on a fairly big chunk of code. Using dependency inversion (see above) would result in a large delegate interface or even multiple large delegate interfaces.


In these cases, you’ve probably discovered that the component you want isn’t a “leaf node,” whereas its dependency may be. In these cases you probably want to componentize this dependency first. Exceptions to this might be where the dependency would be fulfilled in entirely different ways, rather than in one common way, by different embedders; if this is the case then a large delegate interface (or set of delegates) may still be appropriate.


Examples:

  • The generic parts of //chrome/browser/webdata needed to be componentized before //chrome/browser/autofill could be componentized, and the Autofill-specific parts extracted into the Autofill component. See //components/webdata and //components/autofill/browser/webdata.

Pass Fundamental Objects

It’s very common in //chrome that an “everything” object such as Profile is passed in to initialize an object or a subsystem, which then turns around and retrieves just a couple of more fundamental objects from the Profile.


We’ve seen this many times, e.g. a Profile being passed and the only things retrieved from it being the PrefService associated with the Profile, and the SequencedTaskRunner for the IO thread.


In cases like this, you should change the code to pass the most fundamental objects possible, rather than passing more complex “everything” or “bag of stuff” objects. See also Law of Demeter.


Examples:

Splitting Objects

Sometimes, you will encounter an object that brings in a lot of dependencies, but at the same time seems to have functionality that is core to the component.  It might e.g. be an object that has some internal state machine that the component needs to use, and also initiates or participates in a bunch of UI interactions.


In cases like this, consider whether the object needs to be split into multiple objects, where one is the core business logic (which stays with the component) and another is the part that brings in all the dependencies (this could stay with the embedder).


Examples:

Clean up APIs

It's worth noting that the programming interfaces that result from the types of dependency-breaking operations listed above are not always optimal, in fact they are often not as nice as what would have resulted from a feature being written as a component from the start. Once you've finished breaking dependencies, it can certainly be worth going back, taking a fresh look at the interfaces that resulted, and cleaning them up.

Č
Ċ
ď
Jói Sigurðsson,
May 23, 2013, 2:14 PM
Comments