1.背景
笔者开发⼀套分布式系统的时候,在管理端实现了raft算法来避免整个系统的单点故障,整个系统的⼤致描述如下:
- 管理端的所有节点状态是⼀致的,所以⽤peer定义管理端节点是⽐较贴切的;
- 在管理端所有节点中选举出了⼀个leader,所有跟系统管理相关的决策都是由leader发出,peer同步leader的决策,这样所有的peer状态是⼀致的,当leader所在的peer异常,重新选举出来的leader就可以在上⼀个leader的基础上继续执⾏决策;
- 需要注意的⼀点:leader的决策需要通过raft的提议(propose)超过⼀半以上的peer通过够才能被peer应⽤,所以从leader决策开始到整个系统确认决策执⾏成功这期间要经过若⼲个过程,我们
- 这⾥简单描述为这是⼀个异步的过程;
本⽂不讨论raft相关的内容,只是借助raft引出peer和leader的概念。根据以上描述,⽤⾯相对象编程⽅法实现应该如何定义类?
2.分析
⽐较常规⽅法应该是:leader是⼀种具备更多属性和接⼝的peer,所以leader应该继承⾃peer,那么代码(C++)定义如下:
// 定义peer类
class Peer {
public:
Peer(){}
~Peer(){}
public:
void PeerMethod(void) {}
private:
int peerVariables;
}
// 定义leader类,继承自peer
class Leader : public Peer {
public:
Leader(){}
~Leader(){}
public:
void LeaderMethod(void) {}
private:
int leaderVariables;
}
接下来看看这种定义⽅法在实现过程中是否会遇到麻烦,我们先从以下三种视⻆分析:
- leader视⻆:对象是leader类构造,接⼝和属性都是为决策服务的,因为继承⾃peer,当前系统的状态通过继承了peer⾃动获得;
- peer视⻆:对象是peer类构造,接⼝和属性都是与同步系统状态有关的,也不⽤关⼼什么决策问题,让⼲什么就⼲什么;
- leader peer视⻆:以上两种视⻆还是⽐较好理解的,那什么是leader peer呢,就是peer中的leader!
关于leader peer,很多⼩伙伴们肯定都坐不住了,这不是废话么?不是跟leader⼀样的么?这就要从实际场景出发了,在还没选举出leader之前,所有的节点都还是peer,此时提供服务的对象是peer构造出来的;当某个peer成功选举为leader,那么提供服务的对象应该是有leader构造出来的,切记leader也是peer,所以通过leader构造出来的对象同时具备了peer和leader能⼒。
那么问题来了,该⽤什么类型的对象提供服务呢?从上⾯提到的继承关系来看,采⽤peer类型的对象相对更合理,并且⼦类的对象可以赋值给⽗类类对象。但是,当peer需要切换成leader身份的时候,⽆论是C++还是JAVA或多或少都要加⼊⼀些强制转换的语句,将peer对象赋值给leader对象,然后在⽤leader的对象执⾏⼀些操作。如下代码所示:
{
Leader *leader = (Leader*)peer;
leader->LeaderMethod();
}
笔者以前没有接触golang的时候,感觉上⾯的代码再正常不过了,⾃从⾃定义了所谓的“双向继承”就感觉上⾯的代码不够优雅了。所谓的双向继承就是两个类型彼此互相继承,这在C++或者JAVA中是不可想象的,⼀个类A即是类B的⽗类,也是类B的⼦类,从伦理上说不通,代码上也⽆法实现。但是在golang中是可以做到这⼀点的,如下代码所示:
// 定义Peer
type Peer struct {
*Leader
peerVariables int
}
// 定义Leader
type Leader struct {
*Peer
leaderVariables int
}
如何解读这两个类呢?
- Peer:与上⾯提到的Peer基本⼀样,不同点在于Peer.Leader为空就是普通的Peer,不为空就是Leader;
- Leader:与上⾯的提到的Leader完全⼀样;
仅此⼀点点的改变,就会让逻辑变成更加流畅,代码更加优雅。作为提供服务的对象是Peer类型,⽆论是身份的切换还是身份的判断都变得⾮常⾃然,如下代码所示:
// 成功选举为Leader
{
peer.Leader = &Leader{Peer: peer}
}
// 需要切换身份处理时
{
if nil != peer.Leader {
peer.LeaderMethod()
}
}
如果读者对于上⾯的代码没有任何感觉,认为和C++/JAVA没什么区别,要么读者是个⼤神,根本看不上笔者的⼩技巧,要么就是没有get到笔者的点。仅此⼀点点的改变,已经让笔者的代码和逻辑⼀下⼦清爽了很多!
总结
其实从继承⻆度说,本不应该有双向⼀说,否则就不是继承的概念了,笔者⽆⾮是借⽤了golang的继承机制简化了编程和逻辑。这⾥,就不得不提⼀下继承的本质,下⾯⽤C代码描述继承的本质:
// 定义结构体A
struct A {
int a;
}
// 定义结构体B,并且继承A
struct B {
struct A a;
int b;
}
所谓的继承其实就是编译器将⽗类的成员变量全部放到⼦类中,在⼦类中访问⽗类的成员(成员函数或者成员变量)时可以通过点运算符引⽤,⽽⽤C语⾔访问则需要B.a.a才能访问到。当然⾯向对象的语⾔在继承上扩展了很多功能,不在本⽂的讨论范⽂,不再过多描述。
我们再来看看golang的继承⽅法:
// 定义类型A
type A struct {
a int
}
// 定义类型B,并且继承A
type B struct {
A
b int
}
golang的这种继承⽅法与C++/JAVA⼀样,new⼦类对象同时构造了⽗类,因为sizeof(B)=sizeof(A)+B成员变量总⼤⼩(此处忽略虚函数表),⼦类中包含了⽗类的全部内容。但是golang还有⼀种继承⽅式如下代码所示:
type B struct {
*A
b int
}
这种⽅式new B的时候需要再new A,相⽐于上⼀种,区别就在于内存是⼀个的还是两个。就是这⼀
点的区别让开发者拥有了更⼤的发挥空间,本⽂提到的案例就是利⽤了这⼀点。可能有⼈会说,这⽤C语⾔也可以实现呀,如下代码所示:
struct B {
struct A* a;
int b;
}
的确如此,虽然引⽤A的成员时稍微繁琐⼀点,⽐如:B.a->a,但是和golang达到的效果是⼀样的。笔者⾮常赞同这些读者的想法,但笔者要说的是:虽然两个不同的概念最终的实现⽅法是⼀样的,但是每个概念都有他应⽤的地⽅,可以让这个概念所在的上下⽂更加容易理解,更加清晰。B.a和B.a->a,虽然效果是⼀样的,但是表达出来的意义是不⼀样的,前者的意义a是B的⼀个属性,后者的意义a是B⼀个名为a的属性的属性。
最后,再回到leader和peer的案例上来,其实并不是真正的双向继承,leader继承了peer是真继承,
⽽peer中的leader指针应该是peer的⼀个属性,即peer.leader,⽆⾮是笔者采⽤了golang的继承⽅法实
现给⼈⼀种双向继承的假象⽽已。