Đi đến các trường giao diện


105

Tôi quen thuộc với thực tế rằng, trong Go, các giao diện xác định chức năng, thay vì dữ liệu. Bạn đặt một tập hợp các phương thức vào một giao diện, nhưng bạn không thể chỉ định bất kỳ trường nào sẽ được yêu cầu trên bất kỳ thứ gì triển khai giao diện đó.

Ví dụ:

// Interface
type Giver interface {
    Give() int64
}

// One implementation
type FiveGiver struct {}

func (fg *FiveGiver) Give() int64 {
    return 5
}

// Another implementation
type VarGiver struct {
    number int64
}

func (vg *VarGiver) Give() int64 {
    return vg.number
}

Bây giờ chúng ta có thể sử dụng giao diện và các triển khai của nó:

// A function that uses the interface
func GetSomething(aGiver Giver) {
    fmt.Println("The Giver gives: ", aGiver.Give())
}

// Bring it all together
func main() {
    fg := &FiveGiver{}
    vg := &VarGiver{3}
    GetSomething(fg)
    GetSomething(vg)
}

/*
Resulting output:
5
3
*/

Bây giờ, những gì bạn không thể làm là một cái gì đó như thế này:

type Person interface {
    Name string
    Age int64
}

type Bob struct implements Person { // Not Go syntax!
    ...
}

func PrintName(aPerson Person) {
    fmt.Println("Person's name is: ", aPerson.Name)
}

func main() {
    b := &Bob{"Bob", 23}
    PrintName(b)
}

Tuy nhiên, sau khi thử nghiệm với các giao diện và cấu trúc nhúng, tôi đã phát hiện ra một cách để làm điều này, sau một thời gian:

type PersonProvider interface {
    GetPerson() *Person
}

type Person struct {
    Name string
    Age  int64
}

func (p *Person) GetPerson() *Person {
    return p
}

type Bob struct {
    FavoriteNumber int64
    Person
}

Do cấu trúc nhúng, Bob có mọi thứ mà Person có. Nó cũng triển khai giao diện PersonProvider, vì vậy chúng ta có thể chuyển Bob vào các hàm được thiết kế để sử dụng giao diện đó.

func DoBirthday(pp PersonProvider) {
    pers := pp.GetPerson()
    pers.Age += 1
}

func SayHi(pp PersonProvider) {
    fmt.Printf("Hello, %v!\r", pp.GetPerson().Name)
}

func main() {
    b := &Bob{
        5,
        Person{"Bob", 23},
    }
    DoBirthday(b)
    SayHi(b)
    fmt.Printf("You're %v years old now!", b.Age)
}

Đây là một sân chơi cờ vây minh họa đoạn mã trên.

Sử dụng phương pháp này, tôi có thể tạo một giao diện xác định dữ liệu hơn là hành vi và có thể được thực hiện bởi bất kỳ cấu trúc nào chỉ bằng cách nhúng dữ liệu đó. Bạn có thể xác định các hàm tương tác rõ ràng với dữ liệu nhúng đó và không biết về bản chất của cấu trúc bên ngoài. Và mọi thứ được kiểm tra tại thời điểm biên dịch! (Cách duy nhất bạn có thể làm rối tung, mà tôi có thể thấy, là nhúng giao diện PersonProvidervào Bob, thay vì một giao diện cụ thể Person. Nó sẽ biên dịch và thất bại trong thời gian chạy.)

Bây giờ, đây là câu hỏi của tôi: đây là một mẹo nhỏ hay tôi nên làm theo cách khác?


3
"Tôi có thể tạo giao diện xác định dữ liệu hơn là hành vi". Tôi cho rằng bạn có một hành vi trả về dữ liệu.
jmaloney

Tôi sẽ viết một câu trả lời; Tôi nghĩ rằng nó ổn nếu bạn cần nó và biết hậu quả, nhưng có những hậu quả và tôi sẽ không làm điều đó mọi lúc.
twotwotwo

@jmaloney Tôi nghĩ bạn đúng, nếu bạn muốn xem xét nó một cách rõ ràng. Nhưng nhìn chung, với các phần khác nhau mà tôi đã trình bày, ngữ nghĩa trở thành "hàm này chấp nhận bất kỳ cấu trúc nào có ___ trong thành phần của nó". Ít nhất, đó là những gì tôi dự định.
Matt Mc

1
Đây không phải là tài liệu "câu trả lời". Tôi nhận được câu hỏi của bạn bằng googling "interface as struct property golang". Tôi đã tìm thấy một cách tiếp cận tương tự bằng cách đặt một cấu trúc triển khai một giao diện làm thuộc tính của một cấu trúc khác. Đây là sân chơi, play.golang.org/p/KLzREXk9xo Cảm ơn bạn đã cho tôi một số ý tưởng.
Dale

1
Nhìn lại, và sau 5 năm sử dụng cờ vây, tôi thấy rõ rằng những điều trên không phải là cờ vây thành ngữ. Đó là một căng thẳng đối với thuốc chung. Nếu bạn cảm thấy muốn làm điều này, tôi khuyên bạn nên suy nghĩ lại về kiến ​​trúc của hệ thống của bạn. Chấp nhận giao diện và trả về cấu trúc, chia sẻ bằng cách giao tiếp và vui mừng.
Matt Mc

Câu trả lời:


55

Đó chắc chắn là một thủ thuật gọn gàng. Tuy nhiên, con trỏ hiển thị vẫn cung cấp quyền truy cập trực tiếp vào dữ liệu, vì vậy, nó chỉ mang lại cho bạn sự linh hoạt bổ sung hạn chế cho những thay đổi trong tương lai. Ngoài ra, quy ước Go không yêu cầu bạn phải luôn đặt một phần trừu tượng trước các thuộc tính dữ liệu của mình .

Kết hợp những điều đó lại với nhau, tôi sẽ có xu hướng cực đoan này hay cực đoan khác cho một trường hợp sử dụng nhất định: a) chỉ cần tạo thuộc tính công khai (sử dụng tính năng nhúng nếu có) và chuyển các loại cụ thể xung quanh hoặc b) nếu có vẻ như lộ dữ liệu gây rắc rối về sau, hãy để lộ một getter / setter để có sự trừu tượng mạnh mẽ hơn.

Bạn sẽ cân nhắc điều này trên cơ sở từng thuộc tính. Ví dụ: nếu một số dữ liệu dành riêng cho việc triển khai hoặc bạn muốn thay đổi các đại diện vì một số lý do khác, bạn có thể không muốn hiển thị trực tiếp thuộc tính, trong khi các thuộc tính dữ liệu khác có thể đủ ổn định để công khai chúng là một chiến thắng thực sự.


Việc ẩn các thuộc tính đằng sau getters và setters giúp bạn linh hoạt hơn để thực hiện các thay đổi tương thích ngược sau này. Giả sử một ngày nào đó bạn muốn thay đổi Personđể lưu trữ không chỉ một trường "tên" mà còn là tiền tố đầu tiên / giữa / cuối cùng /; nếu bạn có các phương pháp Name() stringSetName(string), bạn có thể giữ cho người dùng hiện tại của Persongiao diện hài lòng trong khi thêm các phương pháp chi tiết mới. Hoặc bạn có thể muốn đánh dấu một đối tượng được cơ sở dữ liệu sao lưu là "bẩn" khi nó có các thay đổi chưa được lưu; bạn có thể làm điều đó khi tất cả các cập nhật dữ liệu đều thông qua SetFoo()các phương thức.

Vì vậy: với getters / setters, bạn có thể thay đổi các trường cấu trúc trong khi duy trì một API tương thích và thêm logic xung quanh thuộc tính get / set vì không ai có thể thực hiện p.Name = "bob"mà không cần xem qua mã của bạn.

Tính linh hoạt đó phù hợp hơn khi kiểu phức tạp (và cơ sở mã lớn). Nếu bạn có PersonCollection, nó có thể được hỗ trợ nội bộ bởi một sql.Rows, a []*Person, một []uintID cơ sở dữ liệu hoặc bất cứ thứ gì. Sử dụng giao diện phù hợp, bạn có thể không để người gọi quan tâm đó là giao diện nào, cách io.Readerlàm cho các kết nối mạng và tệp trông giống nhau.

Một điều cụ thể: interfaces trong Go có thuộc tính đặc biệt mà bạn có thể triển khai mà không cần nhập gói định nghĩa nó; điều đó có thể giúp bạn tránh nhập khẩu theo chu kỳ . Nếu giao diện của bạn trả về a *Person, thay vì chỉ các chuỗi hoặc bất cứ thứ gì, tất cả PersonProvidersđều phải nhập gói ở nơi Personđược xác định. Điều đó có thể ổn hoặc thậm chí không thể tránh khỏi; nó chỉ là một hệ quả để biết về.


Nhưng một lần nữa, cộng đồng cờ vây không có quy ước chặt chẽ về việc tiết lộ các thành viên dữ liệu trong API công khai thuộc loại của bạn . Việc sử dụng quyền truy cập công khai vào một thuộc tính như một phần của API của bạn trong một trường hợp cụ thể là hợp lý, thay vì ngăn cản bất kỳ sự tiếp xúc nào vì nó có thể làm phức tạp hoặc ngăn cản việc triển khai thay đổi sau này.

Vì vậy, ví dụ, stdlib làm những việc như cho phép bạn khởi tạo một http.Servervới cấu hình của bạn và hứa rằng số 0 bytes.Bufferlà có thể sử dụng được. Bạn có thể tự làm những thứ như thế của riêng mình và thực sự, tôi không nghĩ rằng bạn nên trừu tượng hóa mọi thứ trước nếu phiên bản cụ thể hơn, hiển thị dữ liệu có vẻ hoạt động. Nó chỉ là nhận thức về sự đánh đổi.


Một điều bổ sung: cách tiếp cận nhúng giống với kế thừa hơn một chút, phải không? Bạn nhận được bất kỳ trường và phương thức nào mà cấu trúc nhúng có và bạn có thể sử dụng giao diện của nó để mọi cấu trúc thượng tầng sẽ đủ điều kiện mà không cần triển khai lại các tập hợp giao diện.
Matt Mc

Vâng - rất giống thừa kế ảo trong các ngôn ngữ khác. Bạn có thể sử dụng tính năng nhúng để triển khai một giao diện cho dù nó được định nghĩa dưới dạng getters và setters hay một con trỏ tới dữ liệu (hoặc tùy chọn thứ ba để truy cập chỉ đọc vào các cấu trúc nhỏ, một bản sao của cấu trúc).
twotwotwo

Tôi phải nói rằng, điều này khiến tôi hồi tưởng về năm 1999 và học cách viết hàng loạt các bộ định tuyến và bộ định tuyến soạn sẵn trong Java.
Tom

Thật tệ là không phải lúc nào thư viện tiêu chuẩn của Go cũng làm được điều này. Tôi đang cố gắng bắt chước một số cuộc gọi tới os.Process cho các bài kiểm tra đơn vị. Tôi không thể chỉ bọc đối tượng tiến trình trong một giao diện vì biến thành viên Pid được truy cập trực tiếp và giao diện Go không hỗ trợ biến thành viên.
Alex Jansen

1
@Tom Đúng là như vậy. Tôi nghĩ rằng getter / setters bổ sung thêm tính linh hoạt hơn là để lộ một con trỏ, nhưng tôi cũng không nghĩ rằng mọi người nên getter / setter-ify mọi thứ (hoặc phù hợp với phong cách cờ vây điển hình). Trước đây tôi đã có một vài từ để chỉ ra điều đó, nhưng đã sửa lại phần đầu và phần cuối để nhấn mạnh nó nhiều hơn.
twotwotwo

2

Nếu tôi hiểu chính xác, bạn muốn điền một trường cấu trúc vào một trường khác. Ý kiến ​​của tôi không sử dụng giao diện để mở rộng. Bạn có thể dễ dàng làm điều đó bằng cách tiếp cận tiếp theo.

package main

import (
    "fmt"
)

type Person struct {
    Name        string
    Age         int
    Citizenship string
}

type Bob struct {
    SSN string
    Person
}

func main() {
    bob := &Bob{}

    bob.Name = "Bob"
    bob.Age = 15
    bob.Citizenship = "US"

    bob.SSN = "BobSecret"

    fmt.Printf("%+v", bob)
}

https://play.golang.org/p/aBJ5fq3uXtt

Lưu ý Persontrong Bobkhai báo. Điều này sẽ làm cho trường struct được bao gồm có sẵn trong Bobcấu trúc trực tiếp với một số đường cú pháp.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.