Our next pattern is the Flyweight design pattern. It's very commonly used in computer graphics and the video game industry, but not so much in enterprise applications.
Flyweight is a pattern which allows sharing the state of a heavy object between many instances of some type. Imagine that you have to create and store too many objects of some heavy type that are fundamentally equal. You'll run out of memory pretty quickly. This problem can be easily solved with the Flyweight pattern, with additional help of the Factory pattern. The factory is usually in charge of encapsulating object creation, as we saw previously.
Thanks to the Flyweight pattern, we can share all possible states of objects in a single common object, and thus minimize object creation by using pointers to already created objects.
To give an example, we are going to simulate something that you find on betting webpages. Imagine the final match of the European championship, which is viewed by millions of people across the continent. Now imagine that we own a betting webpage, where we provide historical information about every team in Europe. This is plenty of information, which is usually stored in some distributed database, and each team has, literally, megabytes of information about their players, matches, championships, and so on.
If a million users access information about a team and a new instance of the information is created for each user querying for historical data, we will run out of memory in the blink of an eye. With our Proxy solution, we could make a cache of the n most recent searches to speed up queries, but if we return a clone for every team, we will still get short on memory (but faster thanks to our cache). Funny, right?
Instead, we will store each team's information just once, and we will deliver references to them to the users. So, if we face a million users trying to access information about a match, we will actually just have two teams in memory with a million pointers to the same memory direction.
The acceptance criteria for a Flyweight pattern must always reduce the amount of memory that is used, and must be focused primarily on this objective:
Team
struct with some basic information such as the team's name, players, historical results, and an image depicting their shield.Our Team
struct will contain other structs inside, so a total of four structs will be created. The Team
struct has the following signature:
type Team struct { ID uint64 Name string Shield []byte Players []Player HistoricalData []HistoricalData }
Each team has an ID, a name, some image in an slice of bytes representing the team's shield, a slice of players, and a slice of historical data. This way, we will have two teams' ID:
const ( TEAM_A = iota TEAM_B )
We declare two constants by using the const
and iota
keywords. The const
keyword simply declares that the following declarations are constants. iota
is a untyped integer that automatically increments its value for each new constant between the parentheses. The iota
value starts to reset to 0 when we declare TEAM_A
, so TEAM_A
is equal to 0. On the TEAM_B
variable, iota
is incremented by one so TEAM_B
is equal to 1. The iota
assignment is an elegant way to save typing when declaring constant values that doesn't need specific value (like the Pi constant on the math
package).
Our Player
and HistoricalData
are the following:
type Player struct { Name string Surname string PreviousTeam uint64 Photo []byte } type HistoricalData struct { Year uint8 LeagueResults []Match }
As you can see, we also need a Match
struct, which is stored within HistoricalData
struct. A Match
struct, in this context, represents the historical result of a match:
type Match struct { Date time.Time VisitorID uint64 LocalID uint64 LocalScore byte VisitorScore byte LocalShoots uint16 VisitorShoots uint16 }
This is enough to represent a team, and to fulfill Acceptance Criteria 1. You have probably guessed that there is a lot of information on each team, as some of the European teams have existed for more than 100 years.
For Acceptance Criteria 2, the word creation should give us some clue about how to approach this problem. We will build a factory to create and store our teams. Our Factory will consist of a map of years, including pointers to Teams
as values, and a GetTeam
function. Using a map will boost the team search if we know their names in advance. We will also dispose of a method to return the number of created objects, which will be called the GetNumberOfObjects
method:
type teamFlyweightFactory struct { createdTeams map[string]*Team } func (t *teamFlyweightFactory) GetTeam(name string) *Team { return nil } func (t *teamFlyweightFactory) GetNumberOfObjects() int { return 0 }
This is enough to write our first unit test:
func TestTeamFlyweightFactory_GetTeam(t *testing.T) { factory := teamFlyweightFactory{} teamA1 := factory.GetTeam(TEAM_A) if teamA1 == nil { t.Error("The pointer to the TEAM_A was nil") } teamA2 := factory.GetTeam(TEAM_A) if teamA2 == nil { t.Error("The pointer to the TEAM_A was nil") } if teamA1 != teamA2 { t.Error("TEAM_A pointers weren't the same") } if factory.GetNumberOfObjects() != 1 { t.Errorf("The number of objects created was not 1: %d ", factory.GetNumberOfObjects()) } }
In our test, we verify all the acceptance criteria. First we create a factory, and then ask for a pointer of TEAM_A
. This pointer cannot be nil
, or the test will fail.
Then we call for a second pointer to the same team. This pointer can't be nil either, and it should point to the same memory address as the previous one so we know that it has not allocated a new memory.
Finally, we should check whether the number of created teams is only one, because we have asked for the same team twice. We have two pointers but just one instance of the team. Let's run the tests:
$ go test -v -run=GetTeam . === RUN TestTeamFlyweightFactory_GetTeam --- FAIL: TestTeamFlyweightFactory_GetTeam (0.00s) flyweight_test.go:11: The pointer to the TEAM_A was nil flyweight_test.go:21: The pointer to the TEAM_A was nil flyweight_test.go:31: The number of objects created was not 1: 0 FAIL exit status 1 FAIL
Well, it failed. Both pointers were nil and it has not created any object. Interestingly, the function that compares the two pointers doesn't fail; all in all, nil equals nil.
Our GetTeam
method will need to scan the map
field called createdTeams
to make sure the queried team is already created, and return it if so. If the team wasn't created, it will have to create it and store it in the map before returning:
func (t *teamFlyweightFactory) GetTeam(teamID int) *Team { if t.createdTeams[teamID] != nil { return t.createdTeams[teamID] } team := getTeamFactory(teamID) t.createdTeams[teamID] = &team return t.createdTeams[teamID] }
The preceding code is very simple. If the parameter name exists in the createdTeams
map, return the pointer. Otherwise, call a factory for team creation. This is interesting enough to stop for a second and analyze. When you use the Flyweight pattern, it is very common to have a Flyweight factory, which uses other types of creational patterns to retrieve the objects it needs.
So, the getTeamFactory
method will give us the team we are looking for, we will store it in the map, and return it. The team factory will be able to create the two teams: TEAM_A
and TEAM_B
:
func getTeamFactory(team int) Team { switch team { case TEAM_B: return Team{ ID: 2, Name: TEAM_B, } default: return Team{ ID: 1, Name: TEAM_A, } } }
We are simplifying the objects' content so that we can focus on the Flyweight pattern's implementation. Okay, so we just have to define the function to retrieve the number of objects created, which is done as follows:
func (t *teamFlyweightFactory) GetNumberOfObjects() int { return len(t.createdTeams) }
This was pretty easy. The len
function returns the number of elements in an array or slice, the number of characters in a string
, and so on. It seems that everything is done, and we can launch our tests again:
$ go test -v -run=GetTeam .
=== RUN TestTeamFlyweightFactory_GetTeam
--- FAIL: TestTeamFlyweightFactory_GetTeam (0.00s)
panic: assignment to entry in nil map [recovered]
panic: assignment to entry in nil map
goroutine 5 [running]:
panic(0x530900, 0xc0820025c0)
/home/mcastro/Go/src/runtime/panic.go:481 +0x3f4
testing.tRunner.func1(0xc082068120)
/home/mcastro/Go/src/testing/testing.go:467 +0x199
panic(0x530900, 0xc0820025c0)
/home/mcastro/Go/src/runtime/panic.go:443 +0x4f7
/home/mcastro/go-design-patterns/structural/flyweight.(*teamFlyweightFactory).GetTeam(0xc08202fec0, 0x0, 0x0)
/home/mcastro/Desktop/go-design-patterns/structural/flyweight/flyweight.go:71 +0x159
/home/mcastro/go-design-patterns/structural/flyweight.TestTeamFlyweightFactory_GetTeam(0xc082068120)
/home/mcastro/Desktop/go-design-patterns/structural/flyweight/flyweight_test.go:9 +0x61
testing.tRunner(0xc082068120, 0x666580)
/home/mcastro/Go/src/testing/testing.go:473 +0x9f
created by testing.RunTests
/home/mcastro/Go/src/testing/testing.go:582 +0x899
exit status 2
FAIL
Panic! Have we forgotten something? By reading the stack trace on the panic message, we can see some addresses, some files, and it seems that the GetTeam
method is trying to assign an entry to a nil map on line 71 of the flyweight.go
file. Let's look at line 71 closely (remember, if you are writing code while following this tutorial, that the error will probably be in a different line so look closely at your own stark trace):
t.createdTeams[teamName] = &team
Okay, this line is on the GetTeam
method, and, when the method passes through here, it means that it had not found the team on the map-it has created it (the variable team), and is trying to assign it to the map. But the map is nil, because we haven't initialized it when creating the factory. This has a quick solution. In our test, initialize the map where we have created the factory:
factory := teamFlyweightFactory{ createdTeams: make(map[int]*Team,0), }
I'm sure you have seen the problem here already. If we don't have access to the package, we can initialize the variable. Well, we can make the variable public, and that's all. But this would involve every implementer necessarily knowing that they have to initialize the map, and its signature is neither convenient, or elegant. Instead, we are going to create a simple factory builder to do it for us. This is a very common approach in Go:
func NewTeamFactory() teamFlyweightFactory { return teamFlyweightFactory{ createdTeams: make(map[int]*Team), } }
So now, in the test, we replace the factory creation with a call to this function:
func TestTeamFlyweightFactory_GetTeam(t *testing.T) { factory := NewTeamFactory() ... }
And we run the test again:
$ go test -v -run=GetTeam . === RUN TestTeamFlyweightFactory_GetTeam --- PASS: TestTeamFlyweightFactory_GetTeam (0.00s) PASS ok
Perfect! Let's improve the test by adding a second test, just to ensure that everything will be running as expected with more volume. We are going to create a million calls to the team creation, representing a million calls from users. Then, we will simply check that the number of teams created is only two:
func Test_HighVolume(t *testing.T) { factory := NewTeamFactory() teams := make([]*Team, 500000*2) for i := 0; i < 500000; i++ { teams[i] = factory.GetTeam(TEAM_A) } for i := 500000; i < 2*500000; i++ { teams[i] = factory.GetTeam(TEAM_B) } if factory.GetNumberOfObjects() != 2 { t.Errorf("The number of objects created was not 2: %d ",factory.GetNumberOfObjects()) } }
In this test, we retrieve TEAM_A
and TEAM_B
500,000 times each to reach a million users. Then, we make sure that just two objects were created:
$ go test -v -run=Volume .
=== RUN Test_HighVolume
--- PASS: Test_HighVolume (0.04s)
PASS
ok
Perfect! We can even check where the pointers are pointing to, and where they are located. We will check with the first three as an example. Add these lines at the end of the last test, and run it again:
for i:=0; i<3; i++ { fmt.Printf("Pointer %d points to %p and is located in %p ", i, teams[i], &teams[i]) }
In the preceding test, we use the Printf
method to print information about pointers. The %p
flag gives you the memory location of the object that the pointer is pointing to. If you reference the pointer by passing the &
symbol, it will give you the direction of the pointer itself.
Run the test again with the same command; you will see three new lines in the output with information similar to the following:
Pointer 0 points to 0xc082846000 and is located in 0xc082076000 Pointer 1 points to 0xc082846000 and is located in 0xc082076008 Pointer 2 points to 0xc082846000 and is located in 0xc082076010
What it tells us is that the first three positions in the map point to the same location, but that we actually have three different pointers, which are, effectively, much lighter than our team object.
Well, the difference is subtle but it's just there. With the Singleton pattern, we ensure that the same type is created only once. Also, the Singleton pattern is a Creational pattern. With Flyweight, which is a Structural pattern, we aren't worried about how the objects are created, but about how to structure a type to contain heavy information in a light way. The structure we are talking about is the map[int]*Team
structure in our example. Here, we really didn't care about how we created the object; we have simply written an uncomplicated the getTeamFactory
method for it. We gave major importance to having a light structure to hold a shareable object (or objects), in this case, the map.
3.145.206.43