聊聊在Go語言裏使用繼承的翻車經歷

Go不是面向對象的語言,可是使用組合、嵌套和接口能夠支持代碼的複用和多態。關於結構體嵌套:外層結構體類型經過匿名嵌套一個已命名的結構體類型後就能夠得到匿名成員類型的全部導出成員,並且也得到了該類型導出的所有的方法。好比下面這個例子:golang

type ShapeInterface interface {
    GetName() string
}

type Shape struct {
    name string
}

func (s *Shape) GetName() string {
    return s.name
}

type Rectangle struct {
    Shape
    w, h float64
}

Shape類型上定義了GetName()方法,而在矩形Rectangle的定義中匿名嵌套了Shape類型從而得到了成員name和成員方法GetName(),同時RectangleShape同樣又都是ShapeInterface接口的實現。編程

我一開始覺得這和麪向對象的繼承沒有什麼區別,把內部結構體當作是父類,經過嵌套一下結構體就能得到父類的方法,並且還能根據須要重寫父類的方法,在實際項目編程中我也是這麼用的。直到有一天......c#

因爲咱們這不少推廣類促銷類的需求不少,幾乎每個月兩三次,每季度還有大型推廣活動。產品經理也是絞盡腦汁想各類玩法來提升用戶活躍和訂單量。每次都是前面玩法不同,但最後都是參與任務得積分啦、分享後抽獎啦。因而乎我就肩負起了設計通用化流程的任務。根據每次需求通用的部分設計了接口和基礎的實現類型,同時預留了給子類實現的方法,應對每次不同的前置條件,這不就是面向對象裏常常乾的事兒嘛。單元測試

爲了好理解咱們仍是用上面那個ShapeInterface舉例子。測試

type ShapeInterface interface {
    Area() float64
    GetName() string
    PrintArea()
}

// 標準形狀,它的面積爲0.0
type Shape struct {
    name string
}

func (s *Shape) Area() float64 {
    return 0.0
}

func (s *Shape) GetName() string {
    return s.name
}

func (s *Shape) PrintArea() {
    fmt.Printf("%s : Area %v\r\n", s.name, s.Area())
}

// 矩形 : 從新定義了Area方法
type Rectangle struct {
    Shape
    w, h float64
}

func (r *Rectangle) Area() float64 {
    return r.w * r.h
}

// 圓形  : 從新定義 Area 和PrintArea 方法
type Circle struct {
    Shape
    r float64
}

func (c *Circle) Area() float64 {
    return c.r * c.r * math.Pi
}

func (c *Circle) PrintArea() {
    fmt.Printf("%s : Area %v\r\n", c.GetName(), c.Area())
}

咱們在ShapeInterface裏增長了Area()PrintArea()方法,由於每種形狀計算面積的公式不同,基礎實現類型Shape裏的Area只是簡單返回了0.0,具體計算面積的任務交給組合Shape類型的Rectange類經過重寫Area()方法實現,Rectange經過組合得到了ShapePrintArea()方法就能打印出它本身的面積來。spa

到目前爲止,這些還都是個人設想,規劃完後本身感受特興奮,感受本身已經掌握了組合(Composition)這種思想的精髓...... 按這個思路我就把整套流程都寫完了,單元測試只測了每一個子功能,前置條件太複雜加上我還管團隊裏的其餘項目本身的時間不太富餘,因此就交付給組裏的夥伴們使用了讓他們順便幫我測試下整個流程,而後就現場翻車了......設計

咱們把上面那個例子運行一下,爲了能看出區別,又專門寫了一個Circle類型並用這個類型重寫了Area()PrintArea()指針

func main() {

    s := Shape{name: "Shape"}
    c := Circle{Shape: Shape{name: "Circle"}, r: 10}
    r := Rectangle{Shape: Shape{name: "Rectangle"}, w: 5, h: 4}

    listshape := []c{&s, &c, &r}

    for _, si := range listshape {
        si.PrintArea() //!! 猜猜哪一個Area()方法會被調用 !! 
    }

}

運行後的輸出結果以下:code

Shape : Area 0
Circle : Area 314.1592653589793
Rectangle : Area 0

看出問題來了不,Rectangle經過組合Shape得到的PrintArea()方法並無去調用Rectangle實現的Area()方法,而是去調用了ShapeArea()方法。Circle是由於本身重寫了PrintArea()因此在方法裏調用到了自身的Area()對象

在項目裏那個相似例子裏PrintArea()比這裏的複雜不少並且承載着標準化流程的職責,確定是不能每組合一次本身去實現一遍PrintArea()方法啊,那叫什麼設計,並且面子上也說不過去,對吧,好不容易炫一次技,可不能被打臉。

通過Google上一番搜索後找到了一些詳細的解釋,上面咱們期待的那種行爲叫作虛擬方法:指望PrintArea()會去調用重寫的Area()。可是在Go語言裏沒有繼承和虛擬方法,Shape.PrintArea()的定義是調用Shape.Area() Shape不知道它是否被嵌入哪一個結構中,所以它沒法將方法調用「分派」給虛擬的運行時方法。

Go語言規範:選擇器裏描述了計算x.f表達式(其中f多是方法)以選擇最後要調用的方法時遵循的確切規則。裏面的關鍵點闡述是

  • 選擇器f能夠表示類型T的字段或方法f,或者能夠引用T的嵌套匿名字段的字段或方法f。遍歷到達f的匿名字段的數量稱爲其在T中的深度。
  • 對於類型T或* T的值x(其中T不是指針或接口類型),x.f表示存在f的T中最淺深度的字段或方法。

回到咱們的例子中來就是:

對於Rectangle類型來講si.PrintArea()將調用Shape.PrintArea()由於沒有爲Rectangle類型定義PrintArea()方法(沒有接受者是*RectanglePrintArea()方法),而Shape.PrintArea()方法的實現調用的是Shape.Area()而不是Rectangle.Area()-如前面所討論的,Shape不知道Rectangle的存在。因此會看到輸出結果:

Rectangle : Area 0

那麼既然在Go裏不支持繼承,如何以組合解決相似的問題呢。咱們能夠經過定義參數爲ShapeInterface接口的方法定義PrintArea

func  PrintArea (s ShapeInterface){
    fmt.Printf("Interface => %s : Area %v\r\n", s.GetName(), s.Area())
}

由於並不像例子裏的這麼簡單,後來個人解決方法是定義了一個相似InitShape的方法來完成初始化流程,這裏我把ShapeInterface接口和Shape類型作一些調整會更好理解一些。

type ShapeInterface interface {
    Area() float64
    GetName() string
    SetArea(float64)
}
type Shape struct {
    name string
    area float64
}

...
func (s *Shape) SetArea(area float64) {
    s.area = area
}

func (s *Shape) PrintArea() {
    fmt.Printf("%s : Area %v\r\n", s.name, s.area)
}
...

func InitShape(s ShapeInterface) error {
  area, err := s.Area()
  if err != nil {
    return err
  }
  s.SetArea(area)
  ...
}

對於RectangleCircle這樣的組合Shape的類型,只須要按照本身的計算面積的公式實現Area()SetArea()會把Area()計算出的面積存儲在area字段供後面的程序使用。

type Rectangle struct {
    Shape
    w, h float64
}

func (r *Rectangle) Area() float64 {
    return r.w * r.h
}

r := &Rectangle {
    Shape: Shape{name: "Rectangle"},
    w: 5, 4
}

InitShape(r)
r.PrintArea()

這個案例也是我使用Go寫代碼以來第一次研究繼承和組合的區別,以及怎麼用組合的方式在Go語言裏複用代碼和提供多態的支持。我以爲不少以前用慣面嚮對象語言的朋友們或多或少都會遇到一樣的問題,畢竟思惟定式造成後要靠刻意練習才能打破。因爲我不能透漏公司代碼的設計,因此以這個簡單的例子把這部分的使用經驗記錄下來分享給你們。讀者朋友們在用Go語言設計接口和類型時若是遇到相似問題或者有其餘疑問能夠在文章下面留言,一塊兒討論。

第1頁.png

相關文章
相關標籤/搜索