Golang Design Patterns - Proxy
I. Proxy - Structural Pattern
Proxy là một Design Pattern quen thuộc với lập trình viên, được sử dụng để đại diện cho một tài nguyên (resource) gốc. Proxy cung cấp các tính năng tương tự tài nguyên gốc, nhưng được tùy biến cao.
Proxy đóng vai trò thay thế tài nguyên gốc, ẩn đi các chi tiết phức tạp, hoặc bổ sung những tính năng cần thiết như kiểm soát truy cập, tạo bộ nhớ đệm (caching), hoặc giảm tải xử lý.
II. Ví dụ thực tế
Hệ thống cần thực hiện thường xuyên các truy vấn thông tin người dùng từ một cơ sở dữ liệu chính. Tuy nhiên, việc truy vấn trực tiếp đến database chính có thể làm tăng độ trễ và làm giảm hiệu suất hệ thống. Vì vậy, chúng ta sử dụng một Proxy đóng vai trò như bộ đệm (cache) để lưu trữ tạm thời các dữ liệu thường xuyên được truy vấn.
Proxy sẽ kiểm tra cache, nếu dữ liệu đã tồn tại trong cache thì trả về ngay lập tức, ngược lại sẽ truy vấn từ database chính và cập nhật cache.
Use case này giới thiệu cơ chế Proxy cho việc tìm kiếm người dùng (User
) giữa MainDB
(cơ sở dữ liệu chính) và Stack
(bộ nhớ đệm) để cải thiện hiệu năng.
-
Định nghĩa User:
User chỉ có thuộc tínhID
(int
), đại diện cho đối tượng người dùng. -
Interface chung:
Giao diệnUserFinder
định nghĩa phương thứcFind(ID int)
để tìm User theo ID. CảMainDB
vàStack
đều tuân theo giao diện này. -
Cơ sở dữ liệu chính (
MainDB
):
Là nơi lưu trữ User chính,UsersDB
cung cấp chức năngFind
để tìm User vàAdd
để thêm User mới. -
Proxy (
UserFinderProxy
):- Là lớp trung gian dùng
Stack
(bộ nhớ đệm) để truy cập nhanh hơn trước khi tìm trongMainDB
. - Khi User không có trong
Stack
, proxy sẽ kiểm traMainDB
. Nếu tìm thấy, proxy sẽ lưu User vàoStack
để tối ưu cho các lần truy xuất kế tiếp. Stack
có cơ chế giới hạn dung lượng, tự cập nhật khi đầy.
- Là lớp trung gian dùng
Chúng ta sẽ có class diagram như sau
III. Implementation
- Định nghĩa type cho User entity
package proxy
type User struct {
ID int
}
- Định nghĩa Interface, ở đây cả Main database và Proxy database dùng chung
package proxy
type UserFinder interface {
Find(ID int) (User, error)
}
- 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
}
- Định nghĩa 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
}
Chạy chương trình trên:
/*
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")
Với kết quả:
IV. Giải thích ví dụ Proxy
Ví dụ này minh họa cách hoạt động của mô hình Proxy thông qua việc truy vấn người dùng (User) từ cơ sở dữ liệu chính (MainDB
) và sử dụng bộ nhớ đệm (Stack
) để tối ưu hóa các lần tìm kiếm lặp lại. Dưới đây là giải thích từng bước:
1. Khởi tạo cơ sở dữ liệu chính 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)
- Một cơ sở dữ liệu chính được khởi tạo (
MainDB
), chứa danh sách các người dùng (UsersDB
). - 3 người dùng với
ID = 1, 2, 3
được thêm vàoMainDB
. Mọi truy vấn đều có thể tìm thấy các User này nếu được tìm trực tiếp từMainDB
.
Kết quả (log):
Adding to database: {1}
Adding to database: {2}
Adding to database: {3}
2. Khởi tạo Proxy với MainDB và Stack
proxy := proxy.UserFinderProxy{
MainDB: mainDB,
Stack: proxy.UsersStack{},
Capacity: 2,
}
- Tạo một Proxy
UserFinderProxy
với:MainDB
: là cơ sở dữ liệu chính lưu User.Stack
: làm bộ nhớ đệm (ban đầu rỗng).Capacity
: số lượng tối đa User có thể lưu trong Stack (ở đây là 2).
3. Tìm kiếm User
Lần 1: proxy.Find(1)
- Proxy tìm
ID = 1
trongStack
, nhưng Stack đang rỗng, nên trả về lỗiuser not found
. - Proxy chuyển sang kiểm tra trong
MainDB
và tìm thấy User vớiID = 1
. - Sau khi tìm thấy, Proxy thêm User này vào Stack để tối ưu cho lần tìm sau.
Kết quả (log):
Found in mainDB: {1}
Adding to stack: {1}
Lần 2: proxy.Find(2)
- Proxy kiểm trong
Stack
, nhưng không tìm thấyID = 2
. - Proxy kiểm tra
MainDB
, tìm thấy User vớiID = 2
. - Proxy thêm User với
ID = 2
vào Stack. GiờStack
chứa 2 User:{1, 2}
.
Kết quả (log):
Found in mainDB: {2}
Adding to stack: {2}
Lần 3: proxy.Find(3)
- Proxy kiểm
Stack
, không tìm thấyID = 3
. - Proxy kiểm tra
MainDB
, tìm thấy User vớiID = 3
. - Proxy thêm User với
ID = 3
vào Stack. Nhưng vìStack
đã đầy (sức chứa là 2), Proxy loại bỏ User lâu nhất (FIFO), tức làUser{ID: 1}
, và thêmUser{ID: 3}
vào Stack. GiờStack
chứa 2 User:{2, 3}
.
Kết quả (log):
Found in mainDB: {3}
Adding to stack: {3}
Lần 4: proxy.Find(2)
- Proxy kiểm tra
Stack
và tìm thấy User vớiID = 2
ngay trong Stack, không cần truy vấnMainDB
.
Kết quả (log):
Found in stack: {2}
Lần 5: proxy.Find(1)
- Proxy kiểm tra
Stack
, và không tìm thấyID = 1
. - Proxy kiểm tra
MainDB
, tìm thấy User vớiID = 1
. - Proxy thêm lại User này vào Stack. Vì Stack đã đầy, User lâu nhất là
User{ID: 3}
bị loại bỏ. Giờ Stack chứa{2, 1}
.
Kết quả (log):
Found in mainDB: {1}
Adding to stack: {1}
4. Kết thúc ví dụ
Kết quả cuối cùng:
Stack
: chứa{2, 1}
(2 User mới nhất được truy vấn).MainDB
: chứa đầy đủ dữ liệu{1, 2, 3}
.
Log cuối:
*** End of Proxy ***
Tóm tắt
- Mỗi lần truy vấn, Proxy sẽ kiểm tra
Stack
trước để tìm User nhanh hơn. - Nếu không thấy trong
Stack
, Proxy tìm trongMainDB
và thêm kết quả vàoStack
để tối ưu hoá các tìm kiếm trong tương lai. Stack
có giới hạn dung lượng, khi vượt quá, nó sẽ loại bỏ User cũ nhất (FIFO – First In, First Out).
Cách này giúp:
- Giảm số lần truy cập
MainDB
khi các User được truy vấn lặp lại. - Cải thiện hiệu suất cho hệ thống.
V. Lời kết
Không phải lúc nào chúng ta cũng cần sử dụng mẫu thiết kế Proxy để giải quyết các vấn đề tương tự như trên. Đôi khi, việc truy cập trực tiếp vào cơ sở dữ liệu là đủ nếu bài toán không phức tạp hoặc không cần tối ưu hiệu suất. Một số trường hợp khác, bạn có thể cân nhắc mẫu Decorator hoặc các kỹ thuật tối ưu khác. Quan trọng nhất, hãy lựa chọn giải pháp phù hợp nhất với bài toán của mình.
Cảm ơn các bạn đã dành thời gian theo dõi bài viết! 😊
VI. References
-
Go Design Patterns (Mario Castro Contreras)
-
Full source code về golang design patterns: tại đây
All Rights Reserved