What is FlyWeight Design Pattern in Go?

Let’s imagine that you want to work on an MMO gamemassive multiplayer online. This would require you to handle the first name, last name, and username of a lot of users. A lot of this information will be repeated through your dataset; so, let’s see what we can do.

First, let’s create a simple struct for our Users with their full name as an attribute:

type User struct {
    FullName string
}

func NewUser(fullName string) *User {
    return &User{FullName: fullName}
}

Now, we need to initialize some users on our main function:

u1 := NewUser("John Doe")
    u2 := NewUser("Jane Doe")
    u3 := NewUser("Jane Smith")

This is where my point is beginning to make sense. See how the name Jane and the surname Doe are both repeated and how we store them both times in our database? If we had a limited resource database, this would be a huge problem when 700 Jan-Michael Vincent joins our application.

Now, let’s make our application memory oriented. To achieve this, we have to create a new User struct and a string slice to store all the names:

var allNames []string

type User2 struct {
    names []uint8
}

The interesting thing is that the new User is not going to store a string for the name; rather, they are going to store an int8 slice. The downside of this implementation is that the New User constructor will require more processing power.

func NewUser2(fullName string) *User2 {
    getOrAdd := func(s string) uint8 {
        for i := range allNames {
            if allNames[i] == s {
                return uint8(i)
            }
        }
        allNames = append(allNames, s)
        return uint8(len(allNames) - 1)
    }
    result := User2{}
    parts := strings.Split(fullName, " ")
    for _, p := range parts {
        result.names = append(result.names, getOrAdd(p))
    }
    return &result
}

This method has an internal function declared that receives a string (our name) and returns the position of the allNames slice, where that name is, to us. If it’s not there, it will store it in the last position of the array and give us the position back. Then, the constructor will use this internal function to store or retrieve each of the strings on our name (divided by a space).

Every time we need a New User’s Full Name, we will use a method to reconstruct its name:

func (u *User2) FullName() string {
    var parts []string
    for _, id := range u.names {
        parts = append(parts, allNames[id])
    }
    return strings.Join(parts, " ")
}

Now, let’s go to our main function to test what we’ve created:

    john := NewUser("John Doe")
    jane := NewUser("Jane Doe")
    alsoJane := NewUser("Jane Smith")
    fmt.Println(john.FullName)
    fmt.Println(jane.FullName)
    fmt.Println(alsoJane.FullName)
    fmt.Println("Memory taken by users:",
        len([]byte(john.FullName))+
            len([]byte(jane.FullName))+
            len([]byte(alsoJane.FullName)))

    john2 := NewUser2("John Doe")
    jane2 := NewUser2("Jane Doe")
    alsoJane2 := NewUser2("Jane Smith")
    totalMem := 0
    for _, a := range allNames {
        totalMem += len([]byte(a))
    }

    totalMem += len(john2.names)
    totalMem += len(jane2.names)
    totalMem += len(alsoJane2.names)
    fmt.Println("Memory taken by users2", totalMem)

Here, we are creating 3 Users and 3 Users2 and printing how much memory each of the structs consumes:

package main
import "fmt"
import "strings"
type User struct {
FullName string
}
func NewUser(fullName string) *User {
return &User{FullName: fullName}
}
var allNames []string
type User2 struct {
names []uint8
}
func NewUser2(fullName string) *User2 {
getOrAdd := func(s string) uint8 {
for i := range allNames {
if allNames[i] == s {
return uint8(i)
}
}
allNames = append(allNames, s)
return uint8(len(allNames) - 1)
}
result := User2{}
parts := strings.Split(fullName, " ")
for _, p := range parts {
result.names = append(result.names, getOrAdd(p))
}
return &result
}
func (u *User2) FullName() string {
var parts []string
for _, id := range u.names {
parts = append(parts, allNames[id])
}
return strings.Join(parts, " ")
}
func main(){
john := NewUser("John Doe")
jane := NewUser("Jane Doe")
alsoJane := NewUser("Jane Smith")
fmt.Println(john.FullName)
fmt.Println(jane.FullName)
fmt.Println(alsoJane.FullName)
fmt.Println("Memory taken by users:",
len([]byte(john.FullName))+
len([]byte(jane.FullName))+
len([]byte(alsoJane.FullName)))
john2 := NewUser2("John Doe")
jane2 := NewUser2("Jane Doe")
alsoJane2 := NewUser2("Jane Smith")
totalMem := 0
for _, a := range allNames {
totalMem += len([]byte(a))
}
totalMem += len(john2.names)
totalMem += len(jane2.names)
totalMem += len(alsoJane2.names)
fmt.Println("Memory taken by users2", totalMem)
}

In this example, we are saving 4 bytes. It isn’t much, but it’s honest work memory saved on 3 strings. When names begin to get redundant on your database, these 4 bytes will exponentially multiply, saving a lot of memory.

This Design Pattern is a bit different than the previous patterns. It’s memory-oriented instead of behavior-oriented, so use it when you need it instead of just pushing random patterns all over your applications.

Check out the four other design patterns in GO:

Free Resources

Attributions:
  1. undefined by undefined
Copyright ©2024 Educative, Inc. All rights reserved