Menu
CoddyTech

Mẫu tùy chọn hàm

Thuộc phần Lập trình hướng đối tượng trong hành trình GO của Coddy. Bài học 79 trên 107.

Khi tạo struct có nhiều trường cấu hình tùy chọn, các hàm khởi tạo (constructor) có thể trở nên cồng kềnh. Mẫu tùy chọn hàm (Functional Options Pattern) giải quyết vấn đề này một cách tinh tế bằng cách dùng hàm (function) để cấu hình đối tượng (object).

Hãy xét một cấu trúc máy chủ có nhiều thiết lập tùy chọn. Thay vì dùng một hàm khởi tạo (constructor) với nhiều tham số, ta định nghĩa một kiểu tùy chọn và các hàm (function) trả về tùy chọn:

type Server struct {
    host    string
    port    int
    timeout int
}

type Option func(*Server)

func WithPort(port int) Option {
    return func(s *Server) {
        s.port = port
    }
}

func WithTimeout(timeout int) Option {
    return func(s *Server) {
        s.timeout = timeout
    }
}

Hàm khởi tạo nhận một danh sách tùy chọn có số lượng linh hoạt và áp dụng từng tùy chọn:

func NewServer(host string, opts ...Option) *Server {
    s := &Server{
        host:    host,
        port:    8080,    // mặc định
        timeout: 30,      // mặc định
    }
    for _, opt := range opts {
        opt(s)
    }
    return s
}

Giờ đây, việc tạo máy chủ trở nên dễ đọc và linh hoạt:

// Dùng các giá trị mặc định
s1 := NewServer("localhost")

// Tùy chỉnh các tùy chọn cụ thể
s2 := NewServer("localhost", WithPort(3000), WithTimeout(60))

Mẫu này nổi bật vì bên gọi chỉ cần chỉ định những gì họ cần, các giá trị mặc định được trình bày rõ ràng ở một nơi, và việc thêm tùy chọn mới không làm hỏng code hiện có. Mẫu này được dùng rộng rãi trong các thư viện Go như grpc và zap.

challenge icon

Thử thách

Dễ

Hãy xây dựng một kết nối cơ sở dữ liệu có thể cấu hình bằng Mẫu tùy chọn hàm! Mẫu này rất hữu ích khi bạn có nhiều thiết lập tùy chọn với giá trị mặc định hợp lý, đúng như những gì một kết nối cơ sở dữ liệu cần.

Bạn sẽ sắp xếp code trong hai tệp:

  • database.go: Định nghĩa kiểu kết nối cơ sở dữ liệu và các hàm tùy chọn.

    Tạo một cấu trúc DBConnection với các trường không được xuất sau:

    • host (chuỗi)
    • port (số nguyên)
    • username (chuỗi)
    • password (chuỗi)
    • maxConnections (số nguyên)
    • timeout (số nguyên), tính bằng giây

    Định nghĩa kiểu Option là một hàm thay đổi *DBConnection.

    Tạo các hàm tùy chọn sau, mỗi hàm trả về một Option:

    • WithPort(port int)
    • WithCredentials(username, password string)
    • WithMaxConnections(max int)
    • WithTimeout(seconds int)

    Tạo hàm khởi tạo NewDBConnection(host string, opts ...Option) trả về một *DBConnection. Đặt các giá trị mặc định sau: port là 5432, username là "admin", password là "secret", maxConnections là 10, timeout là 30. Áp dụng tất cả tùy chọn được cung cấp sau khi đặt giá trị mặc định.

    Thêm phương thức ConnectionString() để trả về thông tin kết nối theo định dạng sau:

    [username]:[password]@[host]:[port] (max:[maxConnections], timeout:[timeout]s)
  • main.go: Tạo kết nối cơ sở dữ liệu với nhiều cấu hình khác nhau.

    Đọc host, sau đó đọc số lượng tùy chọn cần áp dụng. Với mỗi tùy chọn, hãy đọc loại tùy chọn và giá trị tương ứng:

    • port theo sau là số cổng
    • credentials theo sau là username và password (hai dòng)
    • maxconn theo sau là số lượng kết nối tối đa
    • timeout theo sau là thời gian chờ tính bằng giây

    Tạo một DBConnection với các tùy chọn đã chỉ định và in chuỗi kết nối ra.

Đầu vào sẽ có định dạng sau:

  • Dòng 1: Tên host
  • Dòng 2: Số lượng tùy chọn
  • Các dòng tiếp theo: Loại tùy chọn và giá trị tương ứng

Ví dụ, với đầu vào:

localhost
0

Đầu ra sẽ là:

admin:secret@localhost:5432 (max:10, timeout:30s)

Với đầu vào:

db.example.com
2
port
3306
credentials
root
mypassword

Đầu ra sẽ là:

root:mypassword@db.example.com:3306 (max:10, timeout:30s)

Với đầu vào:

production.server
4
port
5433
credentials
dbuser
securepass123
maxconn
50
timeout
60

Đầu ra sẽ là:

dbuser:securepass123@production.server:5433 (max:50, timeout:60s)

Lưu ý rằng bên gọi chỉ cần chỉ định những tùy chọn họ cần. Các thiết lập không được chỉ định sẽ dùng giá trị mặc định hợp lý. Đó chính là điểm tinh tế của Mẫu tùy chọn hàm!

Tự làm thử

package main

import (
	"bufio"
	"fmt"
	"os"
	"strconv"
	"strings"
)

func main() {
	reader := bufio.NewReader(os.Stdin)

	// Đọc host
	host, _ := reader.ReadString('\n')
	host = strings.TrimSpace(host)

	// Đọc số lượng tùy chọn
	numOptionsStr, _ := reader.ReadString('\n')
	numOptions, _ := strconv.Atoi(strings.TrimSpace(numOptionsStr))

	// Thu thập các tùy chọn
	var opts []Option

	for i := 0; i < numOptions; i++ {
		optionType, _ := reader.ReadString('\n')
		optionType = strings.TrimSpace(optionType)

		switch optionType {
		case "port":
			portStr, _ := reader.ReadString('\n')
			port, _ := strconv.Atoi(strings.TrimSpace(portStr))
			// TODO: Thêm tùy chọn WithPort vào opts
			_ = port // Xóa dòng này khi triển khai

		case "credentials":
			username, _ := reader.ReadString('\n')
			username = strings.TrimSpace(username)
			password, _ := reader.ReadString('\n')
			password = strings.TrimSpace(password)
			// TODO: Thêm tùy chọn WithCredentials vào opts
			_, _ = username, password // Xóa dòng này khi triển khai

		case "maxconn":
			maxStr, _ := reader.ReadString('\n')
			maxConn, _ := strconv.Atoi(strings.TrimSpace(maxStr))
			// TODO: Thêm tùy chọn WithMaxConnections vào opts
			_ = maxConn // Xóa dòng này khi triển khai

		case "timeout":
			timeoutStr, _ := reader.ReadString('\n')
			timeout, _ := strconv.Atoi(strings.TrimSpace(timeoutStr))
			// TODO: Thêm tùy chọn WithTimeout vào opts
			_ = timeout // Xóa dòng này khi triển khai
		}
	}

	// TODO: Tạo DBConnection bằng NewDBConnection với host và opts
	// TODO: In ra chuỗi kết nối bằng phương thức (method) ConnectionString()

	_ = opts // Xóa dòng này khi triển khai
	fmt.Println("TODO: Print connection string here")
}
quiz iconTự kiểm tra

Bài học này có một bài trắc nghiệm ngắn. Bắt đầu bài học để trả lời và theo dõi tiến độ.

Tất cả bài học trong Lập trình hướng đối tượng

Tự luyện tập: Trình biên dịch Go online