Design Patterns in Go: Flyweight, or go easy on your memory

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:

  1. We create a new FlyweightFactory
  2. We try and get a Flyweight from it.
  3. We set its internal state
  4. And we repeats steps 2 and 3 with another Flyweight
  5. 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:

  1. Make the flyweightfactory threadsafe.
  2. Use generics to make the whole pattern more flexible.

Leave a Reply

Your email address will not be published. Required fields are marked *