Design Patterns in Rust: Flyweight or go easy on your memory
The flyweight pattern is a pattern that helps minimize memory usage by sharing and reusing data. A common example is wordprocessor where each character not only holds the character like ‘A’ but also things like markup and formatting data. Instead of having each element of a document carry all this data, this markup and formatting data is stored using flyweight patterns, to which the elements refer. This method saves a lot of memory.
So, flyweight objects can have:
- Intrinsic state: invariant and shareable, but also context independent (like aformentioned charachter ‘A’)
- Extrinsic state: that could be the position of the character in the document.
So, what does it look like? Well, a very simple version is this:
Open a commandline or a terminal in an empty directory and type:
cargo new rust_flyweight
cd rust_flyweight
Now open your favourite IDE in this directory and open the main.rs folder
Start by adding this at the top of the file:
use std::collections::HashMap;
We will be using HashMap later. A Hashmap is just a dictionary, where you keys and values. Here we will use strings as keys and FlyWeights as values.
Next it is time to implement the FlyWeight:
struct FlyWeight<'a> {
shared_state: &'a str,
}
impl<'a> FlyWeight<'a> {
fn new(shared_state: &'a str) -> Self {
FlyWeight { shared_state }
}
fn set_state(&mut self, shared_state: &'a str) {
self.shared_state = shared_state;
}
fn get_state(&self) -> &str {
self.shared_state
}
}
A few notes:
- Notice the lifetime specifier? This is to make sure that the state survives as long as the FlyWeight struct.
- The code is quite self-explanatory: one constructor, and two accessor methods.
Now it gets interesting with the FlyWeightFactory:
impl<'a> FlyWeightFactory<'a> {
fn new() -> Self {
FlyWeightFactory {
flyweights: HashMap::new(),
}
}
fn get_flyweight(&mut self, key: &'a str) -> &mut FlyWeight<'a> {
if !self.flyweights.contains_key(key) {
self.flyweights.insert(key, FlyWeight::new(key));
}
self.flyweights.get_mut(key).unwrap()
}
}
Again a few notes:
- Again a lifetime specifier, to make sure the state survives as long as the factory.
- A simple constructor, where the HashMap is initialized.
- The get_flyweight() method does something interesting: if a key does not exist, it is inserted into the HashMap. The found, or in some cases newly constructed FlyWeight is returned from the function.
Time for a testdrive:
fn main() {
let mut factory = FlyWeightFactory::new();
let flyweight_a = factory.get_flyweight("hello");
flyweight_a.set_state("hello world");
println!("{}", flyweight_a.get_state());
let flyweight_b = factory.get_flyweight("world");
flyweight_b.set_state("world hello");
println!("{}", flyweight_b.get_state());
let flyweight_c = factory.get_flyweight("hello");
println!("{}", flyweight_c.get_state());
}
A short breakdown:
- A FlyWeightFactory is constructed
- A Flyweight is requested, and the state changed and printed
- The same process is repeated for another flyweight
- To make sure we get the same flyweight, the first key is requested again and the state printed. You will find that it has the same state as the first FlyWeight.
As you can tell from the code, the FlyWeight pattern is not a difficult pattern to implement, and it can be useful in many situation, not just in wordprocessors. You could also use it as an intelligent cache or in some cases even as a replacement for a remote proxy.
There are two improvements which I would like to make, but I will make those in a follow-up article:
- Make the implementation threadsafe.
- Use generics to make it more flexible.