golang的“双向继承”让编程更优雅

1.背景

笔者开发⼀套分布式系统的时候,在管理端实现了raft算法来避免整个系统的单点故障,整个系统的⼤致描述如下:

  1. 管理端的所有节点状态是⼀致的,所以⽤peer定义管理端节点是⽐较贴切的; 
  2. 在管理端所有节点中选举出了⼀个leader,所有跟系统管理相关的决策都是由leader发出,peer同步leader的决策,这样所有的peer状态是⼀致的,当leader所在的peer异常,重新选举出来的leader就可以在上⼀个leader的基础上继续执⾏决策;
  3. 需要注意的⼀点:leader的决策需要通过raft的提议(propose)超过⼀半以上的peer通过够才能被peer应⽤,所以从leader决策开始到整个系统确认决策执⾏成功这期间要经过若⼲个过程,我们
  4. 这⾥简单描述为这是⼀个异步的过程;

本⽂不讨论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;
}

接下来看看这种定义⽅法在实现过程中是否会遇到麻烦,我们先从以下三种视⻆分析:

  1. leader视⻆:对象是leader类构造,接⼝和属性都是为决策服务的,因为继承⾃peer,当前系统的状态通过继承了peer⾃动获得;
  2. peer视⻆:对象是peer类构造,接⼝和属性都是与同步系统状态有关的,也不⽤关⼼什么决策问题,让⼲什么就⼲什么;
  3.  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
}

如何解读这两个类呢?

  1. Peer:与上⾯提到的Peer基本⼀样,不同点在于Peer.Leader为空就是普通的Peer,不为空就是Leader;
  2. 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的继承⽅法实
现给⼈⼀种双向继承的假象⽽已。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值