Menu
CoddyTech

Interface như một hợp đồng

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 27 trên 107.

Xem các interface như những hợp đồng giúp bạn thiết kế chương trình Go tốt hơn. Khi một hàm nhận kiểu interface, điều đó có nghĩa là: "Mình không quan tâm bạn đưa kiểu cụ thể nào, miễn là kiểu đó đáp ứng hợp đồng này."

Cách suy nghĩ dựa trên hợp đồng này giúp bạn viết các hàm tập trung vào hành vi thay vì các kiểu dữ liệu cụ thể. Hãy xem một hàm cần xử lý dữ liệu từ nhiều nguồn khác nhau:

type DataSource interface {
    FetchData() string
}

func ProcessData(src DataSource) {
    data := src.FetchData()
    fmt.Println("Processing:", data)
}

Hàm ProcessData không biết và cũng không quan tâm liệu nó nhận được kết nối cơ sở dữ liệu, ứng dụng khách API hay trình đọc tệp. Hàm chỉ yêu cầu đầu vào có thể gọi FetchData(). Bất kỳ kiểu nào tuân theo hợp đồng này đều dùng được.

type Database struct{ ConnectionString string }
func (d Database) FetchData() string { return "data from DB" }

type APIClient struct{ Endpoint string }
func (a APIClient) FetchData() string { return "data from API" }

func main() {
    db := Database{ConnectionString: "localhost"}
    api := APIClient{Endpoint: "https://api.example.com"}
    
    ProcessData(db)   // Chạy được!
    ProcessData(api)  // Cũng chạy được!
}

Cách tiếp cận hợp đồng này cũng giúp code của bạn dễ kiểm thử hơn. Khi kiểm thử, bạn có thể tạo một kiểu giả lập (mock) đáp ứng giao diện (interface) mà không cần cơ sở dữ liệu hay kết nối mạng thật. Hàm (function) đang được kiểm thử không nhận ra sự khác biệt vì hợp đồng đã được đáp ứng.

challenge icon

Thử thách

Dễ

Hãy xây dựng một hệ thống thông báo để minh họa cách các interface đóng vai trò như những hợp đồng. Bạn sẽ tạo một interface Notifier quy định việc gửi thông báo cần những gì, sau đó triển khai nhiều kênh thông báo cùng tuân theo hợp đồng này.

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

  • notifiers.go: Định nghĩa một interface Notifier có một phương thức duy nhất là Notify(message string) string. Sau đó, tạo ba struct khác nhau đáp ứng hợp đồng này:
    • ConsoleNotifier có trường Prefix
    • FileNotifier có trường Filename
    • WebhookNotifier có trường URL
    Mỗi struct cần triển khai phương thức Notify để trả về một chuỗi mô tả cách gửi thông báo.
  • main.go: Tạo một hàm có tên BroadcastAlert nhận một lát cắt gồm các giá trị Notifier và một chuỗi thông báo. Hàm này cần gọi Notify trên từng notifier và in ra kết quả. Đọc cấu hình từ đầu vào, tạo cả ba kiểu notifier rồi gửi thông báo qua tất cả các notifier.

Đầu vào sẽ gồm các dòng sau:

  • Dòng 1: Tiền tố của Console
  • Dòng 2: Tên tệp
  • Dòng 3: URL của webhook
  • Dòng 4: Nội dung thông báo

Các phương thức Notify cần trả về chuỗi theo những định dạng sau:

  • ConsoleNotifier: [Prefix] [message]
  • FileNotifier: Writing to [Filename]: [message]
  • WebhookNotifier: POST [URL]: [message]

Ví dụ, với đầu vào ALERT:, log.txt, https://hooks.example.com và Server down!, kết quả của bạn sẽ là:

ALERT: Server down!
Writing to log.txt: Server down!
POST https://hooks.example.com: Server down!

Điểm mạnh của thiết kế này là hàm BroadcastAlert không cần biết hoặc quan tâm đến các kiểu notifier cụ thể. Hàm chỉ yêu cầu mỗi notifier tuân thủ hợp đồng Notifier. Sau này, bạn có thể thêm SlackNotifier hoặc EmailNotifier mà không cần thay đổi hàm gửi thông báo.

Tự làm thử

package main

import (
	"bufio"
	"fmt"
	"os"
)

// BroadcastAlert gửi thông điệp qua tất cả các bộ thông báo
// TODO: Triển khai hàm này để:
// - Nhận một slice các giá trị Notifier và một chuỗi thông điệp
// - Gọi Notify trên từng bộ thông báo và in kết quả
func BroadcastAlert(notifiers []Notifier, message string) {
	// TODO: Lặp qua notifiers và in kết quả của từng thông báo
}

func main() {
	scanner := bufio.NewScanner(os.Stdin)
	
	// Đọc tiền tố cho bảng điều khiển
	scanner.Scan()
	prefix := scanner.Text()
	
	// Đọc tên tệp
	scanner.Scan()
	filename := scanner.Text()
	
	// Đọc URL của webhook
	scanner.Scan()
	url := scanner.Text()
	
	// Đọc thông điệp cảnh báo
	scanner.Scan()
	message := scanner.Text()
	
	// TODO: Tạo instance của cả ba kiểu bộ thông báo
	// - ConsoleNotifier với tiền tố
	// - FileNotifier với tên tệp
	// - WebhookNotifier với URL
	
	// TODO: Tạo một lát cắt gồm cả ba giá trị Notifier
	
	// TODO: Gọi BroadcastAlert với các giá trị thông báo và message
	
	_ = prefix
	_ = filename
	_ = url
	_ = message
	fmt.Println("TODO: Implement the solution")
}
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