Phương pháp lập trình hướng đối tượng
Phần trước đã giới thiệu hàm (function) giúp tái sử dụng code, đó là kiểu lập trình hàm. Lập trình hướng đối tượng (OOP) dựa trên đối tượng và phương thức, cho phép thao tác trên một đối tượng cụ thể thay vì chỉ gọi hàm đơn thuần.
So sánh hàm và phương thức
// Cách tiếp cận với hàm
func an(nguoi string) {}
func ngu(nguoi string) {}
func choi(nguoi string) {}
// Cách tiếp cận với đối tượng và phương thức
// xiaoHong.an(), xiaoHong.ngu(), xiaoHong.choi()
// Phương thức gắn liền với đối tượng thực hiện hành động
Phương thức đối tượng trong Go
Kiểu tùy chỉnh và phương thức
package main
import "fmt"
type MyInteger int
func (m MyInteger) Display() {
fmt.Printf("Giá trị hiện tại: %d\n", m)
}
func main() {
var val MyInteger = 5
val.Display()
}
Khác biệt giữa hàm và phương thức:
- Hàm:
func tenHam(thamSo) (kqTraVe) - Phương thức:
func (bienNhan kieuNhan) tenPhuongThuc(thamSo) (kqTraVe)
Bộ nhận (Receiver) trong phương thức
Bộ nhận là đối tượng mà phương thức tác động lên. Nó tạo sự liên kết giữa kiểu dữ liệu và phương thức.
package main
import "fmt"
type Person struct {
name string
age uint8
}
func (p *Person) Eat() {
fmt.Printf("%s đang ăn...\n", p.name)
}
func (p *Person) Sleep() {
fmt.Printf("%s đang ngủ...\n", p.name)
}
func (p *Person) Play(game string) {
fmt.Printf("%s đang chơi: %s...\n", p.name, game)
}
func main() {
p := &Person{name: "Eson", age: 2}
p.Eat()
p.Sleep()
p.Play("football")
}
Tổng kết về OOP trong Go
- Mọi kiểu dữ liệu tùy chỉnh đều có thể định nghĩa phương thức (ngoại trừ kiểu built-in và interface)
- Phương thức được liên kết với kiểu dữ liệu thông qua bộ nhận
- Struct thường được dùng làm kiểu dữ liệu cho phương thức, và nên dùng con trỏ để dễ dàng thay đổi giá trị
Kế thừa thông qua Composition
Go không hỗ trợ từ khóa extends, thay vào đó dùng composition (hợp thành) để thay thế kế thừa.
package main
import "fmt"
type Animal struct {
Name string
}
func (a *Animal) Eat() {
fmt.Printf("%v đang ăn\n", a.Name)
}
type Cat struct {
*Animal
}
func main() {
cat := &Cat{
Animal: &Animal{Name: "Mimi"},
}
cat.Eat() // Mimi đang ăn
}
Interface trong Go
Interface trong Go là một kiểu dữ liệu trừu tượng, tập hợp các phương thức. Nó chỉ quan tâm đến hành vi (phương thức), không quan tâm đến thuộc tính (dữ liệu).
Tại sao cần Interface?
Giả sử ta có hệ thống cảnh báo (alert) với SMS, WeChat... Nếu không dùng interface, code sẽ trở nên rất lộn xộn với nhiều logic lặp lại.
package main
import "fmt"
type Alerter interface {
InsertAlarm()
IsAlarm() bool
SendAlarm()
}
type SmsAlarms struct {
ModuleName string
PhoneNumber int
}
func (s *SmsAlarms) InsertAlarm() {
fmt.Printf("SMS - Module: %s ghi cảnh báo vào DB\n", s.ModuleName)
}
func (s *SmsAlarms) IsAlarm() bool {
fmt.Printf("SMS - Module: %s kiểm tra trạng thái cảnh báo\n", s.ModuleName)
return true
}
func (s *SmsAlarms) SendAlarm() {
fmt.Printf("SMS - Module: %s gửi cảnh báo thành công\n", s.ModuleName)
}
type WechatAlarms struct {
ModuleName string
Account string
}
func (w *WechatAlarms) InsertAlarm() {
fmt.Printf("WeChat - Module: %s ghi cảnh báo vào DB\n", w.ModuleName)
}
func (w *WechatAlarms) IsAlarm() bool {
fmt.Printf("WeChat - Module: %s kiểm tra trạng thái cảnh báo\n", w.ModuleName)
return true
}
func (w *WechatAlarms) SendAlarm() {
fmt.Printf("WeChat - Module: %s gửi cảnh báo thành công\n", w.ModuleName)
}
func main() {
var alertSystem Alerter
var choice string
fmt.Scanln(&choice)
switch choice {
case "sms":
alertSystem = &SmsAlarms{ModuleName: "nginx", PhoneNumber: 1234567890}
case "wechat":
alertSystem = &WechatAlarms{ModuleName: "nginx", Account: "test@qq.com"}
default:
fmt.Println("Không gửi cảnh báo")
return
}
alertSystem.InsertAlarm()
if alertSystem.IsAlarm() {
alertSystem.SendAlarm()
}
}
Tác dụng của Interface
- Interface tạo ra một hợp đồng (contract) mà các kiểu dữ liệu phải tuân theo
- Giảm thiểu code trùng lặp, dễ mở rộng hệ thống
- Khi thêm kiểu cảnh báo mới, chỉ cần implement các phương thức của interface
Lồng Interface
package main
import "fmt"
type Eater interface {
Eat(food string)
}
type Walker interface {
Walk(place string)
}
type Animaler interface {
Eater
Walker
}
type Cat struct {
Name string
}
func (c *Cat) Eat(food string) {
fmt.Printf("Mèo %s đang ăn %s\n", c.Name, food)
}
func (c *Cat) Walk(place string) {
fmt.Printf("Mèo %s đang đi ở %s\n", c.Name, place)
}
func main() {
var animal Animaler
animal = &Cat{Name: "Mi"}
animal.Eat("cá")
animal.Walk("vườn")
}
Empty Interface (interface{})
Interface rỗng không có phương thức nào, do đó mọi kiểu dữ liệu đều implement nó. Nó cho phép lưu trữ giá trị của bất kỳ kiểu nào.
package main
import "fmt"
func main() {
var data interface{}
data = "Hello"
fmt.Printf("Kiểu: %T, Giá trị: %v\n", data, data)
data = 42
fmt.Printf("Kiểu: %T, Giá trị: %v\n", data, data)
}
Ứng dụng của Empty Interface
- Làm tham số hàm: cho phép hàm nhận mọi kiểu dữ liệu
- Làm kiểu phần tử trong slice hoặc map: tăng tính linh hoạt
// Hàm nhận mọi kiểu
func printAny(x interface{}) {
fmt.Printf("Kiểu: %T, Giá trị: %v\n", x, x)
}
// Slice với các phần tử kiểu khác nhau
items := []interface{}{10, "text", []int{1, 2, 3}}
// Map với giá trị kiểu khác nhau
info := map[string]interface{}{
"age": 25,
"city": "Hanoi",
"hobby": []string{"reading", "travel"},
}
Type Assertion (Xác nhận kiểu)
Để lấy lại giá trị thực từ empty interface, ta dùng cú pháp x.(T):
package main
import "fmt"
func main() {
var value interface{}
value = "Hello World"
str, ok := value.(string)
if ok {
fmt.Printf("Chuỗi: %v\n", str)
} else {
fmt.Println("Không phải chuỗi")
}
}