DEV Community

Cover image for FlyWeight Design Pattern in Go
Tomas Sirio
Tomas Sirio

Posted on

FlyWeight Design Pattern in Go

Hey there!

New week, new design pattern to learn. This week I'm writing about the FlyWeight Design pattern. This pattern is a little bit different from the ones I've written before because I feel like most of it is oriented toward memory conservation on your computing which we pretty much don't care about on our daily basis. However, if you are working with distributed systems, old computers, or enormous database sets, maybe this pattern is for you.

So, let's imagine that you want to work on an MMO game (massive multiplayer online). This would require you to handle a lot of users with their first name, Lastname and username. A lot of this information would be repeated through your dataset, so let's see what we can do.

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}
}
Enter fullscreen mode Exit fullscreen mode

And we need to initialize some users on our main function:

    u1 := NewUser("John Doe")
    u2 := NewUser("Jane Doe")
    u3 := NewUser("Jane Smith")
Enter fullscreen mode Exit fullscreen mode

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

Then, let's make our application memory oriented. To achieve this, we are going to create a new User struct and a string slice to store all the names:

var allNames []string

type User2 struct {
    names []uint8
}
Enter fullscreen mode Exit fullscreen mode

The interesting thing is that now the new User is not going to store a string for the name but a 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
}
Enter fullscreen mode Exit fullscreen mode

This method has an internal function declared which receives a string (our name) and returns us the position on the allNames slice where that name is. If it's not there, it will store it on 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 that will reconstruct it's name:

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

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)
Enter fullscreen mode Exit fullscreen mode

We are creating 3 Users and 3 Users2 and we are printing how much memory do each of the structs consume:

go run main.go
John Doe
Jane Doe
Jane Smith
Memory taken by users: 26
Memory taken by users2 22
Enter fullscreen mode Exit fullscreen mode

We are saving 4 bytes on this example. It ain'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 multiply exponentially saving a lot of memory.

As I stated before, this Design Pattern is a little bit different from the ones I wrote of before. It's memory oriented instead of Behaviour oriented so use it when in need instead of just pushing random patterns all over your applications.

That's it for this week. Hope you liked it.
Happy coding

Top comments (1)

Collapse
 
cheiras profile image
Dan Tcheche • Edited

Hi, very interesting pattern and great post!
My only concern here is performance. Let's assume you have millions of users with 50k+ different names, going through a whole 50k+ array every time a new user logs in seems like a huge n to iterate. As a possible solution I thought of using a hash, so searching for the name it's going to be O(n). I don't know if this pattern was implemented with that structure.
I'll read more about this pattern and how it deals with huge amount of data, I'm assuming at one point the info is going to be saved in a database, and the ORM is going to take care of linking the data.
Keep up with the great posts!