Chapter 4. Structural Patterns - Proxy, Facade, Decorator, and Flyweight Design Patterns

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.

Proxy design pattern

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.

Description

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).

Objectives

The possibilities of the Proxy pattern are many, but in general, they all try to provide the same following functionalities:

  • Hide an object behind the proxy so the features can be hidden, restricted, and so on
  • Provide a new abstraction layer that is easy to work with, and can be changed easily

Example

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).

Acceptance criteria

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:

  1. All accesse to the database of users will be done through the Proxy type.
  2. A stack of n number of recent users will be kept in the Proxy.
  3. If a user already exists in the stack, it won't query the database, and will return the stored one
  4. If the queried user doesn't exist in the stack, it will query the database, remove the oldest user in the stack if it's full, store the new one, and return it.

Unit test

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.

Implementation

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.

Proxying around actions

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.

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

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