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的繼承⽅法實
現給⼈⼀種雙向繼承的假象⽽已。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章