Flyweight design pattern

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.

Description

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.

Objectives

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.

Example

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.

Acceptance criteria

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:

  1. We will create a Team struct with some basic information such as the team's name, players, historical results, and an image depicting their shield.
  2. We must ensure correct team creation (note the word creation here, candidate for a creational pattern), and not having duplicates.
  3. When creating the same team twice, we must have two pointers pointing to the same memory address.

Basic structs and tests

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.

Implementation

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.

What's the difference between Singleton and Flyweight then?

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.206.43