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:
- Structs: Để lưu dữ liệu
- Methods: Functions gắn với types
- Interfaces: Để định nghĩa behavior contracts (được thỏa mãn ngầm định)
- 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: 115Pattern 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: V6Cá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 usersSự Khác Biệt Chính:
- Embedded (
EnginetrongCar): Methods được promoted, có thể gọicar.Start() - Không Embedded (
loggertrongDatabase): Phải gọi rõ ràngdb.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/hPattern 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: 42Quan 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 implementations4. 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
- Effective Go - Embedding
- Go Blog - Composition with Go
- Design Patterns in Go
- Go Proverbs - "The bigger the interface, the weaker the abstraction"
📬 Đă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.