Introduction
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:
Implementation in Go
Open a commandline or terminal in an empty directory and type:
go mod init flyweight_pattern
Now open your favourite IDE and add a main.go file.
Start with the following:
package main
import (
"fmt"
)
Now we will define the Flyweight:
type Flyweight struct {
number int32
}
func (f *Flyweight) setState(number int32) {
f.number = number
}
func (f *Flyweight) getState() int32 {
return f.number
}
For simplicity I chose an int32 as the state, but this could of course be any object. A nice extension would be to use generics, but I will come to that in a later article.
As you can see, the Flyweight structure has a state, which is accessed through two accessor-functions.
The Flyweight-structures need to be managed, and that is the task of the FlyweightFactory:
type FlyweightFactory struct {
elements map[int32]*Flyweight
}
func NewFactory() *FlyweightFactory {
return &FlyweightFactory{
elements: make(map[int32]*Flyweight),
}
}
func (f *FlyweightFactory) getFlyweight(number int32) *Flyweight {
if _, ok := f.elements[number]; !ok {
f.elements[number] = &Flyweight{number: number}
}
return f.elements[number]
}
This code is a bit more involved:
- The FlyweightFactory struct has a (hash)map or dictionary, with int32 as keys and Flyweight structures as values.
- In the NewFactory method we construct this (hash)map through use of make keyword.
- The getFlyWeight method does two things: if the key does not exists (that is checked for in the first if statement), then a new Flyweight is made and added to the map
- If it does exist or is just added, it is returned to the caller.
Now we can test it:
func main() {
factory := NewFactory()
flyweightA := factory.getFlyweight(1)
flyweightA.setState(11)
flyweightB := factory.getFlyweight(2)
flyweightB.setState(22)
fmt.Println(flyweightA.getState())
fmt.Println(flyweightB.getState())
}
Again, line by line:
- We create a new FlyweightFactory
- We try and get a Flyweight from it.
- We set its internal state
- And we repeats steps 2 and 3 with another Flyweight
- Finally we print them
Conclusion
As you can see the Flyweight pattern is not a difficult pattern to implement and it can be useful in many situations, not only for wordprocessors. You can use it as an intelligent cache, or in some cases even as a replacement for a remote proxy (or in combination with such a pattern)
There are two extensions which I would like to add to this pattern, but I will do that in later articles:
- Make the flyweightfactory threadsafe.
- Use generics to make the whole pattern more flexible.