Go 的接口 —— 进化与高可用性的设计
在我那篇《Go 中的接口》的文章中,我用了一个很简单的示例来讲明Go的接口实现——矩形与方形同时实现了形状接口。但是我们要清楚的认识到,Go的接口并不是一种C或者Java的变体,而是具有了很多的特性,它是大规模程序开发及高可用性程序开发所需要的关键。
先看看Java里面接口的实现方法,然后再返回来看看Go的接口的实现方式,我们就能很容易发现为什么说Go的接口实现可用性可扩展性更高,而且进化到另一个层次。我们的数据模型是 *Bus*,它同时实现了两人个接口——只有可用空间的立方体以及可以运输乘客的运输工具:
// 第一步:在编码之前,先设计好接口以及类的继承
// 第二步:将现实世界中的对象抽象成为我们可以在程序中使用的接口
interface PublicTransport {
int PassengerCapacity();
}
interface Cuboid {
int CubicVolumn();
}
// 第三步:创建数据结构同时去实现我们在第二步中定义的所有接口
public class Bus implements
PublicTransport,
Cuboid {
// 定义类的数据结构
int l, b, h, rows, seatsPerRow;
public Bus(int l, int b, int h, int rows, int seatsPerRow) {
this.l =l; this.b = b; this.h = h; this.rows = rows;
this.seatsPerRow = seatsPerRow;
}
// 定义方法的实现
public int CubicVolumn() { return l*b*h; }
public int PassengerCapacity() { return rows * seatsPerRow; }
// 在 main 函数中调用类及其方法
public static void main() {
Bus b = new Bus(10, 6, 3, 10, 5);
System.out.Println(b.CubicVolume());
System.out.Println(b.PassengerCapacity());
}
}在上面的Java示例中,你必须在实现接口方法之前明确地定义类的数据层次结构,并且需要使用implements 这个关键字,C# 中则是将接口名称在 : 符号后面列出来代替 implements 关键字,在这种情况下,任何这部分的需求的变更都会影响到项目的核心模块。
绝大数的项目就是在这种先定义接口然后去实现最后发现不能完全需求,又返回去修改接口中再实现再修改,如此往复,直到接口已经考虑到各方面的因素/涵盖了所有需求——而如果此时项目的需求还在变化的话,那工作量就更大了。在完全确定了接口之后,就不再推荐修改这些最底层的接口了,即使是项目管理员也不允许随意的修改它,而确实需要修改的话,有可能还需要通过各种各样大大小小的项目组研讨会等最后才能确定是否修改以及如何修改,因为接口做为最底层,它的一点修改,都会影响到上层的实现,如果一个接口有一百个实现,那么它的修改将需要对其一百个实现也做相应的修改。或者还有另一种方法,那就是重新创建一个新的接口,接口的定义就是现在这个接口修改后的定义,然后再创建新的类去实现新的接口。当我们的项目达到某一个层次之后,发现所有的这些事情已经完全失控了。
那有没有更好的办法?我们先来看看上面这个Java示例的的Go的实现:
package main
import "fmt"
// Go第一步:定义你的数据结构
type Bus struct {
l, b, h int
rows, seatsPerRow int
}
// Go第二步:定义一个现实世界对象的抽象层
type Cuboider interface {
CubicVolume() int
}
// Go第三步:实现接口的方法
func (bus Bus) CubicVolume() int {
return bus.l * bus.b * bus.h
}
// 重复上面的二三步
type PublicTransporter interface {
PassengerCapacity() int
}
func (bus Bus) PassengerCapacity() int {
return bus.rows * bus.seatsPerRow
}
func main() {
bus := Bus{
l:10, b:6, h:3,
rows: 10, seatsPerRow: 5}
fmt.Println("Cubic volume of bus: ", bus.CubicVolume())
fmt.Println("Maximum number of passengers:", bus.PassengerCapacity())
}在上在面的Go的实现中,所使用的名词与Java版本都一样,但是这之间却还是有很多的不一样,比如最直接的区别是我们看不到 implements 这种在Java中用来定义类的接口实现的关键字了。另外,Go中,看起来似乎是以数据为中心的,你先定义了你的数据结构,然后定义它的接口,而这种接口与实现之间的层次结构是没有明确说明的,我们没有使用特别的签名来关联接口与实现,而是Go根据语法规则自动的为你实现了接口。在这个示例中,似乎我们看不到Go的实现方法有任何的好处,但是当项目越来越大,或者需求变更时,Go的方法将被证明是更好的。
让我们假设,随着时间的发展,一个项目要求我们的巴士——法律要求,每一个乘客都应该有至于自己的最低限度的立体空间,因为我们之前只是按座位计算可乘客人数,而现还需要考虑汽车的空间以及乘客所能分配到的平均空间量。
那么,我们的汽车需要实现一个新的接口 PersonalSpaceLaw ,它的方法在任何其它的接口都是没有被实现过的(我们不得不这么做,以适应新的法律要求):
// 新的需求
interface PersonalSpaceLaw {
boolean IsCampliantWithLaw();
}
class Bus implements
PublicTransport,
Cuboid,
PersonalSpaceLaw {
//.....
public IsCampliantWithLaw() {
return (l * b * h ) / ( rows * seatsPerRow ) > 3;
}
//......
}该需要的改变需要我们修改类的结构,添加新的实现的方法,而该类修改之后,所有继承Bus的子类都需要做相应的修改以适应新的Bus结构,但是让我们来看看Go的方法,只需要在代码中加入下面这些即可,不需要对其它已有的代码做任何修改:
// 新的项目需求
type PersonalSpaceLaw interface {
IsCampliantWithLaw() bool
}
func (bus Bus) IsCampliantWithLaw() bool {
return ( bus.l * bus.b * bus.h ) / (bus.rows * bus.seatsPerRow) >= 3
}完整代码为:
package main
import "fmt"
// Go第一步:定义你的数据结构
type Bus struct {
l, b, h int
rows, seatsPerRow int
}
// Go第二步:定义一个现实世界对象的抽象层
type Cuboider interface {
CubicVolume() int
}
// Go第三步:实现接口的方法
func (bus Bus) CubicVolume() int {
return bus.l * bus.b * bus.h
}
// 重复上面的二三步
type PublicTransporter interface {
PassengerCapacity() int
}
func (bus Bus) PassengerCapacity() int {
return bus.rows * bus.seatsPerRow
}
// 新的项目需求
type PersonalSpaceLaw interface {
IsCampliantWithLaw() bool
}
func (bus Bus) IsCampliantWithLaw() bool {
return ( bus.l * bus.b * bus.h ) / (bus.rows * bus.seatsPerRow) >= 3
}
func main() {
bus := Bus{
l:10, b:6, h:3,
rows: 10, seatsPerRow: 5}
fmt.Println("Cubic volume of bus: ", bus.CubicVolume())
fmt.Println("Maximum number of passengers:", bus.PassengerCapacity())
fmt.Println("Is campliant with law:", bus.IsCampliantWithLaw())
}输入结果为:
Cubic volume of bus: 180 Maximum number of passengers: 50 Is campliant with law: true
评论已关闭