Back to blog

Học Go: Ưu Tiên Composition Hơn Inheritance

GoDesign PatternsCompositionObject-Oriented ProgrammingBest Practices
Học Go: Ưu Tiên Composition Hơn Inheritance

Giới Thiệu

Một trong những tính năng nổi bật nhất của Go là việc cố ý bỏ qua class-based inheritance. Nếu bạn đến từ các ngôn ngữ như Java, C++ hay Python, điều này có thể có vẻ hạn chế lúc đầu. Tuy nhiên, Go áp dụng nguyên tắc "composition over inheritance" để tạo ra code linh hoạt, dễ bảo trì và ít phụ thuộc.

Trong bài viết này, chúng ta sẽ khám phá tại sao Go đưa ra lựa chọn thiết kế này, hiểu các vấn đề với inheritance truyền thống, và thành thạo các pattern composition thực tế giúp code Go trở nên thanh lịch và mạnh mẽ.

Vấn Đề Với Inheritance

Trước khi đi sâu vào giải pháp của Go, hãy hiểu tại sao inheritance có thể gây vấn đề.

Vấn Đề Fragile Base Class

# Ví dụ Python cho thấy các vấn đề inheritance
class Animal:
    def __init__(self, name):
        self.name = name
        self.energy = 100
    
    def move(self):
        self.energy -= 10
        print(f"{self.name} moved. Energy: {self.energy}")
 
class Bird(Animal):
    def fly(self):
        self.move()  # Phụ thuộc vào move() của parent
        print(f"{self.name} is flying!")
 
# Sau đó, ai đó sửa đổi class Animal
class Animal:
    def __init__(self, name):
        self.name = name
        self.energy = 100
    
    def move(self, distance=1):  # Đã thay đổi signature!
        self.energy -= 10 * distance
        print(f"{self.name} moved {distance} units")
 
# Bird.fly() giờ bị lỗi hoặc hoạt động không như mong đợi!

Vấn đề: Thay đổi ở base class có thể phá vỡ tất cả các derived classes, thường theo cách khó phát hiện.

Vấn Đề Diamond Problem

class A:
    def method(self):
        print("A's method")
 
class B(A):
    def method(self):
        print("B's method")
 
class C(A):
    def method(self):
        print("C's method")
 
class D(B, C):  # D kế thừa method() nào?
    pass
 
d = D()
d.method()  # Không rõ ràng! Phụ thuộc vào MRO (Method Resolution Order)

Vấn đề: Multiple inheritance tạo ra sự mơ hồ và phức tạp.

Hệ Thống Phân Cấp Inheritance Sâu

// Ví dụ Java
class Vehicle { }
class LandVehicle extends Vehicle { }
class MotorizedLandVehicle extends LandVehicle { }
class Car extends MotorizedLandVehicle { }
class ElectricCar extends Car { }
 
// Để hiểu ElectricCar, bạn phải hiểu 5 classes!

Vấn đề: Hệ thống phân cấp sâu khó hiểu, sửa đổi và bảo trì.

Cách Tiếp Cận Của Go: Không Classes, Không Inheritance

Go không có classes hay inheritance. Thay vào đó, nó cung cấp:

  1. Structs: Để lưu dữ liệu
  2. Methods: Functions gắn với types
  3. Interfaces: Để định nghĩa behavior contracts (được thỏa mãn ngầm định)
  4. Embedding: Để compose types

Hãy khám phá từng khái niệm.

Các Khối Xây Dựng Cơ Bản

Structs và Methods

package main
 
import "fmt"
 
// Struct chứa dữ liệu
type Dog struct {
    Name   string
    Breed  string
    Energy int
}
 
// Method gắn với type Dog
func (d *Dog) Bark() {
    fmt.Printf("%s says: Woof!\n", d.Name)
    d.Energy -= 5
}
 
func (d *Dog) Eat() {
    fmt.Printf("%s is eating.\n", d.Name)
    d.Energy += 20
}
 
func main() {
    dog := Dog{Name: "Buddy", Breed: "Golden Retriever", Energy: 100}
    dog.Bark()
    dog.Eat()
    fmt.Printf("Energy: %d\n", dog.Energy)
}

Output:

Buddy says: Woof!
Buddy is eating.
Energy: 115

Pattern Composition #1: Struct Embedding

Go cho phép bạn embed một struct vào struct khác, promoting các fields và methods của embedded struct.

package main
 
import "fmt"
 
// Base types
type Engine struct {
    Horsepower int
    Type       string
}
 
func (e Engine) Start() {
    fmt.Printf("Starting %s engine with %d HP\n", e.Type, e.Horsepower)
}
 
func (e Engine) Stop() {
    fmt.Println("Engine stopped")
}
 
// Car embeds Engine
type Car struct {
    Engine  // Embedded struct
    Make    string
    Model   string
    Year    int
}
 
func (c Car) Drive() {
    fmt.Printf("Driving %d %s %s\n", c.Year, c.Make, c.Model)
}
 
func main() {
    car := Car{
        Engine: Engine{Horsepower: 300, Type: "V6"},
        Make:   "Toyota",
        Model:  "Camry",
        Year:   2024,
    }
    
    // Có thể gọi methods của Engine trực tiếp trên Car
    car.Start()
    car.Drive()
    car.Stop()
    
    // Cũng có thể truy cập Engine một cách rõ ràng
    fmt.Printf("Engine type: %s\n", car.Engine.Type)
}

Output:

Starting V6 engine with 300 HP
Driving 2024 Toyota Camry
Engine stopped
Engine type: V6

Cách Embedding Hoạt Động

Khi bạn embed một type, các fields và methods của nó được promoted lên outer type. Giống như automatic delegation.

type Inner struct {
    X int
}
 
func (i Inner) Method() {
    fmt.Println("Inner method")
}
 
type Outer struct {
    Inner  // Embedding
    Y int
}
 
func main() {
    o := Outer{Inner: Inner{X: 10}, Y: 20}
    
    // Truy cập trực tiếp các promoted fields
    fmt.Println(o.X)  // 10 (promoted từ Inner)
    fmt.Println(o.Y)  // 20
    
    // Truy cập trực tiếp các promoted methods
    o.Method()  // "Inner method" (promoted từ Inner)
}

Pattern Composition #2: Quan Hệ Has-A

Thay vì quan hệ "is-a" của inheritance, composition sử dụng quan hệ "has-a".

package main
 
import "fmt"
 
type Logger struct {
    Prefix string
}
 
func (l Logger) Log(message string) {
    fmt.Printf("[%s] %s\n", l.Prefix, message)
}
 
type Database struct {
    logger Logger  // Quan hệ Has-a (không embedded)
    Name   string
}
 
func (db Database) Connect() {
    db.logger.Log(fmt.Sprintf("Connecting to database: %s", db.Name))
}
 
func (db Database) Query(sql string) {
    db.logger.Log(fmt.Sprintf("Executing query: %s", sql))
}
 
func main() {
    db := Database{
        logger: Logger{Prefix: "DB"},
        Name:   "users_db",
    }
    
    db.Connect()
    db.Query("SELECT * FROM users")
}

Output:

[DB] Connecting to database: users_db
[DB] Executing query: SELECT * FROM users

Sự Khác Biệt Chính:

  • Embedded (Engine trong Car): Methods được promoted, có thể gọi car.Start()
  • Không Embedded (logger trong Database): Phải gọi rõ ràng db.logger.Log()

Pattern Composition #3: Multiple Composition

Bạn có thể embed nhiều types để kết hợp khả năng của chúng.

package main
 
import "fmt"
 
type Walker struct {
    Speed int
}
 
func (w Walker) Walk() {
    fmt.Printf("Walking at %d km/h\n", w.Speed)
}
 
type Swimmer struct {
    Speed int
}
 
func (s Swimmer) Swim() {
    fmt.Printf("Swimming at %d km/h\n", s.Speed)
}
 
type Flyer struct {
    Speed int
}
 
func (f Flyer) Fly() {
    fmt.Printf("Flying at %d km/h\n", f.Speed)
}
 
// Duck có thể walk, swim, và fly
type Duck struct {
    Walker
    Swimmer
    Flyer
    Name string
}
 
// Penguin có thể walk và swim (nhưng không fly)
type Penguin struct {
    Walker
    Swimmer
    Name string
}
 
func main() {
    duck := Duck{
        Walker:  Walker{Speed: 5},
        Swimmer: Swimmer{Speed: 10},
        Flyer:   Flyer{Speed: 50},
        Name:    "Donald",
    }
    
    fmt.Printf("%s the duck:\n", duck.Name)
    duck.Walk()
    duck.Swim()
    duck.Fly()
    
    fmt.Println()
    
    penguin := Penguin{
        Walker:  Walker{Speed: 3},
        Swimmer: Swimmer{Speed: 15},
        Name:    "Pingu",
    }
    
    fmt.Printf("%s the penguin:\n", penguin.Name)
    penguin.Walk()
    penguin.Swim()
    // penguin.Fly() // Compile error! Penguin không thể fly
}

Output:

Donald the duck:
Walking at 5 km/h
Swimming at 10 km/h
Flying at 50 km/h
 
Pingu the penguin:
Walking at 3 km/h
Swimming at 15 km/h

Pattern Composition #4: Interfaces Cho Polymorphism

Kết hợp composition với interfaces cho polymorphism linh hoạt.

package main
 
import "fmt"
 
// Interface định nghĩa behavior
type Notifier interface {
    Notify(message string)
}
 
// Email notifier
type EmailNotifier struct {
    EmailAddress string
}
 
func (e EmailNotifier) Notify(message string) {
    fmt.Printf("Sending email to %s: %s\n", e.EmailAddress, message)
}
 
// SMS notifier
type SMSNotifier struct {
    PhoneNumber string
}
 
func (s SMSNotifier) Notify(message string) {
    fmt.Printf("Sending SMS to %s: %s\n", s.PhoneNumber, message)
}
 
// User embeds một notifier
type User struct {
    Name     string
    Notifier // Embedded interface!
}
 
func (u User) Alert(message string) {
    fmt.Printf("Alerting %s:\n", u.Name)
    u.Notify(message)
}
 
func main() {
    // User với email notifications
    user1 := User{
        Name:     "Alice",
        Notifier: EmailNotifier{EmailAddress: "[email protected]"},
    }
    user1.Alert("Your order has shipped!")
    
    fmt.Println()
    
    // User với SMS notifications
    user2 := User{
        Name:     "Bob",
        Notifier: SMSNotifier{PhoneNumber: "+1-555-0123"},
    }
    user2.Alert("Your package is delivered!")
}

Output:

Alerting Alice:
Sending email to [email protected]: Your order has shipped!
 
Alerting Bob:
Sending SMS to +1-555-0123: Your package is delivered!

Pattern Mạnh Mẽ: Bằng cách embedding một interface, bạn có thể swap implementations tại runtime!

Pattern Composition #5: Method Overriding

Bạn có thể "override" embedded methods bằng cách định nghĩa một method cùng tên trên outer type.

package main
 
import "fmt"
 
type Base struct {
    Value int
}
 
func (b Base) Display() {
    fmt.Printf("Base value: %d\n", b.Value)
}
 
func (b Base) Process() {
    fmt.Println("Processing in Base")
    b.Display()
}
 
type Extended struct {
    Base
    Extra string
}
 
// "Override" Display method
func (e Extended) Display() {
    fmt.Printf("Extended value: %d, extra: %s\n", e.Value, e.Extra)
}
 
func main() {
    ext := Extended{
        Base:  Base{Value: 42},
        Extra: "hello",
    }
    
    ext.Display()  // Gọi Extended.Display()
    ext.Process()  // Gọi Base.Process(), gọi Base.Display()!
    
    // Để gọi Display overridden từ Process, bạn cần explicit forwarding
}

Output:

Extended value: 42, extra: hello
Processing in Base
Base value: 42

Quan Trọng: Không giống inheritance, các methods của embedded type vẫn gọi các methods của embedded type, không phải các methods "overridden". Điều này ngăn chặn vấn đề fragile base class!

Ví Dụ Thực Tế: HTTP Handler Middleware

Hãy xây dựng một ví dụ thực tế: HTTP middleware sử dụng composition.

package main
 
import (
    "fmt"
    "log"
    "net/http"
    "time"
)
 
// Base handler
type Handler struct {
    Pattern string
}
 
func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello from %s\n", h.Pattern)
}
 
// Logging middleware
type LoggingHandler struct {
    http.Handler
}
 
func (l LoggingHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    start := time.Now()
    log.Printf("Started %s %s", r.Method, r.URL.Path)
    
    l.Handler.ServeHTTP(w, r)  // Delegate đến wrapped handler
    
    log.Printf("Completed in %v", time.Since(start))
}
 
// Authentication middleware
type AuthHandler struct {
    http.Handler
    APIKey string
}
 
func (a AuthHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    key := r.Header.Get("X-API-Key")
    if key != a.APIKey {
        http.Error(w, "Unauthorized", http.StatusUnauthorized)
        return
    }
    
    a.Handler.ServeHTTP(w, r)  // Delegate đến wrapped handler
}
 
func main() {
    // Compose handlers
    baseHandler := Handler{Pattern: "/api"}
    
    // Wrap với auth
    authHandler := AuthHandler{
        Handler: baseHandler,
        APIKey:  "secret123",
    }
    
    // Wrap với logging
    loggingHandler := LoggingHandler{
        Handler: authHandler,
    }
    
    http.Handle("/api", loggingHandler)
    
    fmt.Println("Server starting on :8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

Pattern này được sử dụng xuyên suốt standard library của Go!

Lợi Ích Của Composition So Với Inheritance

1. Tính Linh Hoạt

Bạn có thể dễ dàng mix và match các components mà không bị khóa vào một hệ thống phân cấp.

2. Sự Rõ Ràng

Rõ ràng mỗi type chứa gì. Không có behavior kế thừa ẩn.

3. Không Có Vấn Đề Fragile Base Class

Thay đổi một component chỉ ảnh hưởng đến các types sử dụng nó một cách rõ ràng.

4. Testing Dễ Dàng

Bạn có thể test các components độc lập và mock các embedded interfaces.

type MockNotifier struct {
    Messages []string
}
 
func (m *MockNotifier) Notify(message string) {
    m.Messages = append(m.Messages, message)
}
 
// Sử dụng trong tests
func TestUser(t *testing.T) {
    mock := &MockNotifier{}
    user := User{Name: "Test", Notifier: mock}
    
    user.Alert("Test message")
    
    if len(mock.Messages) != 1 {
        t.Errorf("Expected 1 message, got %d", len(mock.Messages))
    }
}

5. Không Có Diamond Problem

Xung đột tên field được giải quyết rõ ràng.

type A struct {
    Value int
}
 
type B struct {
    Value string
}
 
type C struct {
    A
    B
}
 
func main() {
    c := C{}
    // c.Value  // Compile error: mơ hồ
    c.A.Value = 10      // Rõ ràng
    c.B.Value = "hello" // Rõ ràng
}

Best Practices

1. Giữ Interfaces Nhỏ

// Tốt: Interfaces nhỏ, tập trung
type Reader interface {
    Read(p []byte) (n int, err error)
}
 
type Writer interface {
    Write(p []byte) (n int, err error)
}
 
// Compose khi cần
type ReadWriter interface {
    Reader
    Writer
}

2. Accept Interfaces, Return Structs

// Tốt
func ProcessData(r io.Reader) (*Result, error) {
    // ...
}
 
// Ít linh hoạt hơn
func ProcessData(f *os.File) (*Result, error) {
    // ...
}

3. Embed Interfaces Cho Tính Linh Hoạt

type Service struct {
    Storage interface {
        Save(data []byte) error
        Load() ([]byte, error)
    }
}
 
// Dễ dàng swap implementations

4. Sử Dụng Composition Cho Cross-Cutting Concerns

Logging, metrics, authentication, v.v. hoàn hảo cho composition.

5. Ưu Tiên Explicit Hơn Implicit

Nếu embedding làm code không rõ ràng, hãy sử dụng explicit fields và methods.

// Nếu điều này gây nhầm lẫn:
type Confusing struct {
    ComponentA
    ComponentB
    ComponentC
}
 
// Hãy xem xét điều này:
type Clear struct {
    compA ComponentA
    compB ComponentB
    compC ComponentC
}
 
func (c Clear) DoSomething() {
    c.compA.MethodA()
    c.compB.MethodB()
}

Các Pattern Phổ Biến

Decorator Pattern

type Component interface {
    Operation() string
}
 
type ConcreteComponent struct{}
 
func (c ConcreteComponent) Operation() string {
    return "Base"
}
 
type Decorator struct {
    Component
}
 
func (d Decorator) Operation() string {
    return "Decorated(" + d.Component.Operation() + ")"
}

Strategy Pattern

type Strategy interface {
    Execute(data []int) int
}
 
type Context struct {
    Strategy
}
 
func (c Context) Process(data []int) int {
    return c.Strategy.Execute(data)
}

Kết Luận

Mô hình composition của Go là một lựa chọn thay thế mạnh mẽ cho classical inheritance:

  • Structs + Methods: Các khối xây dựng cơ bản
  • Embedding: Automatic delegation và promotion
  • Interfaces: Polymorphism không có tight coupling
  • Composition: Quan hệ "has-a" linh hoạt

Bằng cách ưu tiên composition, Go khuyến khích:

  • Quan hệ type đơn giản hơn, phẳng hơn
  • Code rõ ràng hơn
  • Khả năng test tốt hơn
  • Tính linh hoạt cao hơn

Sự vắng mặt của inheritance không phải là một hạn chế—đó là một lựa chọn thiết kế có chủ ý dẫn đến code dễ bảo trì và linh hoạt hơn. Khi bạn chấp nhận composition, bạn sẽ thấy nó thường là một giải pháp tốt hơn so với inheritance.

Đọc Thêm

📬 Đăng ký nhận bản tin

Nhận những bài viết mới nhất qua email mỗi tuần. Không spam, hủy đăng ký bất cứ lúc nào.

Chúng tôi tôn trọng quyền riêng tư của bạn. Hủy đăng ký bất cứ lúc nào.

💬 Comments

Đăng nhập để bình luận

Chúng tôi sẽ không đăng bất cứ điều gì mà không có sự cho phép của bạn.