The Domain Lens Pattern exposes a specific, more restricted view of a complicated class, customized to the needs of a specific domain. The name comes from the metaphor of looking at a class “through a specific lens”, which magnifies parts of the class and hides other parts. This helps decouple clients from the entire implementation of the complicated class.
Here's an example. Suppose we have a class NetworkRequest:
class NetworkRequest { // ... lots of state ... size_t bytes_done() const; size_t bytes_to_do() const; };
and suppose that we are implementing a UI component that displays the status of a download, which is backed by a NetworkRequest. We could implement that like:
class DownloadView : public NetworkRequestObserver { public: // Start displaying the status of the given network request. DownloadView(NetworkRequest* request); // NetworkRequestObserver: void OnRequestProgress(NetworkRequest* request) override; };
But there are two problems with this:
DiskFileRequest
for a load from local disk), that class must subclass NetworkRequest
for DownloadView to be usable with it, which usually involves a lot of empty methods and stub data.Instead, we can define a domain-specific lens, containing only the subset of the data in NetworkRequest
needed to display download progress, like this:
struct DownloadProgress { base::FilePath destination; size_t bytes_done; size_t bytes_to_do; };
and two interfaces for receiving updates on that domain-specific lens:
class DownloadProgressObserver { public: void OnDownloadProgress(const DownloadProgress& progress); }; class DownloadProgressProvider { public: void AddObserver(DownloadProgressObserver* observer); void RemoveObserver(DownloadProgressObserver* observer); DownloadProgress GetCurrentProgress() const; };
at which point DownloadView
is:
class DownloadView : public DownloadProgressObserver { public: DownloadView(DownloadProgressProvider* provider); };
or perhaps, using a callback rather than an observer class:
using DownloadProgressCallback = base::RepeatingCallback<void(const DownloadProgress& progress)>; class DownloadProgressProvider { public: base::CallbackListSubscription RegisterDownloadProgressCallback( DownloadProgressCallback callback); DownloadProgress GetCurrentProgress(); }; class DownloadView { public: DownloadView(DownloadProgressProvider* provider); };
A domain lens makes sense when you have:
A common use of domain lenses is to extract only the data needed to display an object into a separate type, to decouple the object from its display logic. One Chromium example of this is TabRendererData, which encapsulates only the state of a Tab needed to draw that Tab somewhere, without providing access to the Tab's underlying webcontents or any methods for mutating it.
Here's another hypothetical, to illustrate this pattern. We have a class ProfileAttributesEntry, which contains a lot of different pieces of data about a Profile. We might define a type and a new method on ProfileAttributes
, specifically for drawing the profile “chip” that can replace the app menu icon:
// Note that this is a separate top-level type from ProfileAttributes. One could // instead define this as a nested type ProfileAttributes::LensForChip or // something similar, but then any user of this domain lens would need to have // the declaration of ProfileAttributes visible as well, which would cause some // of the coupling we're trying to avoid in the first place. struct ProfileAttributesForChip { gfx::ImageSkia icon; std::string name; ... }; ProfileAttributesForChip ProfileAttributes::ForChip() { ... }
and then have the profile chip code use a ProfileAttributesForChip
instead of a ProfileAttributes
.
Domain lenses are easy to overuse. In general, any given client of a complicated class uses only a subset of that class‘s functionality, but that does not mean each client should have its own lens - that would cause a lot of extra lens types and tangle the code needlessly. This pattern always involves writing extra translation functions and adapters, so it should only be done when the decreased coupling is worth the extra code needed to implement the lens itself. Clients that don’t have their own domain lens on an object can continue to use the object itself as usual.