OOP: Kế thừa trong golang

Post on: 2023-04-14 22:18:14 | in: Golang
Kế thừa là một khái niệm quan trọng trong OOP, cho phép ta tạo ra các lớp mới bằng cách sử dụng các thuộc tính và phương thức đã có của lớp cha.

Chúng ta sẽ cố gắng giải thích kế thừa trong GO bằng cách so sánh với kế thừa trong JAVA. Điều đầu tiên chúng ta muốn đề cập ở đây là GOLANG không có các từ khóa như "Extends" và "Implements" như trong JAVA. Go cung cấp các chức năng giới hạn của từ khóa "Extends" và "Implements" một cách khác, mỗi từ khóa có những giới hạn riêng của nó. Trước khi chúng ta tiếp tục để hiểu về kế thừa trong GO, có một số điểm cần đề cập.

  • Go ưa thích sự phối hợp hơn là kế thừa. Nó cho phép nhúng struct vào trong struct khác.
  • Go không hỗ trợ kế thừa kiểu.

Chúng ta sẽ bắt đầu với một ví dụ đơn giản nhất về kế thừa trong GO. Sau đó, chúng ta sẽ liệt kê các giới hạn hoặc tính năng bị thiếu. Trong các lần lặp tiếp theo, chúng ta sẽ sửa chữa giới hạn hoặc tiếp tục thêm các tính năng bị thiếu cho đến khi chúng ta đã viết một chương trình hiển thị tất cả các thuộc tính của kế thừa có thể/không thể thực hiện trong Go. Vì vậy, hãy bắt đầu.

Use case rất cơ bản của kế thừa là loại con phải có thể truy cập vào dữ liệu và phương thức chung của loại cha. Điều này được thực hiện trong GO thông qua việc nhúng. Struct cơ bản được nhúng trong struct con và dữ liệu và phương thức của cơ bản có thể được truy cập trực tiếp bởi struct con. Xem đoạn mã bên dưới: struct con có thể truy cập trực tiếp vào dữ liệu "color" và gọi phương thức "say()" trực tiếp.

Chương trình 1

package main
import "fmt"
type base struct {
    color string
}
func (b *base) say() {
    fmt.Println("Hi from say function")
}
type child struct {
    base  //embedding
    style string
}
func main() {
    base := base{color: "Red"}
    child := &child{
        base:  base,
        style: "somestyle",
    }
    child.say()
    fmt.Println("The color is " + child.color)
}

Output:

Hi from say function
The color is Red

Một trong các giới hạn của chương trình trên là bạn không thể truyền loại con cho một hàm mong đợi loại cơ bản vì GO không cho phép kế thừa kiểu. Ví dụ, đoạn mã bên dưới không biên dịch được và cho lỗi - "không thể sử dụng child (kiểu *child) như là kiểu base trong đối số của hàm check":

Chương trình 2

package main
import "fmt"
type base struct {
    color string
}
func (b *base) say() {
    fmt.Println("Hi from say function")
}
type child struct {
    base  //embedding
    style string
}
func check(b base) {
    b.say()
}
func main() {
    base := base{color: "Red"}
    child := &child{
        base:  base,
        style: "somestyle",
    }
    child.say()
    fmt.Println("The color is " + child.color)
    check(child)
}

Output:

cannot use child (type *child) as type base in argument to check

Lỗi trên cho biết rằng việc phân loại con không thể thực hiện được trong GO chỉ bằng cách sử dụng tính nhúng. Hãy cố gắng sửa lỗi này. Đây là nơi mà giao diện GO xuất hiện. Xem phiên bản chương trình dưới đây, ngoài các tính năng đã nêu ở trên, còn sửa lỗi phân loại con:

Chương trình 3

package main
import "fmt"
type iBase interface {
    say()
}
type base struct {
    color string
}
func (b *base) say() {
    fmt.Println("Hi from say function")
}
type child struct {
    base  //embedding
    style string
}
func check(b iBase) {
    b.say()
}
func main() {
    base := base{color: "Red"}
    child := &child{
        base:  base,
        style: "somestyle",
    }
    child.say()
    fmt.Println("The color is " + child.color)
    check(child)
}

Output:

Hi from say function
The color is Red
Hi from say function

Trong chương trình trên, chúng ta đã: (a) Tạo một giao diện "iBase" có phương thức "say" (b) Chúng ta đã thay đổi phương thức "check" để chấp nhận đối số có kiểu iBase.

Vì cấu trúc base thực hiện phương thức "say" và đồng thời, cấu trúc child nhúng base. Vì vậy, phương thức của child gián tiếp thực hiện phương thức "say" và trở thành một kiểu của "iBase" và đó là lý do tại sao chúng ta có thể truyền child vào hàm check bây giờ. Tuyệt vời là chúng ta đã sửa một giới hạn bằng cách sử dụng một kết hợp của struct và interface.
Nhưng vẫn còn một giới hạn nữa. Hãy xem xét trường hợp child và base đều có một hàm "clear". Khi phương thức "say" gọi phương thức "clear", khi phương thức "say" được gọi bằng cấu trúc child, nó sẽ gọi phương thức "clear" của base và không phải phương thức "clear" của child. Xem ví dụ bên dưới:

Chương trình 4

package main
import "fmt"
type iBase interface {
    say()
}
type base struct {
    color string
}
func (b *base) say() {
    b.clear()
}
func (b *base) clear() {
    fmt.Println("Clear from base's function")
}
type child struct {
    base  //embedding
    style string
}
func (b *child) clear() {
    fmt.Println("Clear from child's function")
}
func check(b iBase) {
    b.say()
}
func main() {
    base := base{color: "Red"}
    child := &child{
        base:  base,
        style: "somestyle",
    }
    child.say()
}

Output:

Clear from base's function

Như bạn có thể thấy ở trên, phương thức "clear" của base được gọi thay vì phương thức "clear" của child. Điều này khác với Java, nơi phương thức "clear" của "child" sẽ được gọi.

Một cách để khắc phục vấn đề trên là làm cho "clear" là một thuộc tính có kiểu là một hàm trong cấu trúc base. Điều này có thể thực hiện được trong GO vì các hàm được coi là biến first-class trong GO. Xem giải pháp dưới đây:

Chương trình 5

package main
import "fmt"
type iBase interface {
    say()
}
type base struct {
    color string
    clear func()
}
func (b *base) say() {
    b.clear()
}
type child struct {
    base  //embedding
    style string
}
func check(b iBase) {
    b.say()
}
func main() {
    base := base{color: "Red",
        clear: func() {
            fmt.Println("Clear from child's function")
        }}
    child := &child{
        base:  base,
        style: "somestyle",
    }
    child.say()
}

Output:

Clear from child's function

Hãy thử thêm một tính năng khác vào chương trình trên đó là -

  • Kế thừa đa năng - cấu trúc con phải có thể truy cập nhiều thuộc tính và phương thức từ hai cấu trúc base và cũng phải cho phép phân loại con. Đây là mã của chương trình:

Chương trình 6

package main
import "fmt"
type iBase1 interface {
    say()
}
type iBase2 interface {
    walk()
}
type base1 struct {
    color string
}
func (b *base1) say() {
    fmt.Println("Hi from say function")
}
type base2 struct {
}
func (b *base1) walk() {
    fmt.Println("Hi from walk function")
}
type child struct {
    base1 //embedding
    base2 //embedding
    style string
}
func (b *child) clear() {
    fmt.Println("Clear from child's function")
}
func check1(b iBase1) {
    b.say()
}
func check2(b iBase2) {
    b.walk()
}
func main() {
    base1 := base1{color: "Red"}
    base2 := base2{}
    child := &child{
        base1: base1,
        base2: base2,
        style: "somestyle",
    }
    child.say()
    child.walk()
    check1(child)
    check2(child)
}

Output:

Hi from say function
Hi from walk function
Hi from say function
Hi from walk function

Trong chương trình trên, child nhúng cả base1 và base2. Nó cũng có thể được truyền dưới dạng một phiên bản của giao diện iBase1 và iBase2 tương ứng đến hàm check1 và check2. Đây là cách chúng ta đạt được kế thừa đa năng.

Bây giờ, một câu hỏi lớn là làm thế nào chúng ta triển khai "Thừa kế kiểu" trong GO. Như đã đề cập, kế thừa kiểu không được phép trong GO và do đó nó không có bảng kế thừa kiểu.GO có ý định không cho phép tính năng này để bất kỳ thay đổi nào trong hành vi của một giao diện chỉ được lan truyền đến các cấu trúc trực tiếp của nó mà định nghĩa tất cả các phương thức của giao diện.

Mặc dù chúng ta có thể triển khai bảng kế thừa kiểu sử dụng các giao diện và cấu trúc như sau:

Chương trình 7

package main
import "fmt"
type iAnimal interface {
    breathe()
}
type animal struct {
}
func (a *animal) breathe() {
    fmt.Println("Animal breate")
}
type iAquatic interface {
    iAnimal
    swim()
}
type aquatic struct {
    animal
}
func (a *aquatic) swim() {
    fmt.Println("Aquatic swim")
}
type iNonAquatic interface {
    iAnimal
    walk()
}
type nonAquatic struct {
    animal
}
func (a *nonAquatic) walk() {
    fmt.Println("Non-Aquatic walk")
}
type shark struct {
    aquatic
}
type lion struct {
    nonAquatic
}
func main() {
    shark := &shark{}
    checkAquatic(shark)
    checkAnimal(shark)
    lion := &lion{}
    checkNonAquatic(lion)
    checkAnimal(lion)
}
func checkAquatic(a iAquatic) {}
func checkNonAquatic(a iNonAquatic) {}
func checkAnimal(a iAnimal) {}

Trong chương trình trên, chúng ta đã tạo được một hierarchy (cấu trúc phân cấp) (xem bên dưới). Đây là cách tiêu chuẩn của Go để tạo ra các cấu trúc phân cấp bằng cách sử dụng embedding trên cả cấp độ struct và cấp độ interface. Lưu ý rằng nếu bạn muốn tạo ra sự phân biệt trong cấu trúc phân cấp của mình, ví dụ như một "cá mập" không nên đồng thời là "iAquatic" và "iNonAquatic", thì ít nhất phải có một phương thức trong tập hợp các phương thức của "iAquatic" và "iNonAquatic" không có trong phương thức của interface kia. Trong ví dụ của chúng ta, "swim" và "walk" là những phương thức đó.

iAnimal
--iAquatic
----shark
--iNonAquatic
----lion

Kết luận

Go không hỗ trợ kế thừa kiểu dữ liệu nhưng điều tương tự có thể đạt được bằng cách sử dụng kết hợp nhúng, tuy nhiên, người lập trình cần phải cẩn thận khi tạo kiểu thừa kế như thế. Ngoài ra, Go không cung cấp tính năng ghi đè phương thức.

Tag: go Golang nâng cao OOP