With this chapter, we will finish with the Structural patterns. We have left some of the most complex ones till the end so that you get more used to the mechanics of design patterns, and the features of Go language.
In this chapter, we will work at writing a cache to access a database, a library to gather weather data, a server with runtime middleware, and discuss a way to save memory by saving shareable states between the types values.
We'll start the final chapter on structural patterns with the Proxy pattern. It's a simple pattern that provides interesting features and possibilities with very little effort.
The Proxy pattern usually wraps an object to hide some of its characteristics. These characteristics could be the fact that it is a remote object (remote proxy), a very heavy object such as a very big image or the dump of a terabyte database (virtual proxy), or a restricted access object (protection proxy).
The possibilities of the Proxy pattern are many, but in general, they all try to provide the same following functionalities:
For our example, we are going to create a remote proxy, which is going to be a cache of objects before accessing a database. Let's imagine that we have a database with many users, but instead of accessing the database each time we want information about a user, we will have a First In First Out (FIFO) stack of users in a Proxy pattern (FIFO is a way of saying that when the cache needs to be emptied, it will delete the first object that entered first).
We will wrap an imaginary database, represented by a slice, with our Proxy pattern. Then, the pattern will have to stick to the following acceptance criteria:
n
number of recent users will be kept in the Proxy.Since version 1.7 of Go, we can embed tests within tests by using closures so we can group them in a more human-readable way, and reduce the number of Test_
functions. Refer to
Chapter 1
, Ready... Steady... Go! to learn how to install the new version of Go if your current version is older than version 1.7.
The types for this pattern will be the proxy user and user list structs as well as a UserFinder
interface that the database and the Proxy will implement. This is key because the Proxy must implement the same interfaces as the features of the type it tries to wrap:
type UserFinder interface { FindUser(id int32) (User, error) }
The UserFinder
is the interface that the database and the Proxy implement. The User
is a type with a member called ID
, which is int32
type:
type User struct { ID int32 }
Finally, the UserList
is a type of a slice of users. Consider the following syntax for that:
type UserList []User
If you are asking why we aren't using a slice of users directly, the answer is that by declaring a sequence of users this way, we can implement the UserFinder
interface but with a slice, we can't.
Finally, the Proxy type, called UserListProxy
will be composed of a UserList
slice, which will be our database representation. The StackCache
members which will also be of UserList
type for simplicity, StackCapacity
to give our stack the size we want.
We will cheat a bit for the purpose of this tutorial and declare a Boolean state on a field called DidDidLastSearchUsedCache
that will hold if the last performed search has used the cache, or has accessed the database:
type UserListProxy struct { SomeDatabase UserList StackCache UserList StackCapacity int DidDidLastSearchUsedCache bool } func (u *UserListProxy) FindUser(id int32) (User, error) { return User{}, errors.New("Not implemented yet") }
The UserListProxy
type will cache a maximum of StackCapacity
users, and rotate the cache if it reaches this limit. The StackCache
members will be populated from objects from SomeDatabase
type.
The first test is called TestUserListProxy
, and is listed next:
import ( "math/rand" "testing" ) func Test_UserListProxy(t *testing.T) { someDatabase := UserList{} rand.Seed(2342342) for i := 0; i < 1000000; i++ { n := rand.Int31() someDatabase = append(someDatabase, User{ID: n}) }
The preceding test creates a user list of 1 million users with random names. To do so, we feed the random number generator by calling the Seed()
function with some constant seed so our randomized results are also constant; and the user IDs are generated from it. It might have some duplicates, but it serves our purpose.
Next, we need a proxy with a reference to someDatabase
, which we have just created:
proxy := UserListProxy{ SomeDatabase: &someDatabase, StackCapacity: 2, StackCache: UserList{}, }
At this point, we have a proxy
object composed of a mock database with 1 million users, and a cache implemented as a FIFO stack with a size of 2. Now we will get three random IDs from someDatabase
to use in our stack:
knownIDs := [3]int32 {someDatabase[3].ID, someDatabase[4].ID,someDatabase[5].ID}
We took the fourth, fifth, and sixth IDs from the slice (remember that arrays and slices start with 0, so the index 3 is actually the fourth position in the slice).
This is going to be our starting point before launching the embedded tests. To create an embedded test, we have to call the Run
method of the testing.T
pointer, with a description and a closure with the func(t *testing.T)
signature:
t.Run("FindUser - Empty cache", func(t *testing.T) { user, err := proxy.FindUser(knownIDs[0]) if err != nil { t.Fatal(err) }
For example, in the preceding code snippet, we give the description FindUser - Empty cache
. Then we define our closure. First it tries to find a user with a known ID, and checks for errors. As the description implies, the cache is empty at this point, and the user will have to be retrieved from the someDatabase
array:
if user.ID != knownIDs[0] { t.Error("Returned user name doesn't match with expected") } if len(proxy.StackCache) != 1 { t.Error("After one successful search in an empty cache, the size of it must be one") } if proxy.DidLastSearchUsedCache { t.Error("No user can be returned from an empty cache") } }
Finally, we check whether the returned user has the same ID as that of the expected user at index 0 of the knownIDs
slice, and that the proxy cache now has a size of 1. The state of the member DidLastSearchUsedCache
proxy must not be true
, or we will not pass the test. Remember, this member tells us whether the last search has been retrieved from the slice that represents a database, or from the cache.
The second embedded test for the Proxy pattern is to ask for the same user as before, which must now be returned from the cache. It's very similar to the previous test, but now we have to check if the user is returned from the cache:
t.Run("FindUser - One user, ask for the same user", func(t *testing.T) { user, err := proxy.FindUser(knownIDs[0]) if err != nil { t.Fatal(err) } if user.ID != knownIDs[0] { t.Error("Returned user name doesn't match with expected") } if len(proxy.StackCache) != 1 { t.Error("Cache must not grow if we asked for an object that is stored on it") } if !proxy.DidLastSearchUsedCache { t.Error("The user should have been returned from the cache") } })
So, again we ask for the first known ID. The proxy cache must maintain a size of 1 after this search, and the DidLastSearchUsedCache
member must be true this time, or the test will fail.
The last test will overflow the StackCache
array on the proxy
type. We will search for two new users that our proxy
type will have to retrieve from the database. Our stack has a size of 2, so it will have to remove the first user to allocate space for the second and third users:
user1, err := proxy.FindUser(knownIDs[0]) if err != nil { t.Fatal(err) } user2, _ := proxy.FindUser(knownIDs[1]) if proxy.DidLastSearchUsedCache { t.Error("The user wasn't stored on the proxy cache yet") } user3, _ := proxy.FindUser(knownIDs[2]) if proxy.DidLastSearchUsedCache { t.Error("The user wasn't stored on the proxy cache yet") }
We have retrieved the first three users. We aren't checking for errors because that was the purpose of the previous tests. This is important to recall that there is no need to over-test your code. If there is any error here, it will arise in the previous tests. Also, we have checked that the user2
and user3
queries do not use the cache; they shouldn't be stored there yet.
Now we are going to look for the user1
query in the Proxy. It shouldn't exist, as the stack has a size of 2, and user1
was the first to enter, hence, the first to go out:
for i := 0; i < len(proxy.StackCache); i++ { if proxy.StackCache[i].ID == user1.ID { t.Error("User that should be gone was found") } } if len(proxy.StackCache) != 2 { t.Error("After inserting 3 users the cache should not grow" + " more than to two") }
It doesn't matter if we ask for a thousand users; our cache can't be bigger than our configured size.
Finally, we are going to again range over the users stored in the cache, and compare them with the last two we queried. This way, we will check that just those users are stored in the cache. Both must be found on it:
for _, v := range proxy.StackCache { if v != user2 && v != user3 { t.Error("A non expected user was found on the cache") } } }
Running the tests now should give some errors, as usual. Let's run them now:
$ go test -v . === RUN Test_UserListProxy === RUN Test_UserListProxy/FindUser_-_Empty_cache === RUN Test_UserListProxy/FindUser_-_One_user,_ask_for_the_same_user === RUN Test_UserListProxy/FindUser_-_overflowing_the_stack --- FAIL: Test_UserListProxy (0.06s) --- FAIL: Test_UserListProxy/FindUser_-_Empty_cache (0.00s) proxy_test.go:28: Not implemented yet --- FAIL: Test_UserListProxy/FindUser_-_One_user,_ask_for_the_same_user (0.00s) proxy_test.go:47: Not implemented yet --- FAIL: Test_UserListProxy/FindUser_-_overflowing_the_stack (0.00s) proxy_test.go:66: Not implemented yet FAIL exit status 1 FAIL
So, let's implement the FindUser
method to act as our Proxy.
In our Proxy, the FindUser
method will search for a specified ID in the cache list. If it finds it, it will return the ID. If not, it will search in the database. Finally, if it's not in the database list, it will return an error.
If you remember, our Proxy pattern is composed of two UserList
types (one of them a pointer), which are actually slices of User
type. We will implement a FindUser
method in User
type too, which, by the way, has the same signature as the UserFinder
interface:
type UserList []User func (t *UserList) FindUser(id int32) (User, error) { for i := 0; i < len(*t); i++ { if (*t)[i].ID == id { return (*t)[i], nil } } return User{}, fmt.Errorf("User %s could not be found ", id) }
The FindUser
method in the UserList
slice will iterate over the list to try and find a user with the same ID as the id
argument, or return an error if it can't find it.
You may be wondering why the pointer t
is between parentheses. This is to dereference the underlying array before accessing its indexes. Without it, you'll have a compilation error, because the compiler tries to search the index before dereferencing the pointer.
So, the first part of the proxy FindUser
method can be written as follows:
func (u *UserListProxy) FindUser(id int32) (User, error) { user, err := u.StackCache.FindUser(id) if err == nil { fmt.Println("Returning user from cache") u.DidLastSearchUsedCache = true return user, nil }
We use the preceding method to search for a user in the StackCache
member. The error will be nil if it can find it, so we check this to print a message to the console, change the state of DidLastSearchUsedCache
to true
so that the test can check whether the user was retrieved from cache, and finally, return the user.
So, if the error was not nil, it means that it couldn't find the user in the stack. So, the next step is to search in the database:
user, err = u.SomeDatabase.FindUser(id) if err != nil { return User{}, err }
We can reuse the FindUser
method we wrote for UserList
database in this case, because both have the same type for the purpose of this example. Again, it searches the user in the database represented by the UserList
slice, but in this case, if the user isn't found, it returns the error generated in UserList
.
When the user is found (err
is nil), we have to add the user to the stack. For this purpose, we write a dedicated private method that receives a pointer of type UserListProxy
:
func (u *UserListProxy) addUserToStack(user User) { if len(u.StackCache) >= u.StackCapacity { u.StackCache = append(u.StackCache[1:], user) } else { u.StackCache.addUser(user) } } func (t *UserList) addUser(newUser User) { *t = append(*t, newUser) }
The addUserToStack
method takes the user argument, and adds it to the stack in place. If the stack is full, it removes the first element in it before adding. We have also written an addUser
method to UserList
to help us in this. So, now in FindUser
method, we just have to add one line:
u.addUserToStack(user)
This adds the new user to the stack, removing the last if necessary.
Finally, we just have to return the new user of the stack, and set the appropriate value on DidLastSearchUsedCache
variable. We also write a message to the console to help in the testing process:
fmt.Println("Returning user from database") u.DidLastSearchUsedCache = false return user, nil }
With this, we have enough to pass our tests:
$ go test -v . === RUN Test_UserListProxy === RUN Test_UserListProxy/FindUser_-_Empty_cache Returning user from database === RUN Test_UserListProxy/FindUser_-_One_user,_ask_for_the_same_user Returning user from cache === RUN Test_UserListProxy/FindUser_-_overflowing_the_stack Returning user from cache Returning user from database Returning user from database --- PASS: Test_UserListProxy (0.09s) --- PASS: Test_UserListProxy/FindUser_-_Empty_cache (0.00s) --- PASS: Test_UserListProxy/FindUser_-_One_user,_ask_for_the_same_user (0.00s) --- PASS: Test_UserListProxy/FindUser_-_overflowing_the_stack (0.00s) PASS ok
You can see in the preceding messages that our Proxy has worked flawlessly. It has returned the first search from the database. Then, when we search for the same user again, it uses the cache. Finally, we made a new test that calls three different users and we can observe, by looking at the console output, that just the first was returned from the cache and that the other two were fetched from the database.
Wrap proxies around types that need some intermediate action, like giving authorization to the user or providing access to a database, like in our example.
Our example is a good way to separate application needs from database needs. If our application accesses the database too much, a solution for this is not in your database. Remember that the Proxy uses the same interface as the type it wraps, and, for the user, there shouldn't be any difference between the two.
18.119.255.139