+1

Golang Design Patterns - Proxy

Design Patterns Whiteboard Image.png

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.

  1. Định nghĩa User:
    User chỉ có thuộc tính ID (int), đại diện cho đối tượng người dùng.

  2. Interface chung:
    Giao diện UserFinder định nghĩa phương thức Find(ID int) để tìm User theo ID. Cả MainDBStack đều tuân theo giao diện này.

  3. Cơ sở dữ liệu chính (MainDB):
    Là nơi lưu trữ User chính, UsersDB cung cấp chức năng Find để tìm User và Add để thêm User mới.

  4. Proxy (UserFinderProxy):

    • Là lớp trung gian dùng Stack (bộ nhớ đệm) để truy cập nhanh hơn trước khi tìm trong MainDB.
    • Khi User không có trong Stack, proxy sẽ kiểm tra MainDB. Nếu tìm thấy, proxy sẽ lưu User vào Stack để 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.

Chúng ta sẽ có class diagram như sau

III. Implementation

  1. Định nghĩa type cho User entity
package proxy

type User struct {
	ID int
}
  1. Định nghĩa Interface, ở đây cả Main database và Proxy database dùng chung
package proxy

type UserFinder interface {
	Find(ID int) (User, error)
}

  1. 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
}
  1. Đị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ả: image.png

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ào MainDB. 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)
  1. Proxy tìm ID = 1 trong Stack, nhưng Stack đang rỗng, nên trả về lỗi user not found.
  2. Proxy chuyển sang kiểm tra trong MainDB và tìm thấy User với ID = 1.
  3. 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)
  1. Proxy kiểm trong Stack, nhưng không tìm thấy ID = 2.
  2. Proxy kiểm tra MainDB, tìm thấy User với ID = 2.
  3. 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)
  1. Proxy kiểm Stack, không tìm thấy ID = 3.
  2. Proxy kiểm tra MainDB, tìm thấy User với ID = 3.
  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êm User{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)
  1. Proxy kiểm tra Stacktìm thấy User với ID = 2 ngay trong Stack, không cần truy vấn MainDB.

Kết quả (log):

Found in stack:  {2}
Lần 5: proxy.Find(1)
  1. Proxy kiểm tra Stack, và không tìm thấy ID = 1.
  2. Proxy kiểm tra MainDB, tìm thấy User với ID = 1.
  3. 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 trong MainDB và thêm kết quả vào Stack để 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

Viblo
Let's register a Viblo Account to get more interesting posts.