1
头图

Review the Flyweight model and consider the various issues in implementing it.

Prologue

Skip

FlyWeight Pattern

theory

Flyweight mode is a structural design mode that extracts and maintains the same constituent elements of complex objects separately. These same constituent elements are called shared elements, and they are uniquely managed in a single container, and complex objects only need to hold a reference to the unique instance, without the need to repeatedly create such same elements, so as to be able to Significantly reduce memory usage.

Take the word processor as an example. Each character has independent special attributes that distinguish it from other characters: such as font style, background, border, alignment, and so on. If all characters in a document are stored separately with a copy of all its attributes, then this will be a huge memory requirement. But considering that a large number of characters (for example, 1000) may have the same attribute of "Arial, 9pt", then in fact we only need to store a separate font style attribute of "Arial, 9pt", and one character only You need a pointer to the font style attribute, which is much more economical than 1000 font style attribute copies of 1000 characters.

There are quite a few similar cases. For example, each particle in the example system (such as bullets, shrapnel, or enemy aircraft) has some of the same attributes (such as color, outline, etc.), occupying a large area, but the value is the same .

Factory mode

It is easy to think that we can manage Flyweight objects in a factory on-site. When a customer requests a flyweight object with a specific value, the factory will retrieve whether the flyweight exists from a dictionary, and then return the reference of the element to the customer. If the flyweight does not exist yet, the factory will create it and then return the reference.

Immutability

According to traditional terms, the Flyweight model requires these same parts (Flyweight, the same constituent elements) to be immutable. But this is not an iron law.

One way is to take a Flyweight as a whole, and we can modify the Flyweight reference held by the object as a whole.

For example, we are modifying the font style of a word in the word processor from "Song Ti, 9pt" to "HeiTi, 12pt", then we can directly modify the reference point. In other words, we provide an overall modification interface such as character.apply_font_style(font_style& style)

Another method can be modified from a finer granularity, such as changing from "Song Ti, 9pt" to "Song Ti, 10pt", but when the change occurs, try to verify the reference of the new value from the factory. In other words, we provide character.set_font_size(float pt) , but remember to check the flyweight factory (manager) during its implementation to update internal references.

C++ implementation

The traditional flyweight mode has such sample code:

namespace hicc::dp::flyweight::basic {

  /**
     * flyweight Design Pattern
     *
     * Intent: Lets you fit more objects into the available amount of RAM by sharing
     * common parts of state between multiple objects, instead of keeping all of the
     * data in each object.
     */
  struct shared_state {
    std::string brand_;
    std::string model_;
    std::string color_;

    shared_state(const std::string &brand, const std::string &model, const std::string &color)
      : brand_(brand)
        , model_(model)
        , color_(color) {
      }

    friend std::ostream &operator<<(std::ostream &os, const shared_state &ss) {
      return os << "[ " << ss.brand_ << " , " << ss.model_ << " , " << ss.color_ << " ]";
    }
  };

  struct unique_state {
    std::string owner_;
    std::string plates_;

    unique_state(const std::string &owner, const std::string &plates)
      : owner_(owner)
        , plates_(plates) {
      }

    friend std::ostream &operator<<(std::ostream &os, const unique_state &us) {
      return os << "[ " << us.owner_ << " , " << us.plates_ << " ]";
    }
  };

  /**
     * The flyweight stores a common portion of the state (also called intrinsic
     * state) that belongs to multiple real business entities. The flyweight accepts
     * the rest of the state (extrinsic state, unique for each entity) via its
     * method parameters.
     */
  class flyweight {
    private:
    shared_state *shared_state_;

    public:
    flyweight(const shared_state *o)
      : shared_state_(new struct shared_state(*o)) {
      }
    flyweight(const flyweight &o)
      : shared_state_(new struct shared_state(*o.shared_state_)) {
      }
    ~flyweight() { delete shared_state_; }
    shared_state *state() const { return shared_state_; }
    void Operation(const unique_state &unique_state) const {
      std::cout << "flyweight: Displaying shared (" << *shared_state_ << ") and unique (" << unique_state << ") state.\n";
    }
  };

  /**
     * The flyweight Factory creates and manages the flyweight objects. It ensures
     * that flyweights are shared correctly. When the client requests a flyweight,
     * the factory either returns an existing instance or creates a new one, if it
     * doesn't exist yet.
     */
  class flyweight_factory {
    std::unordered_map<std::string, flyweight> flyweights_;
    std::string key(const shared_state &ss) const {
      return ss.brand_ + "_" + ss.model_ + "_" + ss.color_;
    }

    public:
    flyweight_factory(std::initializer_list<shared_state> lists) {
      for (const shared_state &ss : lists) {
        this->flyweights_.insert(std::make_pair<std::string, flyweight>(this->key(ss), flyweight(&ss)));
      }
    }

    /**
     * Returns an existing flyweight with a given state or creates a new one.
     */
    flyweight get(const shared_state &shared_state) {
      std::string key = this->key(shared_state);
      if (this->flyweights_.find(key) == this->flyweights_.end()) {
        std::cout << "flyweight_factory: Can't find a flyweight, creating new one.\n";
        this->flyweights_.insert(std::make_pair(key, flyweight(&shared_state)));
      } else {
        std::cout << "flyweight_factory: Reusing existing flyweight.\n";
      }
      return this->flyweights_.at(key);
    }
    void list() const {
      size_t count = this->flyweights_.size();
      std::cout << "\nflyweight_factory: I have " << count << " flyweights:\n";
      for (std::pair<std::string, flyweight> pair : this->flyweights_) {
        std::cout << pair.first << "\n";
      }
    }
  };

  // ...
  void AddCarToPoliceDatabase(
    flyweight_factory &ff,
    const std::string &plates, const std::string &owner,
    const std::string &brand, const std::string &model, const std::string &color) {
    std::cout << "\nClient: Adding a car to database.\n";
    const flyweight &flyweight = ff.get({brand, model, color});
    // The client code either stores or calculates extrinsic state and passes it
    // to the flyweight's methods.
    flyweight.Operation({owner, plates});
  }

} // namespace hicc::dp::flyweight::basic

void test_flyweight_basic() {
  using namespace hicc::dp::flyweight::basic;

  flyweight_factory *factory = new flyweight_factory({ {"Chevrolet", "Camaro2018", "pink"}, {"Mercedes Benz", "C300", "black"}, {"Mercedes Benz", "C500", "red"}, {"BMW", "M5", "red"}, {"BMW", "X6", "white"} });
  factory->list();

  AddCarToPoliceDatabase(*factory,
                         "CL234IR",
                         "James Doe",
                         "BMW",
                         "M5",
                         "red");

  AddCarToPoliceDatabase(*factory,
                         "CL234IR",
                         "James Doe",
                         "BMW",
                         "X1",
                         "red");
  factory->list();
  delete factory;
}

The output is like this:

--- BEGIN OF test_flyweight_basic                     ----------------------

flyweight_factory: I have 5 flyweights:
BMW_X6_white
Mercedes Benz_C500_red
Mercedes Benz_C300_black
BMW_M5_red
Chevrolet_Camaro2018_pink

Client: Adding a car to database.
flyweight_factory: Reusing existing flyweight.
flyweight: Displaying shared ([ BMW , M5 , red ]) and unique ([ James Doe , CL234IR ]) state.

Client: Adding a car to database.
flyweight_factory: Can't find a flyweight, creating new one.
flyweight: Displaying shared ([ BMW , X1 , red ]) and unique ([ James Doe , CL234IR ]) state.

flyweight_factory: I have 6 flyweights:
BMW_X1_red
Mercedes Benz_C300_black
BMW_X6_white
Mercedes Benz_C500_red
BMW_M5_red
Chevrolet_Camaro2018_pink
--- END OF test_flyweight_basic                       ----------------------

As you can see, a [ BMW , X1 , red ] like 0613dba941e858 has a large single instance (tens, hundreds or even tens of K bytes), and the reference is just the size of a pointer (usually 64 bytes on 64-bit OS) , Then the final memory savings is very considerable.

FlyWeight Pattern in Metaprogramming

The above example is in the old style. After C++11, we need to use smart pointers and template syntax extensively. After C++17, the better in-situ construction capabilities allow our code to be more meaningful.

flyweight_factory

One idea is that we think that a flyweight factory that is as versatile as possible may be beneficial to code writing. So we try such a Flyweight Factory template:

namespace hicc::dp::flyweight::meta {

  template<typename shared_t = shared_state_impl, typename unique_t = unique_state_impl>
  class flyweight {
    std::shared_ptr<shared_t> shared_state_;

    public:
    flyweight(flyweight const &o)
      : shared_state_(std::move(o.shared_state_)) {
      }
    flyweight(shared_t const &o)
      : shared_state_(std::make_shared<shared_t>(o)) {
      }
    ~flyweight() {}
    auto state() const { return shared_state_; }
    auto &state() { return shared_state_; }
    void Operation(const unique_t &unique_state) const {
      std::cout << "flyweight: Displaying shared (" << *shared_state_ << ") and unique (" << unique_state << ") state.\n";
    }
    friend std::ostream &operator<<(std::ostream &os, const flyweight &o) {
      return os << *o.shared_state_;
    }
  };

  template<typename shared_t = shared_state_impl,
                   typename unique_t = unique_state_impl,
                   typename flyweight_t = flyweight<shared_t, unique_t>,
                   typename hasher_t = std::hash<shared_t>>
  class flyweight_factory {
    public:
    flyweight_factory() {}
    explicit flyweight_factory(std::initializer_list<shared_t> args) {
      for (auto const &ss : args) {
        flyweights_.emplace(_hasher(ss), flyweight_t(ss));
      }
    }

    flyweight_t get(shared_t const &shared_state) {
      auto key = _hasher(shared_state);
      if (this->flyweights_.find(key) == this->flyweights_.end()) {
        std::cout << "flyweight_factory: Can't find a flyweight, creating new one.\n";
        this->flyweights_.emplace(key, flyweight_t(shared_state));
      } else {
        std::cout << "flyweight_factory: Reusing existing flyweight.\n";
      }
      return this->flyweights_.at(key);
    }
    void list() const {
      size_t count = this->flyweights_.size();
      std::cout << "\nflyweight_factory: I have " << count << " flyweights:\n";
      for (auto const &pair : this->flyweights_) {
        std::cout << pair.first << " => " << pair.second << "\n";
      }
    }

    private:
    std::unordered_map<std::size_t, flyweight_t> flyweights_;
    hasher_t _hasher{};
  };

} // namespace hicc::dp::flyweight::meta

Then we can use this Flyweight Factory directly in the form of a derived class:

class vehicle : public flyweight_factory<shared_state_impl, unique_state_impl> {
  public:
  using flyweight_factory<shared_state_impl, unique_state_impl>::flyweight_factory;

  void AddCarToPoliceDatabase(
    const std::string &plates, const std::string &owner,
    const std::string &brand, const std::string &model, const std::string &color) {
    std::cout << "\nClient: Adding a car to database.\n";
    auto const &flyweight = this->get({brand, model, color});
    flyweight.Operation({owner, plates});
  }
};

Among them, using flyweight_factory<shared_state_impl, unique_state_impl>::flyweight_factory; is the new syntax after C++17. It copies all the constructors of the parent class to the derived class as-is, so that you don't have to copy and paste the code and then modify the class name.

In the vehicle template class, we use the default flyweight<shared_t, unique_t> , but you can modify it in the template parameters of flyweight_factory

Test code

void test_flyweight_meta() {
    using namespace hicc::dp::flyweight::meta;

    auto factory = std::make_unique<vehicle>(
            std::initializer_list<shared_state_impl>{
                    {"Chevrolet", "Camaro2018", "pink"},
                    {"Mercedes Benz", "C300", "black"},
                    {"Mercedes Benz", "C500", "red"},
                    {"BMW", "M5", "red"},
                    {"BMW", "X6", "white"}});

    factory->list();

    factory->AddCarToPoliceDatabase("CL234IR",
                                    "James Doe",
                                    "BMW",
                                    "M5",
                                    "red");

    factory->AddCarToPoliceDatabase("CL234IR",
                                    "James Doe",
                                    "BMW",
                                    "X1",
                                    "red");
    factory->list();
}

Attach

We use slightly different basic classes shared_state_impl and unique_state_impl :

namespace hicc::dp::flyweight::meta {
  struct shared_state_impl {
    std::string brand_;
    std::string model_;
    std::string color_;

    shared_state_impl(const std::string &brand, const std::string &model, const std::string &color)
      : brand_(brand)
        , model_(model)
        , color_(color) {
      }
    shared_state_impl(shared_state_impl const &o)
      : brand_(o.brand_)
        , model_(o.model_)
        , color_(o.color_) {
      }
    friend std::ostream &operator<<(std::ostream &os, const shared_state_impl &ss) {
      return os << "[ " << ss.brand_ << " , " << ss.model_ << " , " << ss.color_ << " ]";
    }
  };
  struct unique_state_impl {
    std::string owner_;
    std::string plates_;

    unique_state_impl(const std::string &owner, const std::string &plates)
      : owner_(owner)
        , plates_(plates) {
      }

    friend std::ostream &operator<<(std::ostream &os, const unique_state_impl &us) {
      return os << "[ " << us.owner_ << " , " << us.plates_ << " ]";
    }
  };
} // namespace hicc::dp::flyweight::meta

namespace std {
  template<>
  struct hash<hicc::dp::flyweight::meta::shared_state_impl> {
    typedef hicc::dp::flyweight::meta::shared_state_impl argument_type;
    typedef std::size_t result_type;
    result_type operator()(argument_type const &s) const {
      result_type h1(std::hash<std::string>{}(s.brand_));
      hash_combine(h1, s.model_, s.color_);
      return h1;
    }
  };
} // namespace std

This is because we use std::hash technology in flyweight_factory to manage the key value of a flyweight, so we must explicitly implement the std::hash specialization version of shared_state_impl.

In this specialized version, we use a special hash_combine function.

hash_combine

This is a very technical concept because it involves a magic number (magicnum) 0x9e3779b9.

We provide an extension of the same name derived from boost::hash_combine hicc-cxx / cmdr-cxx

namespace std {
  template<typename T, typename... Rest>
  inline void hash_combine(std::size_t &seed, T const &t, Rest &&...rest) {
    std::hash<T> hasher;
    seed ^= 0x9e3779b9 + (seed << 6) + (seed >> 2) + hasher(t);
    int i[] = {0, (hash_combine(seed, std::forward<Rest>(rest)), 0)...};
    (void) (i);
  }

  template<typename T>
  inline void hash_combine(std::size_t &seed, T const &v) {
    std::hash<T> hasher;
    seed ^= 0x9e3779b9 + (seed << 6) + (seed >> 2) + hasher(v);
  }
} // namespace std

Its function is to calculate the hash value of a series of objects and combine them.

On how to correctly combine a bunch of hash values, the simpler method is:

std::size_t h1 = std::hash<std::string>("hello");
std::size_t h2 = std::hash<std::string>("world");
std::size_t h = h1 | (h2 << 1);

But there are still more discussions, among which the best method (in C++) that has been recognized is a method derived from boost::hash_combine to implement the code:

seed ^= hasher(v) + 0x9e3779b9 + (seed<<6) + (seed>>2);

For now it is known academic study, which is best of.

So who made such a fantastic golden ratio? The original work that can be tested should be: A Hash Function for Hash Table Lookup or Hash Functions for Hash Table Lookup . The original author Bob Jenkins, originally published in the DDJ publication in 1997, the code is about to take shape in 1996. And this number comes from this expression: $\frac{2^{32}}{\frac{1+\sqrt{5}}{2}}$ ( image-20210907202334864 ).

Epilogue

Well, although it is not too satisfactory, I did implement a C++17 barely more general flyweight_factory template class, and that's it.

:end:


hedzr
95 声望19 粉丝