鲁迅曾经说过,复杂的问题都能用增加一层抽象来解决。(鲁迅: 我特么没说过!)
在实际工程中,为了解耦,可能需要定义大量的接口。
如在用户管理系统中,可能含有一个像根据用户名检索用户的服务 UserSearcher:
type UserSearcher interface{
Search(name string)([]*User,error)
}
一般情况下在项目初期,只有一个基于数据库的实现——*DBUserSearcher。
type DBUserSearcher struct{}
func (*DBUserSearcher) Search(name string)(users []*User,err error){
return
}
如此这样,如果有几十个服务,可能就要定义几十个接口,毕竟随时可能替换具体实现,避免改动对整个系统造成的冲击。然而百分之九十的情况之下,像这些的接口在软件整个生命中都只存在一种实现,代码量的膨胀显然带来的收益很小。
而在项目刚启动的时候,接口的定义往往改动很频繁,这时候不仅仅需要在接口定义上修改,还要去接口实现的类型上进行修改,这样就会非常痛苦,很不灵活。
如果这些困扰也曾让您烦恼过,那么接下来,我介绍的这一种我称之为“延后抽象”的技巧就能够帮助您减轻很大一部分痛苦。
在Go的1.9版本以上,利用类型别名的特性,结合golang接口的鸭子类型特点:
先将本要定义成接口的抽象类型,直接定义为具体实现类型的别名,在将来需要扩展抽象类型具体实现的时候,再将抽象类型定义为接口。
比如:
在第一个版本代码里,我们简单的将UserSearcher取为*DBUserSearcher的别名。
type UserSearcher = *DBUserSearcher
这个时候其他模块使用的是UserSearcher这一类型,而不是‘具体’的*DBUserSearcher类型。
而在将来某次迭代中,为了提升检索效率,可能选择性的使用基于Elasticsearch的检索。
我们在此时再将UserSearcher 改为接口定义
type UserSearcher interface{
Search(name string)([]*User,error)
}
这时,由于Go接口的鸭子类型特性,*DBUserSearcher自然仍然是UserSearcher类型。
其余代码不变,只需要增加一个基于Elasticsearch调用的UserSearcher实现即可。
type ESUserSearcher struct{}
func (*ESUserSearcher) Search(name string)(users []*User,err error){
return
}
这种通过这种结合了鸭子类型和类型别名的“延迟抽象”的办法,由于避免了大量无用的接口定义,就能在项目刚开发阶段省下非常多不必要的代码,同时也不会降低代码的可维护性。