聊聊在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#

因为咱们这不少推广类促销类的需求不少,几乎每个月两三次,每季度还有大型推广活动。产品经理也是绞尽脑汁想各类玩法来提升用户活跃和订单量。每次都是前面玩法不同,但最后都是参与任务得积分啦、分享后抽奖啦。因而乎我就肩负起了设计通用化流程的任务。根据每次需求通用的部分设计了接口和基础的实现类型,同时预留了给子类实现的方法,应对每次不同的前置条件,这不就是面向对象里常常干的事儿嘛。bash

为了好理解咱们仍是用上面那个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()方法就能打印出它本身的面积来。测试

到目前为止,这些还都是个人设想,规划完后本身感受特兴奋,感受本身已经掌握了组合(Composition)这种思想的精髓...... 按这个思路我就把整套流程都写完了,单元测试只测了每一个子功能,前置条件太复杂加上我还管团队里的其余项目本身的时间不太富余,因此就交付给组里的伙伴们使用了让他们顺便帮我测试下整个流程,而后就现场翻车了......spa

咱们把上面那个例子运行一下,为了能看出区别,又专门写了一个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()方法会被调用 !! 
    }

}
复制代码

运行后的输出结果以下:指针

Shape : Area 0
Circle : Area 314.1592653589793
Rectangle : Area 0
复制代码

看出问题来了不,Rectangle经过组合Shape得到的PrintArea()方法并无去调用Rectangle实现的Area()方法,而是去调用了ShapeArea()方法。Circle是由于本身重写了PrintArea()因此在方法里调用到了自身的Area()code

在项目里那个相似例子里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语言设计接口和类型时若是遇到相似问题或者有其余疑问能够在文章下面留言,一块儿讨论。

相关文章
相关标签/搜索