0

Series Go Backend: Interfaces (Phần 1) - Tính đa hình và Duck Typing trong Go

Trong các ngôn ngữ OOP truyền thống như C++, Java hay PHP, khi muốn một Class tuân thủ một Interface, bạn bắt buộc phải khai báo tường minh bằng từ khóa (ví dụ: class ZaloPay implements PaymentGateway).

Go đi theo một triết lý hoàn toàn khác biệt và linh hoạt hơn rất nhiều: Triển khai ngầm định (Implicit Implementation) dựa trên khái niệm Duck Typing.

1. Duck Typing là gì?

"If it walks like a duck, swims like a duck, and quacks like a duck, then it probably is a duck." (Nếu một con vật đi lạch bạch như vịt, bơi như vịt và kêu cạp cạp như vịt, thì nó chính là một con vịt).

Trong Go, bạn không bao giờ cần (và cũng không có cách nào) khai báo một Struct implements một Interface. Trình biên dịch của Go tự động kiểm tra: Nếu Struct của bạn sở hữu tất cả các methods mà Interface yêu cầu, thì Struct đó nghiễm nhiên được công nhận là đã triển khai Interface đó.

2. Định nghĩa Interface

Interface trong Go là một tập hợp các Method Signatures (Chỉ bao gồm tên hàm, tham số đầu vào và kiểu trả về, tuyệt đối không có phần thân hàm).

Giả sử chúng ta xây dựng hệ thống thanh toán cần hỗ trợ nhiều cổng khác nhau. Thay vì viết code dính chặt (coupled) vào một cổng cụ thể, ta định nghĩa một Interface:

Go

package main

import "fmt"

// Định nghĩa Interface PaymentGateway
// Bất cứ Struct nào có method tên là ProcessPayment nhận vào 1 số float64 và trả về error
// đều được coi là một PaymentGateway.
type PaymentGateway interface {
    ProcessPayment(amount float64) error
}

3. Triển khai Tính đa hình (Polymorphism)

Bây giờ, chúng ta định nghĩa hai Struct đại diện cho hai cổng thanh toán khác nhau và viết method cho chúng.

Go

// --- Cổng thanh toán ZaloPay ---
type ZaloPay struct {
    AppID string
}

// ZaloPay triển khai method ProcessPayment (Khớp 100% với Interface)
func (z ZaloPay) ProcessPayment(amount float64) error {
    fmt.Printf("Đang gọi API ZaloPay (AppID: %s) để thanh toán: %.2f VNĐ\n", z.AppID, amount)
    // Logic tạo QR code hoặc trừ tiền...
    return nil
}

// --- Cổng thanh toán VNPay ---
type VNPay struct {
    MerchantCode string
}

// VNPay cũng triển khai method ProcessPayment (Khớp 100% với Interface)
func (v VNPay) ProcessPayment(amount float64) error {
    fmt.Printf("Đang redirect sang VNPay (Merchant: %s) để thanh toán: %.2f VNĐ\n", v.MerchantCode, amount)
    return nil
}

Lúc này, phép màu của tính đa hình sẽ phát huy tác dụng ở tầng Business Logic (ví dụ: Service hoặc Controller xử lý Checkout). Chúng ta sẽ viết một hàm nhận vào PaymentGateway thay vì nhận các Struct cụ thể:

Go

// Hàm Checkout không cần biết nó đang dùng ZaloPay hay VNPay.
// Nó chỉ quan tâm: Truyền vào cho tôi một đối tượng có khả năng ProcessPayment.
func Checkout(gw PaymentGateway, amount float64) {
    fmt.Println("--- Bắt đầu quy trình Checkout ---")
    err := gw.ProcessPayment(amount)
    
    if err != nil {
        fmt.Println("Thanh toán thất bại:", err)
        return
    }
    fmt.Println("Thanh toán thành công!")
}

func main() {
    // Khởi tạo các object
    zalo := ZaloPay{AppID: "ZLP_12345"}
    vnpay := VNPay{MerchantCode: "VNP_9999"}
    
    // Đa hình: Truyền ZaloPay vào hàm Checkout
    Checkout(zalo, 500000)
    
    // Đa hình: Truyền VNPay vào hàm Checkout
    Checkout(vnpay, 150000)
}

Tại sao điều này lại làm nên sức mạnh của Backend Go?

  • Decoupling (Giảm thiểu sự phụ thuộc): Hàm Checkout hoàn toàn độc lập với các thư viện thanh toán. Ngày mai bạn muốn tích hợp thêm Momo hay Stripe, bạn chỉ cần tạo Struct mới và viết hàm ProcessPayment cho nó. Code cốt lõi của hàm Checkout không cần sửa dù chỉ một dòng.

  • Mocking dễ dàng: Khi viết Unit Test cho hàm Checkout, thay vì phải gọi API thật tốn tiền, bạn chỉ cần tạo một Struct giả (MockGateway) có hàm ProcessPayment trả về nil (thành công) hoặc error (thất bại) để test các kịch bản khác nhau.

4. Triết lý thiết kế Interface trong Go

"The bigger the interface, the weaker the abstraction." - Rob Pike (Đồng tác giả ngôn ngữ Go)

Trong Java/C#, các interface thường rất đồ sộ, nhét hàng tá methods vào chung một file (ví dụ: IUserRepository có đủ Create, Read, Update, Delete).

Trong Go, cộng đồng khuyến khích thiết kế Interface cực nhỏ, thường chỉ chứa từ 1 đến 2 methods. Các interface chuẩn của Go như io.Reader (chỉ có hàm Read) hay io.Writer (chỉ có hàm Write) là minh chứng kinh điển cho triết lý này. Các interface nhỏ dễ dàng lắp ghép (compose) lại với nhau thành các interface lớn hơn khi cần thiết.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí