0

Go Zero to Hero - Bài 36: Khái niệm Interface và tính Đa hình (Polymorphism)

Trong lập trình hướng đối tượng và kiến trúc phần mềm, Interface là công cụ mạnh mẽ nhất giúp tách biệt hoàn toàn giữa "Bản thiết kế hành vi" và "Cách hiện thực hóa hành vi đó". Golang mang đến một cách tiếp cận Interface cực kỳ độc đáo và linh hoạt.


1. Interface là gì? Khái niệm "Ổ cắm đa năng"

Hãy tưởng tượng bạn đi du lịch và mang theo một chiếc sạc laptop. Chiếc sạc của bạn không hề quan tâm dòng điện phía sau bức tường được tạo ra từ thủy điện, nhiệt điện hay năng lượng mặt trời. Nó chỉ quan tâm một điều duy nhất: Cái ổ cắm trên tường có vừa với phích cắm của nó hay không.

Trong lập trình, Interface chính là cái ổ cắm đó.

Nó là một bản hợp đồng (contract) quy định trước một tập hợp các hành vi (Methods) mà một thực thể phải có, nhưng nó KHÔNG hề quan tâm thực thể đó làm cách nào để thực hiện hành vi ấy.

Cú pháp khai báo:

package main

// Định nghĩa một Interface
type PaymentGateway interface {
    // Chỉ khai báo tên hàm, tham số và kiểu trả về. 
    // TUYỆT ĐỐI KHÔNG CÓ phần thân hàm { ... }
    ProcessPayment(amount float64) (string, error)
}

2. Duck Typing: Sự khác biệt vĩ đại của Go

Nếu bạn đến từ Java hay C#, để một Class tuân thủ một Interface, bạn BẮT BUỘC phải dùng từ khóa implements.

Nhưng Go sử dụng một triết lý cực kỳ linh hoạt gọi là Duck Typing (Kiểu vịt):

"Nếu nó đi lạch bạch như một con vịt, quạc quạc như một con vịt, thì nó CẢNH SÁT MỌI NGƯỜI LÀ MỘT CON VỊT!"

Trong Go, bạn không cần phải khai báo rõ ràng rằng một Struct đang implements một Interface. Chỉ cần Struct đó sở hữu đầy đủ các Method (cùng tên, cùng tham số, cùng kiểu trả về) mà Interface yêu cầu, trình biên dịch sẽ tự động công nhận nó.

Hãy xem cách chúng ta định nghĩa 2 phương thức thanh toán hoàn toàn khác nhau cho hệ thống AFC:

import (
    "fmt"
    "errors"
)

// Thực thể 1: Thẻ thông minh (SmartCard) nội bộ
type SmartCard struct {
    CardID  string
    Balance float64
}

// SmartCard TỰ ĐỘNG thỏa mãn Interface PaymentGateway vì nó có hàm ProcessPayment chuẩn chỉ
func (s *SmartCard) ProcessPayment(amount float64) (string, error) {
    if s.Balance < amount {
        return "", errors.New("SmartCard: Số dư không đủ")
    }
    s.Balance -= amount
    return fmt.Sprintf("TXN-SC-%s", s.CardID), nil
}

// Thực thể 2: Ví điện tử Momo
type MomoWallet struct {
    PhoneNumber string
}

// MomoWallet CŨNG TỰ ĐỘNG thỏa mãn Interface PaymentGateway
func (m *MomoWallet) ProcessPayment(amount float64) (string, error) {
    // Logic gọi API sang hệ thống Momo...
    return fmt.Sprintf("TXN-MOMO-%s", m.PhoneNumber), nil
}

Việc không có từ khóa implements giúp các module trong Go hoàn toàn "mù" về nhau (Decoupled), một đặc điểm sống còn của Clean Architecture.


3. Tính Đa hình (Polymorphism) trong thực chiến

Đa hình là khả năng một biến (thuộc kiểu Interface) có thể đại diện cho nhiều loại Struct khác nhau tại thời điểm chạy (Runtime).

Lợi ích là gì?

Bạn có thể viết một hàm xử lý thanh toán duy nhất nhận vào cái "ổ cắm" PaymentGateway, và nó sẽ chạy trơn tru bất kể người dùng quẹt thẻ SmartCard, hay quét mã Momo.

// Hàm này KHÔNG CẦN BIẾT nó đang xử lý thẻ vật lý hay ví điện tử.
// neutrinos chỉ cần biết đối số truyền vào có hàm ProcessPayment() là được!
func Checkout(gateway PaymentGateway, fare float64) {
    txnID, err := gateway.ProcessPayment(fare)
    if err != nil {
        fmt.Println("❌ Giao dịch thất bại:", err)
        return
    }
    fmt.Println("✅ Giao dịch thành công! Mã chuẩn chi:", txnID)
}

func main() {
    // 1. Khách dùng thẻ thông minh
    myCard := &SmartCard{CardID: "1001", Balance: 50000}
    
    // 2. Khách dùng Momo
    myMomo := &MomoWallet{PhoneNumber: "0901234567"}

    fmt.Println("--- Khách A qua cổng ---")
    Checkout(myCard, 15000) // Đa hình: Truyền SmartCard vào Interface

    fmt.Println("--- Khách B qua cổng ---")
    Checkout(myMomo, 15000) // Đa hình: Truyền MomoWallet vào Interface
}

Sức mạnh mở rộng:

Tháng sau, nếu sếp yêu cầu tích hợp thêm VNPay, bạn chỉ cần tạo một Struct VNPay và viết hàm ProcessPayment cho nó. Hàm Checkout hoàn toàn không cần sửa đổi một dòng code nào! Đó chính là cốt lõi của nguyên lý Open-Closed (Mở để mở rộng, Đóng để sửa đổi) trong SOLID.


4. The "Empty Interface" interface{}: Con dao hai lưỡi

Vì Interface được thỏa mãn dựa trên số lượng Method, điều gì sẽ xảy ra nếu bạn tạo ra một Interface... không yêu cầu một Method nào cả?

var anything interface{}

Mọi Struct, mọi kiểu dữ liệu (từ int, string cho đến cả func) đều TỰ ĐỘNG thỏa mãn một Interface rỗng. Do đó, interface{} trong Go hoạt động giống hệt kiểu any trong TypeScript hay Object trong Java. Nó có thể chứa mọi thứ trên đời. (Từ Go 1.18, từ khóa any chính thức ra đời như một bí danh ngắn gọn thay cho interface{}).

Hàm fmt.Println chính là một ví dụ kinh điển: Nó nhận vào một mảng các interface{} để có thể in ra bất kỳ kiểu dữ liệu nào bạn ném vào.

⚠️ Cảnh báo Backend: Tuyệt đối hạn chế lạm dụng interface{} (hoặc any) trong logic nghiệp vụ. Việc sử dụng nó đồng nghĩa với việc bạn đang vô hiệu hóa toàn bộ sức mạnh kiểm tra kiểu (Type-checking) an toàn của trình biên dịch Go, khiến code dễ sinh bug ngầm lúc runtime.


Tổng kết

Interface chính là cánh cổng đưa bạn từ một thợ gõ code (Coder) trở thành một Kiến trúc sư phần mềm (Software Architect):

  1. Định nghĩa hành vi, không định nghĩa dữ liệu: Interface chỉ chứa chữ ký hàm, không có ruột.
  2. Implicit Implementation (Duck Typing): Struct tự động thỏa mãn Interface mà không cần từ khóa khai báo rườm rà.
  3. Đa hình: Cho phép một hàm duy nhất xử lý vô số thực thể khác nhau, miễn là chúng "cắm" vừa vào cái Interface đó.

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í