Michael Safyan

Flyweight

Flyweight Pattern

Overview

In the flyweight pattern, objects are recycled rather than allocated and initialized from scratch.

When to Use

The flyweight pattern is typically used for efficiency reasons. This pattern can be useful with certain types of objects whose construction/initialization is particularly expensive (e.g. threads) or with objects that are frequently compared for equality and whose equality comparison may be expensive (e.g. representations of locales). In these cases, reusing/caching the same object reduces costs (e.g. by eliminating the need to initialize a thread or by allowing equality comparison to be done based on identity / address rather than by contents).

When NOT to Use

The flyweight pattern should not be used unless there is actually an efficiency concern that needs to be addressed and it has been measured / demonstrated that using this pattern will solve that problem. The flyweight pattern also should not be used with mutable objects that need to be reset/reinitialized when recycled.

General Pattern

There are many different ways in which the flyweight pattern may be realized. While this is true of other patterns, as well, there seems to be much greater variability with flyweight. In some cases, the flyweight construction is hidden from view (that is, you explicitly allocate an object that, under the hood, recycles a helper class), whereas in other cases it is explicit (e.g. there is a distinct class that is responsible for the lifecycle management of the flyweight objects that is separate from the flyweight object, itself). My personal recommendation is to use the latter approach, as it makes it easier for applications to control the caching and lifetime of the flyweight objects (and, for example, to bound the size of the cache or to instantiate objects directly, bypassing the cache, when appropriate -- such as in test code).

In the explicit management case, there is typically a class that contains the word "Pool", "Cache", "Manager", or "Loader" that is reponsible for flyweight construction as in the following two outlines of this pattern:

Java

       // This is the kind of object that gets cached as a flyweight
       public interface NameOfFlyWeightObject {
          // ...
       }

       // This is an example implementation of the flyweight interface
       public class NameOfFlyWeightObjectImpl
           implements NameOfFlyWeightObject  {
          // ...
       }

       // This is the interface for the cache that maintains/recycles the objects.
       public interface NameOfFlyWeightObjectPool {
          // The parameters of this function typically match those of the constructor
          // for "NameOfFlyWeightObjectImpl" (or whatever implementation is used). This
          // function lazily constructs the object and caches the result so that
          // invocations with the same parameters return the same exact object. This is
          // very similar to the factory pattern, except that in the factory case,
          // the result is not cached in this way.
          //
          // Note that in some variations of this pattern, this could return null (e.g.
          // if the cache has a maximum size that cannot be exceeded). In such cases,
          // that should be documented and the function should be marked "@Nullable"
          // to clearly indicate that the method can return null.
          NameOfFlyWeightObject get(ParamType1 param1, ..., ParamTypeN paramN);
       }
    

C++

       // This is the kind of object that gets cached as a flyweight
       class NameOfFlyWeightObject {
        public:
         virtual ~NameOfFlyWeightObject();
         // ...
        protected:
         NameOfFlyWeightObject();
       };

       // This is an example implementation of the flyweight interface
       class NameOfFlyWeightObjectImpl final : public NameOfFlyWeightObject {
          // ...
       };

       // This is the interface for the cache that maintains/recycles the objects.
       class NameOfFlyWeightObjectPool {
        public:
          virtual ~NameOfFlyWeightObjectPool();

          // The parameters of this function typically match those of the constructor
          // for "NameOfFlyWeightObjectImpl" (or whatever implementation is used). This
          // function lazily constructs the object and caches the result so that
          // invocations with the same parameters return the same exact object. This is
          // very similar to the factory pattern, except that in the factory case,
          // the result is not cached and the caller takes ownership over the object;
          // by contrast, in the flyweight pattern,  the "pool" object owns the result
          // that is returned from this function.
          //
          // It should be noted that, in this example, we've assumed that construction
          // will always succeed (or will throw an exception). In other cases, there
          // may be cause to allow a failure to be indicated through the result of this
          // function, in which case using a constant pointer rather than a constant
          // reference would be more appropriate (to allow "nullptr" as a result). In
          // those cases, it should be clearly documented both the conditions in which
          // a null result will be returned and the fact that the returned pointer is
          // owned by the pool, not by the caller.
          //
          // It should also be noted that, in this example, we've decided to mark this
          // method "const" under the assumption that the internal cache will be marked
          // "mutable". I think it is appropriate to use "const" here, because it is
          // "conceptualy const" in the sense that two invocations of the "Get()"
          // function will always return the same result assuming there are no
          // interleaved non-const functions (e.g. to clear the cache). However, there is
          // a bit of a debate / dispute regarding whether "const" should be used for
          // literal const-ness or conceptual const-ness; those who argue that "const"
          // should only be used when the internal state doesn't change would object to
          // this usage. The bottom line is that you may encounter a similar object both
          // with and without "const". There is no "right" answer, so long as the code
          // base is consistent in how it interprets the meaning of "const".
          virtual const NameOfFlyWeightObject& Get(
              ParamType1 param1, ..., ParamTypeN paramN) const = 0;
        protected:
          NameOfFlyWeightObjectPool();
       };
    
Good Examples

Thread Pool

       // This example is a little bit unusual in that it doesn't return the
       // same object from the pool each time, but rather load balances between
       // the cached objects (possibly allocating more as needed). This is
       // a good example because threads/workers are expensive to create,
       // and so recycling them like this is an efficiency win.
       @Override
       protected void initialize(WorkerPool workerPool) {
          for (Task task : getTaskList()) {
            Worker worker = workerPool.getLeastLoaded();
            worker.add(task);
          }
       }
    

Locale

      // This example illustrates how using the flyweight pattern might
      // speed up a comparison; it might be expensive to load or construct
      // the locale for the first time, but if there is a very limited number
      // of languages and one maintains a mapping from various equivalent
      // names of languages to the corresponding Locale object, using
      // flyweight might simplify and speed up comparisons of these objects.
      bool IsEquivalentLanguage(
          const LocalePool& locale_pool,
          const std::string& locale_string1,
          const std::string& locale_string2) {
        const Locale* locale1 = locale_pool.Get(locale_string1);
        const Locale* locale2 = loclae_pool.Get(locale_string2);
        // Intentionally comparing addresses rather than comparing the
        // dereferenced locales; the pool guarantees that two retrievals
        // of equivalent objects will always return the same one.
        return locale1 == locale2;
      }
    
Bad Examples

Contact

       // This example is bad because the object in question is mutable,
       // and it doesn't make sense to cache a contact by first name...
       // there can be many contacts with the same first name, and doing
       // something like this is non-sensical and is likely to result in
       // information getting corrupted between different contacts.
       // ...
       Contact contact = contactPool.get("FirstName");
       contact.setEmail("someemail@somedomain");
       // ...
    

Vector3D

       // This is a bad example because even if the vector object is immutable,
       // the shear number of possible vectors is so large that caching them
       // like this makes no sense, and constructing them is not expensive
       // enough to make a good case for using a flyweight in the first place.
       // ...
       const Vector3D& v1 = vector_pool.Get(0.5, 1.8, -7.34);
       // ...
    
Real Examples

Curious to see how this design pattern is used in the wild? While we make no claim regarding the "good-ness" or "bad-ness" of these uses, here are a handful of real examples to look at on GitHub that illlustrate this design pattern:

See also
Disclaimer

My statements are my own and do not necessarily reflect the opinions of Google Inc.