I. Proxy - Structural Pattern
The Proxy is a familiar Design Pattern for developers, used to represent an original resource. The Proxy provides functionalities similar to the original resource but with high customizability.
The Proxy acts as a substitute for the original resource, hiding complex details or adding necessary features such as access control, caching, or load balancing.
II. Real-world Example
The system frequently needs to query user information from a main database. However, directly querying the main database can increase latency and reduce system performance. Therefore, we use a Proxy that acts as a cache to temporarily store frequently queried data.
The Proxy checks the cache, and if the data exists in the cache, it returns it immediately. Otherwise, it queries the main database and updates the cache.
This use case introduces the mechanism of a Proxy for searching users (User) between the MainDB (main database) and the Stack (cache) to improve performance.
Definition of User:
A User only has an ID attribute (int), representing a user entity.Shared Interface:
The UserFinder interface defines the Find(ID int) method to search for a user by their ID. Both MainDB and Stack adhere to this interface.Main Database (MainDB):
The main database where users are stored. UsersDB provides the Find function to search for a user and the Add function to add a new user.Proxy (UserFinderProxy):
The Proxy is a middle layer that uses the Stack (cache memory) for faster access before searching in the MainDB.
- When a User is not in the Stack, the Proxy checks the MainDB.
- If the user is found, the Proxy saves the User in the Stack to optimize subsequent queries.
- The Stack has a capacity-limiting mechanism and auto-updates when full.
The class diagram for the above scenario would be depicted as follows:
III. Implementation
- Definition of the Interface User entity:
package proxy
type User struct {
ID int
}
- Definition of the Interface:
package proxy
type UserFinder interface {
Find(ID int) (User, error)
}
- Definition of Database
package proxy
import "fmt"
type UsersDB []User
func (u *UsersDB) Find(ID int) (User, error) {
for _, user := range *u {
if user.ID == ID {
return user, nil
}
}
return User{}, fmt.Errorf("user not found")
}
func (u *UsersDB) Add(user User) *UsersDB {
fmt.Println("Adding to database: ", user)
*u = append(*u, user)
return u
}
- Definition of Proxy:
package proxy
import "fmt"
type UsersStack []User
func (u *UsersStack) Find(ID int) (User, error) {
for _, user := range *u {
if user.ID == ID {
return user, nil
}
}
return User{}, fmt.Errorf("user not found")
}
func (u *UsersStack) Add(user User) *UsersStack {
*u = append(*u, user)
return u
}
type UserFinderProxy struct {
MainDB UsersDB
Stack UsersStack
Capacity int
}
func (u *UserFinderProxy) Find(ID int) (User, error) {
user, err := u.Stack.Find(ID)
if err == nil {
fmt.Println("Found in stack: ", user)
return user, nil
}
user, err = u.MainDB.Find(ID)
if err != nil {
return User{}, err
}
fmt.Println("Found in mainDB: ", user)
u.AddToStack(user)
return user, nil
}
func (u *UserFinderProxy) AddToStack(user User) error {
fmt.Println("Adding to stack: ", user)
if len(u.Stack) >= u.Capacity {
u.Stack = append(u.Stack[1:], user)
} else {
u.Stack.Add(user)
}
return nil
}
- Run the application:
/*
Example Proxy
*/
fmt.Println("*** Example Proxy ***")
mainDB := proxy.UsersDB{}
user1 := proxy.User{ID: 1}
user2 := proxy.User{ID: 2}
user3 := proxy.User{ID: 3}
mainDB.Add(user1).Add(user2).Add(user3)
proxy := proxy.UserFinderProxy{
MainDB: mainDB,
Stack: proxy.UsersStack{},
Capacity: 2,
}
proxy.Find(1)
proxy.Find(2)
proxy.Find(3)
proxy.Find(2)
proxy.Find(1)
fmt.Print("*** End of Proxy ***\n\n\n")
With the output:
IV. Explain the example above
This example illustrates how the Proxy pattern works through querying users (User) from the main database (MainDB) while using a stack-based cache (Stack) to optimize repeated search operations. Below is the step-by-step explanation:
1. Initializing the Main Database (MainDB)
mainDB := proxy.UsersDB{}
user1 := proxy.User{ID: 1}
user2 := proxy.User{ID: 2}
user3 := proxy.User{ID: 3}
mainDB.Add(user1).Add(user2).Add(user3)
- A main database (
MainDB
) is initialized, which holds a list of users (UsersDB
). - Three users with IDs
1
,2
, and3
are added to theMainDB
. Any query will be able to find these users if searched directly inMainDB
.
Log results:
Adding to database: {1}
Adding to database: {2}
Adding to database: {3}
2. Initializing the Proxy with MainDB and Stack
proxy := proxy.UserFinderProxy{
MainDB: mainDB,
Stack: proxy.UsersStack{},
Capacity: 2,
}
The UserFinderProxy
is created with the following components:
- MainDB: The primary database that stores all the users.
- Stack: A cache layer that is initially empty.
-
Capacity: The maximum number of users that the
Stack
can hold (set to2
here).
3. Searching for Users
First Search: proxy.Find(1)
- Proxy checks for ID
1
in the Stack but finds nothing (Stack is initially empty). - Proxy switches to searching in the
MainDB
and finds the user with ID1
. - The proxy adds this user to the Stack for faster future queries.
Log results:
Found in mainDB: {1}
Adding to stack: {1}
Second Search: proxy.Find(2)
- Proxy checks for ID
2
in the Stack but does not find it. - Proxy searches the
MainDB
, finds the user with ID2
, and adds it to the Stack. - The Stack now contains two users:
{1, 2}
.
Log results:
Found in mainDB: {2}
Adding to stack: {2}
Third Search: proxy.Find(3)
- Proxy checks for ID
3
in the Stack but does not find it. - Proxy searches the
MainDB
and finds the user with ID3
. - The
Stack
, already at full capacity (2
), operates on a FIFO (First In, First Out) mechanism:- It removes the oldest user (User with ID
1
). - Then, it adds the new user (
3
).
- It removes the oldest user (User with ID
- The Stack now contains
{2, 3}
.
Log results:
Found in mainDB: {3}
Adding to stack: {3}
Fourth Search: proxy.Find(2)
- Proxy checks for ID
2
in the Stack and finds it directly. - No need to query the
MainDB
—the search is instant.
Log results:
Found in stack: {2}
Fifth Search: proxy.Find(1)
- Proxy checks for ID
1
in the Stack but does not find it (it was removed earlier). - Proxy searches the
MainDB
and finds the user with ID1
. - The
Stack
, at capacity, removes the oldest user (User with ID3
) and adds the new user (1
). - The Stack now contains
{2, 1}
.
Log results:
Found in mainDB: {1}
Adding to stack: {1}
4. End of Example
Final State:
-
Stack: Contains
{2, 1}
(the two most recently queried users). -
MainDB: Remains unchanged, containing all users
{1, 2, 3}
.
Final Log:
*** End of Proxy ***
Summary
- For each query, the Proxy first checks the Stack to quickly locate the User.
- If the User is not found in the Stack, the Proxy searches the MainDB and adds the result to the Stack for optimized future searches.
- The Stack has a capacity limit, and when it is exceeded, the oldest User is removed (FIFO – First In, First Out).
Advantages of this approach:
- Reduces the number of accesses to the MainDB when Users are queried repeatedly.
- Improves system performance by utilizing the cache (Stack) for faster lookups.
V. Conclusion
We do not always need to use the Proxy design pattern to address similar problems. In some cases, direct access to the database might suffice if the problem is not complex or performance optimization is not required. In other situations, you might consider using the Decorator pattern or other optimization techniques.
The most important thing is to choose the solution that best fits your specific problem.
Thank you for taking the time to read this article! 😊
VI. References
- Go Design Patterns (Mario Castro Contreras)
- Full source code for Go design patterns: available here.
Top comments (0)