Menu
CoddyTech

Quy tắc thỏa mãn interface

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

Dù Go dùng kiểu định kiểu vịt để ngầm đáp ứng giao diện, vẫn có những quy tắc cụ thể xác định một kiểu có thực sự đáp ứng giao diện hay không. Hiểu rõ các quy tắc này giúp bạn tránh những bug khó nhận ra.

Quy tắc quan trọng nhất liên quan đến bộ nhận con trỏ (pointer receiver). Nếu một phương thức được định nghĩa với bộ nhận con trỏ, chỉ con trỏ đến kiểu đó mới đáp ứng giao diện, chứ không phải bản thân giá trị:

type Saver interface {
    Save() string
}

type Document struct{ Name string }

func (d *Document) Save() string {  // bộ nhận kiểu con trỏ
    return "Saved: " + d.Name
}

func Process(s Saver) {
    fmt.Println(s.Save())
}

Vì Save() có receiver là con trỏ, chỉ *Document mới thỏa mãn Saver:

func main() {
    doc := Document{Name: "report.txt"}
    
    Process(&doc)  // Chạy được: con trỏ đáp ứng giao diện (interface)
    // Process(doc)  // lỗi biên dịch: giá trị không đáp ứng giao diện
}

Tuy nhiên, cách ngược lại linh hoạt hơn. Nếu một phương thức có bộ nhận giá trị, cả kiểu giá trị lẫn kiểu con trỏ đều thỏa mãn giao diện. Go tự động bỏ tham chiếu con trỏ khi gọi các phương thức có bộ nhận giá trị:

func (d Document) Info() string {  // receiver giá trị
    return d.Name
}

// Cả Document và *Document đều đáp ứng interface yêu cầu Info()

Sự bất đối xứng này tồn tại vì Go luôn có thể lấy giá trị từ một con trỏ (bằng cách giải tham chiếu), nhưng không phải lúc nào cũng lấy được con trỏ từ một giá trị (giá trị đó có thể không thể lấy địa chỉ). Ghi nhớ quy tắc này giúp bạn tránh gặp lỗi trình biên dịch khó hiểu khi làm việc với các interface.

challenge icon

Thử thách

Dễ

Hãy xây dựng một hệ thống cấu hình để minh họa cách bộ nhận giá trị (value receiver) và bộ nhận con trỏ (pointer receiver) ảnh hưởng đến việc một kiểu dữ liệu có thỏa mãn giao diện (interface) hay không. Bạn sẽ tạo các kiểu mà lựa chọn bộ nhận quyết định liệu giá trị, con trỏ hay cả hai có thể dùng với một giao diện hay không.

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

  • config.go: Định nghĩa giao diện Configurable, yêu cầu hai phương thức: GetValue() string và SetValue(string). Sau đó tạo hai kiểu cấu hình:
    • ReadOnlyConfig có trường Value: dùng bộ nhận giá trị cho GetValue() (trả về Value) và bộ nhận con trỏ cho SetValue() (cập nhật Value)
    • Setting có trường Data: dùng bộ nhận giá trị cho cả hai phương thức (GetValue trả về Data, SetValue chỉ in ra "Cannot modify" mà không thay đổi gì)
  • processor.go: Tạo hàm có tên ProcessConfig, nhận một Configurable và một chuỗi giá trị mới. Hàm này sẽ in ra giá trị hiện tại bằng GetValue(), gọi SetValue() với giá trị mới, rồi in lại giá trị để cho thấy mọi thay đổi.
  • main.go: Đọc thông tin cấu hình từ đầu vào và minh họa các quy tắc thỏa mãn giao diện:
    1. Tạo một ReadOnlyConfig và truyền một con trỏ vào ProcessConfig (cần thiết vì SetValue có bộ nhận con trỏ)
    2. Tạo một Setting và truyền trực tiếp giá trị vào ProcessConfig (hoạt động vì cả hai phương thức đều có bộ nhận giá trị)

Đầu vào sẽ gồm:

  • Dòng 1: Giá trị ban đầu của ReadOnlyConfig
  • Dòng 2: Giá trị mới cần đặt cho ReadOnlyConfig
  • Dòng 3: Giá trị ban đầu của Setting
  • Dòng 4: Giá trị mới định đặt cho Setting

Hàm ProcessConfig của bạn cần in ra theo định dạng sau:

Current: [value]
Current: [value after SetValue]

Ví dụ, với các giá trị debug, production, localhost và remote, đầu ra sẽ là:

Current: debug
Current: production
Current: localhost
Cannot modify
Current: localhost

Lưu ý rằng giá trị của ReadOnlyConfig thực sự thay đổi (vì ta đã truyền một con trỏ), còn Setting vẫn giữ nguyên (phương thức SetValue có bộ nhận giá trị không thể sửa đổi giá trị ban đầu). Điểm mấu chốt là chỉ riêng các giá trị ReadOnlyConfig không thỏa mãn Configurable, chỉ các con trỏ mới thỏa mãn, còn các giá trị Setting có thể dùng trực tiếp vì mọi phương thức của nó đều dùng bộ nhận giá trị.

Tự làm thử

package main

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

func main() {
	scanner := bufio.NewScanner(os.Stdin)
	
	// Đọc các đầu vào
	scanner.Scan()
	rocInitial := scanner.Text()
	scanner.Scan()
	rocNew := scanner.Text()
	scanner.Scan()
	settingInitial := scanner.Text()
	scanner.Scan()
	settingNew := scanner.Text()
	
	// TODO: Tạo ReadOnlyConfig với giá trị rocInitial
	// TODO: Truyền CON TRỎ vào ProcessConfig (cần thiết vì SetValue có bộ nhận là con trỏ)
	
	// TODO: Tạo Setting với giá trị settingInitial
	// TODO: Truyền trực tiếp GIÁ TRỊ vào ProcessConfig (vì cả hai phương thức đều có bộ nhận là giá trị)
	
	// Dùng các biến này để tránh lỗi biến không được sử dụng
	_ = rocInitial
	_ = rocNew
	_ = settingInitial
	_ = settingNew
}
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