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()
,同时Rectangle
和 Shape
同样又都是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
经过组合得到了Shape
的PrintArea()
方法就能打印出它本身的面积来。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()
方法,而是去调用了Shape
的Area()
方法。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()
方法(没有接受者是*Rectangle
的PrintArea()
方法),而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) ... }
对于Rectangle
和Circle
这样的组合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
语言设计接口和类型时若是遇到相似问题或者有其余疑问能够在文章下面留言,一块儿讨论。