声明:本文所述技巧纯属个人原创,如有雷同且发表于我之前,纯属我没搜对关键字;如有雷同且发表于我之后,纯属抄袭或他没搜对关键字。
前言
您是否偶尔嫌弃golang的继承实现过于的松散或随意?是否苦恼于时常无意间实现某些莫名其妙的接口或自己接口被人莫名其妙的实现?是否希望在继承或实现关系上增加多一些的约束?——本文一口气满足你,且在使用上还有注释标签般的简洁。
使用场景
比如我们有如下两个接口:
type CekAlgo interface {
Decrypt(s string) ([]byte, error)
}
type ContentAlgo interface {
Encrypt(data []byte) (string, error)
Decrypt(s string) ([]byte, error)
}
假设我们的设计初衷,是这两个接口各有各的使用场景、互不干涉,只是恰好出现了同名方法。然而按照go语言的基本规则,后者天然会成为前者的继承。于是在定义上就会违背了我们的预期,在现实使用中也会凭空多出一些干扰和隐患。比如当我定义了这样一个方法,将二者打包配套使用时——
func Xxxx(cekAlgo CekAlgo, contentAlgo ContentAlgo){
}
就很容易出现参数类型过于泛用的问题(参数2的对象可以直接传给参数1),令我们对方法参数的类型限制形同虚设,从而增加传错参数的隐患。
虽然我们在使用时小心些也没问题,但是接口的使用者(你的迷糊同事、你分布在天南海北的sdk使用者、忘记此处应当小心的未来你自己)永远是不可靠的,能在设计阶段就精益求精、尽量防呆,何乐不为?本文即是帮助您解决这个问题。
解决方案1.0:粗略的做法
防止接口被相近的接口意料外地实现,本质就还是咱定义的内容太“宽泛”、“通用“,所以最简单的做法是为它加上一个只属于他自己的“特征方法”。类似这样:
type CekAlgo interface {
//cek只需要解密,加密是客户端的事
Decrypt(s string) ([]byte, error)
CekAlgoOnly()
}
从此往后类似ContentAlgo这样的接口或类就不再会自动成为CekAlgo的继承或实现。而你主动希望某类成为CekAlgo的继承或实现时只需手动加上func (ca *CekAlgoXxxx) CekAlgoOnly(){}的方法(假设CekAlgoRsaBase64是我们期望的CekAlgo子类),操作如下——
type CekAlgoRsaBase64 struct {
privateKey string
}
func (ca *CekAlgoRsaBase64) Decrypt(s string) ([]byte, error) {
return rsa.RsaDecryptByBase64(s, ca.privateKey)
}
//⬇️附带这个小尾巴后,才·能·成为CekAlgo的实现类
func (ca *CekAlgoRsaBase64) CekAlgoOnly(){}
但是这样总归有些不顺眼,因为从定义上讲CekAlgoOnly()应该是一个正儿八经的方法,但在这里它却不包含任何代码和逻辑,仅凭一个名称的定义在起作用,性质和功能相当的不一致——文不对题就难免会增加工作上的交流理解门槛——同时这长长一串的空方法定义也略感累赘。
那么有没有办法足够的“一目了然”?在保障防呆的同时还能清晰易懂?
于是我们有了方案2——
解决方案2.0:“特征方法”的精简
首先我们将上述的“特征方法”改为私有
type CekAlgo interface {
cekAlgoPrivate() //防止被其他接口无意实现
Decrypt(s string) ([]byte, error)
}
此时整个CekAlgo都成了包外无法直接实现的状态,不要慌,我们再在包里加上一个自定义对象
type BaseCekAlgo struct {
}
func (ca *BaseCekAlgo) cekAlgoPrivate() {
}
我们知道go中没有直接的“类的继承”,一般都是使用struts的匿名嵌套来达到“继承”的效果,于是上述CekAlgoRsaBase64就可以如下改造来曲线救国,间接实现cekAlgoPrivate()方法
type CekAlgoRsaBase64 struct {
BaseCekAlgo
privateKey string
}
func (ca *CekAlgoRsaBase64) Decrypt(s string) ([]byte, error) {
return rsa.RsaDecryptByBase64(s, ca.privateKey)
}
这套做法,简而言之,就是用类的匿名嵌套(继承)代替了实现时的手写“特征方法”,从而在子类的定义阶段用简洁的BaseCekAlgo的嵌套代替了较为冗长复杂的
func (ca *CekAlgoRsaBase64) CekAlgoOnly(){}
这种替换的一个好处是BaseCekAlgo将会出现在struct的定义部分,而非方案1中的“方法定义”部分,更符合其存在的意义;另一个好处则是他是一段完全由你自定义的词组、单词,而不再是一个必须须尾俱全的冗长且完整的空方法定义。
感觉如何?我们用如此简洁的方式规避了接口过于泛用的隐患,是否确实是一个极具性价比的选择?
解决方案3.0:画龙点睛的改名
严格说来真正的技术到上一条就完成了,但我太喜欢这最后一步,所以单独拎出来作为一个进一步升级的步骤。那就是——
改名!
单单一个BaseCekAlgo只能说中规中矩,但当你将它改成这样⬇️时
type IAmCekAlgo struct {
}
func (ca *IAmCekAlgo) cekAlgoPrivate() {
}
在具体实现的代码里,你的代码就会变成这样
type CekAlgoRsaBase64 struct {
IAmCekAlgo
privateKey string
}
func (ca *CekAlgoRsaBase64) Decrypt(s string) ([]byte, error) {
return rsa.RsaDecryptByBase64(s, ca.privateKey)
}
有没有觉得在生动有趣方面上升了好几个档次?BaseXxx只是干巴巴地告诉你“这里内嵌了一个基础类”,而IAmXxx则像一个自述,一个声明,声明它自己是铁板钉钉的CekAlgo子孙,这一刻有没有感觉代码有了灵魂在主动与你对话?
所以我强烈建议本篇的读者用IAmXxxx来表示一个子类(子接口)的实现(继承)关系。
补遗
自我开发出本篇策略的一天之后,我忽然发现一个悲伤的故事:
原先以为的
type ContentAlgo interface {
Encrypt(data []byte) (string, error)
Decrypt(s string) ([]byte, error)
}
接口其实少考虑了一个参数,事实上应该是
type ContentAlgo interface {
Encrypt(data []byte, cek []byte) (string, error)
Decrypt(s string, cek []byte) ([]byte, error)
}
而那个很容易喜当爹的CekAlgo依然是
type CekAlgo interface {
Decrypt(s string) ([]byte, error)
}
所以……
这俩接口从一开始就彻底不同……
所以……从一开始就没必要做什么防呆策略或者预防“串味”……
……好吧……
虽然我自己是用不上了,但我亲爱的同行们,这些策略也可以记着,将来做架构师造其他轮子的时候,或许也是用得上的。