ไปที่ฟิลด์อินเทอร์เฟซ


105

ฉันคุ้นเคยกับความจริงที่ว่าใน Go อินเทอร์เฟซกำหนดฟังก์ชันการทำงานแทนที่จะเป็นข้อมูล คุณใส่ชุดวิธีการลงในอินเทอร์เฟซ แต่คุณไม่สามารถระบุฟิลด์ใด ๆ ที่จำเป็นสำหรับสิ่งที่ใช้อินเทอร์เฟซนั้น

ตัวอย่างเช่น:

// 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
}

ตอนนี้เราสามารถใช้อินเทอร์เฟซและการใช้งานได้:

// 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
*/

ตอนนี้สิ่งที่คุณทำไม่ได้มีดังนี้:

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)
}

อย่างไรก็ตามหลังจากเล่นกับอินเทอร์เฟซและโครงสร้างแบบฝังฉันได้ค้นพบวิธีที่จะทำสิ่งนี้หลังจากแฟชั่น:

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
}

เนื่องจากโครงสร้างที่ฝังไว้ Bob จึงมีทุกสิ่งที่บุคคลมี นอกจากนี้ยังใช้อินเทอร์เฟซ PersonProvider ดังนั้นเราจึงสามารถส่ง Bob ไปสู่ฟังก์ชันที่ออกแบบมาเพื่อใช้อินเทอร์เฟซนั้นได้

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)
}

นี่คือ Go Playgroundที่แสดงรหัสด้านบน

ด้วยวิธีนี้ฉันสามารถสร้างอินเทอร์เฟซที่กำหนดข้อมูลมากกว่าพฤติกรรมและสามารถใช้งานได้โดยโครงสร้างใด ๆ เพียงแค่ฝังข้อมูลนั้น คุณสามารถกำหนดฟังก์ชันที่โต้ตอบกับข้อมูลที่ฝังไว้อย่างชัดเจนและไม่ทราบถึงลักษณะของโครงสร้างภายนอก และทุกอย่างจะถูกตรวจสอบในเวลารวบรวม! (วิธีเดียวที่คุณสามารถเลอะที่ฉันสามารถดูจะได้รับการฝังอินเตอร์เฟซPersonProviderในBobมากกว่าคอนกรีตPerson. มันจะรวบรวมและล้มเหลวที่รันไทม์.)

ต่อไปนี้เป็นคำถามของฉัน: นี่เป็นเคล็ดลับที่เรียบร้อยหรือฉันควรทำอย่างอื่นดี?


4
"ฉันสามารถสร้างอินเทอร์เฟซที่กำหนดข้อมูลมากกว่าพฤติกรรม" ฉันขอยืนยันว่าคุณมีพฤติกรรมที่ส่งคืนข้อมูล
jmaloney

ฉันจะเขียนคำตอบ ฉันคิดว่ามันก็ดีถ้าคุณต้องการมันและรู้ถึงผลที่ตามมา แต่มันก็มีผลตามมาและฉันจะไม่ทำมันตลอดเวลา
twotwotwo

@jmaloney ฉันคิดว่าคุณพูดถูกถ้าคุณต้องการที่จะมองมันอย่างชัดเจน แต่โดยรวมกับชิ้นส่วนต่างๆที่ฉันได้แสดงความหมายกลายเป็น "ฟังก์ชันนี้ยอมรับโครงสร้างใด ๆ ที่มี ___ ในองค์ประกอบ" อย่างน้อยนั่นคือสิ่งที่ฉันตั้งใจไว้
Matt Mc

1
นี่ไม่ใช่เนื้อหา "คำตอบ" ฉันตอบคำถามของคุณโดย googling "interface as struct property golang" ฉันพบวิธีการที่คล้ายกันโดยการตั้งค่าโครงสร้างที่ใช้อินเทอร์เฟซเป็นคุณสมบัติของโครงสร้างอื่น นี่คือสนามเด็กเล่นplay.golang.org/p/KLzREXk9xoขอบคุณที่ให้ข้อคิดเห็น
Dale

1
เมื่อมองย้อนกลับไปและหลังจาก 5 ปีของการใช้ Go ฉันเห็นได้ชัดว่าสิ่งที่กล่าวมาข้างต้นไม่ใช่สำนวน Go มันเป็นเรื่องยากสำหรับยาชื่อสามัญ หากคุณรู้สึกอยากทำสิ่งนี้ฉันขอแนะนำให้คุณคิดทบทวนสถาปัตยกรรมของระบบของคุณเสียใหม่ ยอมรับอินเทอร์เฟซและส่งคืนโครงสร้างแบ่งปันโดยการสื่อสารและชื่นชมยินดี
Matt Mc

คำตอบ:


55

มันเป็นเคล็ดลับที่ประณีตแน่นอน อย่างไรก็ตามการเปิดเผยตัวชี้ยังคงทำให้สามารถเข้าถึงข้อมูลได้โดยตรงดังนั้นจึงซื้อเฉพาะความยืดหยุ่นเพิ่มเติมที่ จำกัด ไว้สำหรับการเปลี่ยนแปลงในอนาคต นอกจากนี้การประชุม Go ไม่ได้กำหนดให้คุณต้องใส่สิ่งที่เป็นนามธรรมไว้หน้าแอตทริบิวต์ข้อมูลของคุณเสมอไป

เมื่อนำสิ่งเหล่านั้นมารวมกันฉันมักจะไปทางใดทางหนึ่งสำหรับกรณีการใช้งานที่กำหนด: a) เพียงสร้างแอตทริบิวต์สาธารณะ (โดยใช้การฝังถ้ามี) และส่งผ่านประเภทคอนกรีตรอบ ๆ หรือ b) หากดูเหมือนว่าการเปิดเผยข้อมูลจะ ทำให้เกิดปัญหาในภายหลังเปิดเผยผู้เริ่มต้น / ผู้ตั้งค่าสำหรับนามธรรมที่แข็งแกร่งยิ่งขึ้น

คุณจะชั่งน้ำหนักสิ่งนี้ตามเกณฑ์ต่อแอตทริบิวต์ ตัวอย่างเช่นหากข้อมูลบางส่วนเป็นข้อมูลเฉพาะการนำไปใช้งานหรือคุณคาดว่าจะเปลี่ยนการนำเสนอด้วยเหตุผลอื่นคุณอาจไม่ต้องการเปิดเผยแอตทริบิวต์โดยตรงในขณะที่แอตทริบิวต์ข้อมูลอื่น ๆ อาจมีความเสถียรเพียงพอที่จะทำให้เป็นแบบสาธารณะถือเป็นการชนะสุทธิ


การซ่อนคุณสมบัติไว้เบื้องหลัง getters และ setters ช่วยให้คุณมีความยืดหยุ่นมากขึ้นในการเปลี่ยนแปลงที่เข้ากันได้ในภายหลัง สมมติว่าสักวันคุณต้องการเปลี่ยนPersonการจัดเก็บไม่ใช่แค่ช่อง "ชื่อ" เดียว แต่เป็นช่องแรก / กลาง / สุดท้าย / คำนำหน้า หากคุณมีวิธีการName() stringและSetName(string)คุณสามารถทำให้ผู้ใช้Personอินเทอร์เฟซที่มีอยู่มีความสุขในขณะที่เพิ่มวิธีการใหม่ ๆ ที่ละเอียดกว่า หรือคุณอาจต้องการทำเครื่องหมายวัตถุที่สำรองฐานข้อมูลว่า "สกปรก" เมื่อมีการเปลี่ยนแปลงที่ยังไม่ได้บันทึก คุณสามารถทำได้เมื่อการอัปเดตข้อมูลทั้งหมดต้องใช้SetFoo()วิธีการ

ดังนั้น: ด้วย getters / setters คุณสามารถเปลี่ยนฟิลด์ struct ในขณะที่รักษา API ที่เข้ากันได้และเพิ่มตรรกะรอบ ๆ คุณสมบัติ get / set เนื่องจากไม่มีใครสามารถทำได้p.Name = "bob"โดยไม่ต้องผ่านโค้ดของคุณ

ความยืดหยุ่นนั้นมีความเกี่ยวข้องมากขึ้นเมื่อประเภทมีความซับซ้อน (และ codebase มีขนาดใหญ่) หากคุณมีPersonCollectionมันอาจจะได้รับการสนับสนุนภายในโดยsql.Rowsการ[]*Personเป็น[]uintรหัสฐานข้อมูลหรืออะไรก็ตาม ด้วยการใช้อินเทอร์เฟซที่เหมาะสมคุณสามารถบันทึกผู้โทรจากการดูแลซึ่งเป็นวิธีที่io.Readerทำให้การเชื่อมต่อเครือข่ายและไฟล์ดูเหมือนกัน

สิ่งหนึ่งที่เฉพาะเจาะจง: interfaceใน Go มีคุณสมบัติพิเศษที่คุณสามารถใช้งานได้โดยไม่ต้องนำเข้าแพ็คเกจที่กำหนด ที่สามารถช่วยให้คุณหลีกเลี่ยงการนำเข้าวงจร หากอินเทอร์เฟซของคุณส่งคืน a *Personแทนที่จะเป็นเพียงสตริงหรืออะไรก็ตามทั้งหมดPersonProvidersจะต้องนำเข้าแพ็กเกจที่Personกำหนดไว้ นั่นอาจจะดีหรือหลีกเลี่ยงไม่ได้ เป็นเพียงผลที่ตามมาที่ควรทราบ


แต่อีกครั้งชุมชน Go ไม่มีหลักการที่ชัดเจนในการเปิดเผยสมาชิกข้อมูลใน API สาธารณะประเภทของคุณ คุณต้องใช้วิจารณญาณว่าเหมาะสมหรือไม่ที่จะใช้การเข้าถึงแบบสาธารณะสำหรับแอตทริบิวต์เป็นส่วนหนึ่งของ API ของคุณในบางกรณีแทนที่จะกีดกันการเปิดเผยใด ๆเพราะอาจทำให้ซับซ้อนหรือป้องกันไม่ให้มีการเปลี่ยนแปลงการนำไปใช้ในภายหลัง

ตัวอย่างเช่น stdlib ทำสิ่งต่างๆเช่นให้คุณเริ่มต้นhttp.Serverด้วย config ของคุณและสัญญาว่าbytes.Bufferจะใช้งานศูนย์ได้ เป็นเรื่องปกติที่จะทำสิ่งต่างๆของคุณเองเช่นนั้นและฉันไม่คิดว่าคุณควรแยกสิ่งต่าง ๆ ออกไปล่วงหน้าหากเวอร์ชันที่เปิดเผยข้อมูลที่เป็นรูปธรรมมากขึ้นดูเหมือนจะใช้งานได้ เป็นเพียงการตระหนักถึงการแลกเปลี่ยน


อีกอย่างหนึ่ง: วิธีการฝังก็เหมือนกับการถ่ายทอดทางพันธุกรรมใช่ไหม? คุณได้รับฟิลด์และวิธีการใด ๆ ที่โครงสร้างที่ฝังไว้มีและคุณสามารถใช้อินเทอร์เฟซเพื่อให้โครงสร้างพิเศษใด ๆ มีคุณสมบัติโดยไม่ต้องนำชุดอินเทอร์เฟซมาใช้ใหม่
Matt Mc

ใช่ - เหมือนกับการสืบทอดเสมือนในภาษาอื่น ๆ คุณสามารถใช้การฝังเพื่อใช้อินเทอร์เฟซไม่ว่าจะกำหนดในรูปแบบของ getters และ setters หรือตัวชี้ไปยังข้อมูล (หรือตัวเลือกที่สามสำหรับการเข้าถึงโครงสร้างขนาดเล็กแบบอ่านอย่างเดียวสำเนาของโครงสร้าง)
twotwotwo

ฉันต้องบอกว่านี่ทำให้ฉันย้อนไปถึงปี 1999 และเรียนรู้ที่จะเขียนรีมของตัวรับและตัวตั้งค่าสำเร็จรูปใน Java
ทอม

มันแย่เกินไปห้องสมุดมาตรฐานของ Go เองก็ไม่ได้ทำเช่นนี้เสมอไป ฉันกำลังพยายามล้อเลียนการโทรไปยังระบบปฏิบัติการขั้นตอนสำหรับการทดสอบหน่วย ฉันไม่สามารถรวมออบเจ็กต์กระบวนการในอินเทอร์เฟซได้เนื่องจากตัวแปรสมาชิก Pid ถูกเข้าถึงโดยตรงและอินเทอร์เฟซ Go ไม่รองรับตัวแปรสมาชิก
Alex Jansen

1
@ ทอมนั่นคือเรื่องจริง ฉันคิดว่า getters / setters เพิ่มความยืดหยุ่นมากกว่าการเปิดเผยตัวชี้ แต่ฉันก็ไม่คิดว่าทุกคนควรจะเข้าใจ / setter-ify ทุกอย่าง (หรือจะตรงกับสไตล์ Go ทั่วไป) ก่อนหน้านี้ฉันมีคำพูดไม่กี่คำที่แสดงท่าทางนั้น แต่แก้ไขจุดเริ่มต้นและจุดสิ้นสุดเพื่อเน้นให้มากขึ้น
twotwotwo

2

ถ้าฉันเข้าใจถูกต้องว่าคุณต้องการเติมฟิลด์โครงสร้างหนึ่งในอีกฟิลด์หนึ่ง ความคิดเห็นของฉันที่จะไม่ใช้อินเทอร์เฟซเพื่อขยาย คุณสามารถทำได้ง่ายๆโดยแนวทางถัดไป

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

หมายเหตุPersonในBobการประกาศ สิ่งนี้จะทำให้ฟิลด์Bobโครงสร้างที่รวมอยู่พร้อมใช้งานในโครงสร้างโดยตรงกับน้ำตาลวากยสัมพันธ์

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.