Go開發(fā)關(guān)鍵技術(shù)指南:Interfaces

Interfaces

Go在類型和接口上的思考是:

  • Go類型系統(tǒng)并不是一般意義的OO,并不支持虛函數(shù)。
  • Go的接口是隱含實(shí)現(xiàn),更靈活,更便于適配和替換。
  • Go支持的是組合、小接口、組合+小接口。
  • 接口設(shè)計(jì)應(yīng)該考慮正交性,組合更利于正交性。

Type System

Go的類型系統(tǒng)是比較容易和C++/Java混淆的,特別是習(xí)慣于類體系和虛函數(shù)的思路后,很容易想在Go走這個(gè)路子,可惜是走不通的。而interface因?yàn)樘^(guò)于簡(jiǎn)單,而且和C++/Java中的概念差異不是特別明顯,所以這個(gè)章節(jié)專門分析Go的類型系統(tǒng)。

先看一個(gè)典型的問(wèn)題Is it possible to call overridden method from parent struct in golang?,代碼如下所示:

package main

import (
  "fmt"
)

type A struct {
}

func (a *A) Foo() {
  fmt.Println("A.Foo()")
}

func (a *A) Bar() {
  a.Foo()
}

type B struct {
  A
}

func (b *B) Foo() {
  fmt.Println("B.Foo()")
}

func main() {
  b := B{A: A{}}
  b.Bar()
}

本質(zhì)上它是一個(gè)模板方法模式(TemplateMethodPattern),A的Bar調(diào)用了虛函數(shù)Foo,期待子類重寫虛函數(shù)Foo,這是典型的C++/Java解決問(wèn)題的思路。

我們借用模板方法模式(TemplateMethodPattern)中的例子,考慮實(shí)現(xiàn)一個(gè)跨平臺(tái)編譯器,提供給用戶使用的函數(shù)是crossCompile,而這個(gè)函數(shù)調(diào)用了兩個(gè)模板方法collectSourcecompileToTarget

public abstract class CrossCompiler {
  public final void crossCompile() {
    collectSource();
    compileToTarget();
  }
  //Template methods
  protected abstract void collectSource();
  protected abstract void compileToTarget();
}

C++版,不用OOAD思維參考C++: CrossCompiler use StateMachine,代碼如下所示:

// g++ compiler.cpp -o compiler && ./compiler
#include <stdio.h>

void beforeCompile() {
  printf("Before compile\n");
}

void afterCompile() {
  printf("After compile\n");
}

void collectSource(bool isIPhone) {
  if (isIPhone) {
    printf("IPhone: Collect source\n");
  } else {
        printf("Android: Collect source\n");
    }
}

void compileToTarget(bool isIPhone) {
  if (isIPhone) {
    printf("IPhone: Compile to target\n");
  } else {
        printf("Android: Compile to target\n");
    }
}

void IDEBuild(bool isIPhone) {
  beforeCompile();

  collectSource(isIPhone);
  compileToTarget(isIPhone);

  afterCompile();
}

int main(int argc, char** argv) {
  IDEBuild(true);
  //IDEBuild(false);
  return 0;
}

C++版本使用OOAD思維,可以參考C++: CrossCompiler,代碼如下所示:

// g++ compiler.cpp -o compiler && ./compiler
#include <stdio.h>

class CrossCompiler {
public:
  void crossCompile() {
    beforeCompile();

    collectSource();
    compileToTarget();

    afterCompile();
  }
private:
  void beforeCompile() {
    printf("Before compile\n");
  }
  void afterCompile() {
    printf("After compile\n");
  }
// Template methods.
public:
  virtual void collectSource() = 0;
  virtual void compileToTarget() = 0;
};

class IPhoneCompiler : public CrossCompiler {
public:
  void collectSource() {
    printf("IPhone: Collect source\n");
  }
  void compileToTarget() {
    printf("IPhone: Compile to target\n");
  }
};

class AndroidCompiler : public CrossCompiler {
public:
  void collectSource() {
      printf("Android: Collect source\n");
  }
  void compileToTarget() {
      printf("Android: Compile to target\n");
  }
};

void IDEBuild(CrossCompiler* compiler) {
  compiler->crossCompile();
}

int main(int argc, char** argv) {
  IDEBuild(new IPhoneCompiler());
  //IDEBuild(new AndroidCompiler());
  return 0;
}

我們可以針對(duì)不同的平臺(tái)實(shí)現(xiàn)這個(gè)編譯器,比如Android和iPhone:

public class IPhoneCompiler extends CrossCompiler {
  protected void collectSource() {
    //anything specific to this class
  }
  protected void compileToTarget() {
    //iphone specific compilation
  }
}

public class AndroidCompiler extends CrossCompiler {
  protected void collectSource() {
    //anything specific to this class
  }
  protected void compileToTarget() {
    //android specific compilation
  }
}

在C++/Java中能夠完美的工作,但是在Go中,使用結(jié)構(gòu)體嵌套只能這么實(shí)現(xiàn),讓IPhoneCompiler和AndroidCompiler內(nèi)嵌CrossCompiler,參考Go: TemplateMethod,代碼如下所示:

package main

import (
  "fmt"
)

type CrossCompiler struct {
}

func (v CrossCompiler) crossCompile() {
  v.collectSource()
  v.compileToTarget()
}

func (v CrossCompiler) collectSource() {
  fmt.Println("CrossCompiler.collectSource")
}

func (v CrossCompiler) compileToTarget() {
  fmt.Println("CrossCompiler.compileToTarget")
}

type IPhoneCompiler struct {
  CrossCompiler
}

func (v IPhoneCompiler) collectSource() {
  fmt.Println("IPhoneCompiler.collectSource")
}

func (v IPhoneCompiler) compileToTarget() {
  fmt.Println("IPhoneCompiler.compileToTarget")
}

type AndroidCompiler struct {
  CrossCompiler
}

func (v AndroidCompiler) collectSource() {
  fmt.Println("AndroidCompiler.collectSource")
}

func (v AndroidCompiler) compileToTarget() {
  fmt.Println("AndroidCompiler.compileToTarget")
}

func main() {
  iPhone := IPhoneCompiler{}
  iPhone.crossCompile()
}

執(zhí)行結(jié)果卻讓人手足無(wú)措:

# Expect
IPhoneCompiler.collectSource
IPhoneCompiler.compileToTarget

# Output
CrossCompiler.collectSource
CrossCompiler.compileToTarget

Go并沒(méi)有支持類繼承體系和多態(tài),Go是面向?qū)ο髤s不是一般所理解的那種面向?qū)ο?,用老子的話說(shuō)“道可道,非常道”。

實(shí)際上在OOAD中,除了類繼承之外,還有另外一個(gè)解決問(wèn)題的思路就是組合Composition,面向?qū)ο笤O(shè)計(jì)原則中有個(gè)很重要的就是The Composite Reuse Principle (CRP),Favor delegation over inheritance as a reuse mechanism,重用機(jī)制應(yīng)該優(yōu)先使用組合(代理)而不是類繼承。類繼承會(huì)喪失靈活性,而且訪問(wèn)的范圍比組合要大;組合有很高的靈活性,另外組合使用另外對(duì)象的接口,所以能獲得最小的信息。

C++如何使用組合代替繼承實(shí)現(xiàn)模板方法?可以考慮讓CrossCompiler使用其他的類提供的服務(wù),或者說(shuō)使用接口,比如CrossCompiler依賴于ICompiler

public interface ICompiler {
  //Template methods
  protected abstract void collectSource();
  protected abstract void compileToTarget();
}

public abstract class CrossCompiler {
  public ICompiler compiler;
  public final void crossCompile() {
    compiler.collectSource();
    compiler.compileToTarget();
  }
}

C++版本可以參考C++: CrossCompiler use Composition,代碼如下所示:

// g++ compiler.cpp -o compiler && ./compiler
#include <stdio.h>

class ICompiler {
// Template methods.
public:
  virtual void collectSource() = 0;
  virtual void compileToTarget() = 0;
};

class CrossCompiler {
public:
  CrossCompiler(ICompiler* compiler) : c(compiler) {
  }
  void crossCompile() {
    beforeCompile();

    c->collectSource();
    c->compileToTarget();

    afterCompile();
  }
private:
  void beforeCompile() {
    printf("Before compile\n");
  }
  void afterCompile() {
    printf("After compile\n");
  }
  ICompiler* c;
};

class IPhoneCompiler : public ICompiler {
public:
  void collectSource() {
    printf("IPhone: Collect source\n");
  }
  void compileToTarget() {
    printf("IPhone: Compile to target\n");
  }
};

class AndroidCompiler : public ICompiler {
public:
  void collectSource() {
      printf("Android: Collect source\n");
  }
  void compileToTarget() {
      printf("Android: Compile to target\n");
  }
};

void IDEBuild(CrossCompiler* compiler) {
  compiler->crossCompile();
}

int main(int argc, char** argv) {
  IDEBuild(new CrossCompiler(new IPhoneCompiler()));
  //IDEBuild(new CrossCompiler(new AndroidCompiler()));
  return 0;
}

我們可以針對(duì)不同的平臺(tái)實(shí)現(xiàn)這個(gè)ICompiler,比如Android和iPhone。這樣從繼承的類體系,變成了更靈活的接口的組合,以及對(duì)象直接服務(wù)的調(diào)用:

public class IPhoneCompiler implements ICompiler {
  protected void collectSource() {
    //anything specific to this class
  }
  protected void compileToTarget() {
    //iphone specific compilation
  }
}

public class AndroidCompiler implements ICompiler {
  protected void collectSource() {
    //anything specific to this class
  }
  protected void compileToTarget() {
    //android specific compilation
  }
}

在Go中,推薦用組合和接口,小的接口,大的對(duì)象。這樣有利于只獲得自己應(yīng)該獲取的信息,或者不會(huì)獲得太多自己不需要的信息和函數(shù),參考Clients should not be forced to depend on methods they do not use. –Robert C. Martin,以及The bigger the interface, the weaker the abstraction, Rob Pike。關(guān)于面向?qū)ο蟮脑瓌t在Go中的體現(xiàn),參考Go: SOLID中文版Go: SOLID。

先看如何使用Go的思路實(shí)現(xiàn)前面的例子,跨平臺(tái)編譯器,Go Composition: Compiler,代碼如下所示:

package main

import (
  "fmt"
)

type SourceCollector interface {
  collectSource()
}

type TargetCompiler interface {
  compileToTarget()
}

type CrossCompiler struct {
  collector SourceCollector
  compiler  TargetCompiler
}

func (v CrossCompiler) crossCompile() {
  v.collector.collectSource()
  v.compiler.compileToTarget()
}

type IPhoneCompiler struct {
}

func (v IPhoneCompiler) collectSource() {
  fmt.Println("IPhoneCompiler.collectSource")
}

func (v IPhoneCompiler) compileToTarget() {
  fmt.Println("IPhoneCompiler.compileToTarget")
}

type AndroidCompiler struct {
}

func (v AndroidCompiler) collectSource() {
  fmt.Println("AndroidCompiler.collectSource")
}

func (v AndroidCompiler) compileToTarget() {
  fmt.Println("AndroidCompiler.compileToTarget")
}

func main() {
  iPhone := IPhoneCompiler{}
  compiler := CrossCompiler{iPhone, iPhone}
  compiler.crossCompile()
}

這個(gè)方案中,將兩個(gè)模板方法定義成了兩個(gè)接口,CrossCompiler使用了這兩個(gè)接口,因?yàn)楸举|(zhì)上C++/Java將它的函數(shù)定義為抽象函數(shù),意思也是不知道這個(gè)函數(shù)如何實(shí)現(xiàn)。而IPhoneCompilerAndroidCompiler并沒(méi)有繼承關(guān)系,而它們兩個(gè)實(shí)現(xiàn)了這兩個(gè)接口,供CrossCompiler使用;也就是它們之間的關(guān)系,從之前的強(qiáng)制綁定,變成了組合。

type SourceCollector interface {
    collectSource()
}

type TargetCompiler interface {
    compileToTarget()
}

type CrossCompiler struct {
    collector SourceCollector
    compiler  TargetCompiler
}

func (v CrossCompiler) crossCompile() {
    v.collector.collectSource()
    v.compiler.compileToTarget()
}

Rob Pike在Go Language: Small and implicit中描述Go的類型和接口,第29頁(yè)說(shuō):

  • Objects implicitly satisfy interfaces. A type satisfies an interface simply by implementing its methods. There is no "implements" declaration; interfaces are satisfied implicitly. 這種隱式的實(shí)現(xiàn)接口,實(shí)際中還是很靈活的,我們?cè)赗efector時(shí)可以將對(duì)象改成接口,縮小所依賴的接口時(shí),能夠不改變其他地方的代碼。比如如果一個(gè)函數(shù)foo(f *os.File),最初依賴于os.File,但實(shí)際上可能只是依賴于io.Reader就可以方便做UTest,那么可以直接修改成foo(r io.Reader)所有地方都不用修改,特別是這個(gè)接口是新增的自定義接口時(shí)就更明顯。
  • In Go, interfaces are usually small: one or two or even zero methods. 在Go中接口都比較小,非常小,只有一兩個(gè)函數(shù);但是對(duì)象卻會(huì)比較大,會(huì)使用很多的接口。這種方式能夠以最靈活的方式重用代碼,而且保持接口的有效性和最小化,也就是接口隔離。

隱式實(shí)現(xiàn)接口有個(gè)很好的作用,就是兩個(gè)類似的模塊實(shí)現(xiàn)同樣的服務(wù)時(shí),可以無(wú)縫的提供服務(wù),甚至可以同時(shí)提供服務(wù)。比如改進(jìn)現(xiàn)有模塊時(shí),比如兩個(gè)不同的算法。更厲害的時(shí),兩個(gè)模塊創(chuàng)建的私有接口,如果它們簽名一樣,也是可以互通的,其實(shí)簽名一樣就是一樣的接口,無(wú)所謂是不是私有的了。這個(gè)非常強(qiáng)大,可以允許不同的模塊在不同的時(shí)刻升級(jí),這對(duì)于提供服務(wù)的服務(wù)器太重要了。

比較被嚴(yán)重誤認(rèn)為是繼承的,莫過(guò)于是Go的內(nèi)嵌Embeding,因?yàn)镋mbeding本質(zhì)上還是組合不是繼承,參考Embeding is still composition。

Embeding在UTest的Mocking中可以顯著減少需要Mock的函數(shù),比如Mocking net.Conn,如果只需要mock Read和Write兩個(gè)函數(shù),就可以通過(guò)內(nèi)嵌net.Conn來(lái)實(shí)現(xiàn),這樣loopBack也實(shí)現(xiàn)了整個(gè)net.Conn接口,不必每個(gè)接口全部寫一遍:

type loopBack struct {
    net.Conn
    buf bytes.Buffer
}

func (c *loopBack) Read(b []byte) (int, error) {
    return c.buf.Read(b)
}

func (c *loopBack) Write(b []byte) (int, error) {
    return c.buf.Write(b)
}

Embeding只是將內(nèi)嵌的數(shù)據(jù)和函數(shù)自動(dòng)全部代理了一遍而已,本質(zhì)上還是使用這個(gè)內(nèi)嵌對(duì)象的服務(wù)。Outer內(nèi)嵌了Inner,和Outer繼承Inner的區(qū)別在于:內(nèi)嵌Inner是不知道自己被內(nèi)嵌,調(diào)用Inner的函數(shù),并不會(huì)對(duì)Outer有任何影響,Outer內(nèi)嵌Inner只是自動(dòng)將Inner的數(shù)據(jù)和方法代理了一遍,但是本質(zhì)上Inner的東西還不是Outer的東西;對(duì)于繼承,調(diào)用Inner的函數(shù)有可能會(huì)改變Outer的數(shù)據(jù),因?yàn)镺uter繼承Inner,那么Outer就是Inner,二者的依賴是更緊密的。

如果很難理解為何Embeding不是繼承,本質(zhì)上是沒(méi)有區(qū)分繼承和組合的區(qū)別,可以參考Composition not inheritance,Go選擇組合不選擇繼承是深思熟慮的決定,面向?qū)ο蟮睦^承、虛函數(shù)、多態(tài)和類樹被過(guò)度使用了。類繼承樹需要前期就設(shè)計(jì)好,而往往系統(tǒng)在演化時(shí)發(fā)現(xiàn)類繼承樹需要變更,我們無(wú)法在前期就精確設(shè)計(jì)出完美的類繼承樹;Go的接口和組合,在接口變更時(shí),只需要變更最直接的調(diào)用層,而沒(méi)有類子樹需要變更。

The designs are nothing like hierarchical, subtype-inherited methods. They are looser (even ad hoc), organic, decoupled, independent, and therefore scalable.

組合比繼承有個(gè)很關(guān)鍵的優(yōu)勢(shì)是正交性orthogonal,詳細(xì)參考正交性。

Orthogonal

真水無(wú)香,真的牛逼不用裝?!獊?lái)自網(wǎng)絡(luò)

軟件是一門科學(xué)也是藝術(shù),換句話說(shuō)軟件是工程。科學(xué)的意思是邏輯、數(shù)學(xué)、二進(jìn)制,比較偏基礎(chǔ)的理論都是需要數(shù)學(xué)的,比如C的結(jié)構(gòu)化編程是有論證的,那些關(guān)鍵字和邏輯是夠用的。實(shí)際上Go的GC也是有數(shù)學(xué)證明的,還有一些網(wǎng)絡(luò)傳輸算法,又比如奠定一個(gè)新領(lǐng)域的論文比如Google的論文。藝術(shù)的意思是,大部分時(shí)候都用不到嚴(yán)密的論證,有很多種不同的路,還需要看自己的品味或者叫偏見,特別容易引起口水仗和爭(zhēng)論,從好的方面說(shuō),好的軟件或代碼,是能被感覺到很好的。

由于大部分時(shí)候軟件開發(fā)是要靠經(jīng)驗(yàn)的,特別是國(guó)內(nèi)填鴨式教育培養(yǎng)了對(duì)于數(shù)學(xué)的莫名的仇恨(“莫名”主要是早就把該忘的不該忘記的都忘記了),所以在代碼中強(qiáng)調(diào)數(shù)學(xué),會(huì)激發(fā)起大家心中一種特別的鄙視和懷疑,而這種鄙視和懷疑應(yīng)該是以蔥白和畏懼為基礎(chǔ)——大部分時(shí)候在代碼中吹數(shù)學(xué)都會(huì)被認(rèn)為是裝逼。而Orthogonal(正交性)則不擇不扣的是個(gè)數(shù)學(xué)術(shù)語(yǔ),是線性代數(shù)(就是矩陣那個(gè)玩意兒)中用來(lái)描述兩個(gè)向量相關(guān)性的,在平面中就是兩個(gè)線條的垂直。比如下圖:

image.png

Vectors A and B are orthogonal to each other.

旁白:妮瑪,兩個(gè)線條垂直能和代碼有個(gè)毛線關(guān)系,八竿子打不著關(guān)系吧,請(qǐng)繼續(xù)吹。

先請(qǐng)看Go關(guān)于Orthogonal相關(guān)的描述,可能還不止這些地方:

Composition not inheritance Object-oriented programming provides a powerful insight: that the behavior of data can be generalized independently of the representation of that data. The model works best when the behavior (method set) is fixed, but once you subclass a type and add a method, the behaviors are no longer identical. If instead the set of behaviors is fixed, such as in Go's statically defined interfaces, the uniformity of behavior enables data and programs to be composed uniformly, orthogonally, and safely.

JSON-RPC: a tale of interfaces In an inheritance-oriented language like Java or C++, the obvious path would be to generalize the RPC class, and create JsonRPC and GobRPC subclasses. However, this approach becomes tricky if you want to make a further generalization orthogonal to that hierarchy.

實(shí)際上Orthogonal并不是只有Go才提,參考Orthogonal Software。實(shí)際上很多軟件設(shè)計(jì)都會(huì)提正交性,比如OOAD里面也有不少地方用這個(gè)描述。我們先從實(shí)際的例子出發(fā)吧,關(guān)于線程一般Java、Python、C#等語(yǔ)言,會(huì)定義個(gè)線程的類Thread,可能包含以下的方法管理線程:

var thread = new Thread(thread_main_function);
thread.Start();
thread.Interrupt();
thread.Join();
thread.Stop();

如果把goroutine也看成是Go的線程,那么實(shí)際上Go并沒(méi)有提供上面的方法,而是提供了幾種不同的機(jī)制來(lái)管理線程:

  • go關(guān)鍵字啟動(dòng)goroutine。
  • sync.WaitGroup等待線程退出。
  • chan也可以用來(lái)同步,比如等goroutine啟動(dòng)或退出,或者傳遞退出信息給goroutine。
  • context也可以用來(lái)管理goroutine,參考Context。
s := make(chan bool, 0)
q := make(chan bool, 0)
go func() {
    s <- true // goroutine started.
    for {
        select {
        case <-q:
            return
        default:
            // do something.
        }
    }
} ()

<- s // wait for goroutine started.
time.Sleep(10)
q <- true // notify goroutine quit.

注意上面只是例子,實(shí)際中推薦用Context管理goroutine。

如果把goroutine看成一個(gè)向量,把sync看成一個(gè)向量,把chan看成一個(gè)向量,這些向量都不相關(guān),也就是它們是正交的。

再舉在Orthogonal Software的例子,將對(duì)象存儲(chǔ)到TEXT或XML文件,可以直接寫對(duì)象的序列化函數(shù):

def read_dictionary(file)
  if File.extname(file) == ".xml"
    # read and return definitions in XML from file
  else
    # read and return definitions in text from file
  end
end

這個(gè)的壞處包括:

  1. 邏輯代碼和序列化代碼混合在一起,隨處可見序列化代碼,非常難以維護(hù)。
  2. 如果要新增序列化的機(jī)制比如將對(duì)象序列化存儲(chǔ)到網(wǎng)絡(luò)就很費(fèi)勁了。
  3. 假設(shè)TEXT要支持JSON格式,或者INI格式呢?

如果改進(jìn)下這個(gè)例子,將存儲(chǔ)分離:

class Dictionary
  def self.instance(file)
    if File.extname(file) == ".xml"
      XMLDictionary.new(file)
    else
      TextDictionary.new(file)
    end
  end
end

class TextDictionary < Dictionary
  def write
    # write text to @file using the @definitions hash
  end
  def read
    # read text from @file and populate the @definitions hash
  end
end

如果把Dictionay看成一個(gè)向量,把存儲(chǔ)方式看成一個(gè)向量,再把JSON或INI格式看成一個(gè)向量,他們實(shí)際上是可以不相關(guān)的。

再看一個(gè)例子,考慮上面JSON-RPC: a tale of interfaces的修改,實(shí)際上是將序列化的部分,從*gob.Encoder變成了接口ServerCodec,然后實(shí)現(xiàn)了jsonCodec和gobCodec兩種Codec,所以RPC和ServerCodec是正交的。非正交的做法,就是從RPC繼承兩個(gè)類jsonRPC和gobRPC,這樣RPC和Codec是耦合的并不是不相關(guān)的。

Orthogonal不相關(guān)到底有什么好說(shuō)的?

  • 數(shù)學(xué)中不相關(guān)的兩個(gè)向量,可以作為空間的基,比如平面上就是x和y軸,從向量看就是兩個(gè)向量,這兩個(gè)不相關(guān)的向量x和y可以組合出平面的任意向量,平面任一點(diǎn)都可以用x和y表示;如果向量不正交,有些區(qū)域就不能用這兩個(gè)向量表達(dá),有些點(diǎn)就不能表達(dá)。這個(gè)在接口設(shè)計(jì)上就是:正交的接口,能讓用戶靈活組合出能解決各種問(wèn)題的調(diào)用方式,不相關(guān)的向量可以張成整個(gè)向量空間;同樣的如果不正交,有時(shí)候就發(fā)現(xiàn)自己想要的功能無(wú)法通過(guò)現(xiàn)有接口實(shí)現(xiàn),必須修改接口的定義。
  • 比如goroutine的例子,我們可以用sync或chan達(dá)到自己想要的控制goroutine的方式。比如context也是組合了chan、timeout、value等接口提供的一個(gè)比較明確的功能庫(kù)。這些語(yǔ)言級(jí)別的正交的元素,可以組合成非常多樣和豐富的庫(kù)。比如有時(shí)候我們需要等goroutine啟動(dòng),有時(shí)候不用;有時(shí)候甚至不需要管理goroutine,有時(shí)候需要主動(dòng)通知goroutine退出;有時(shí)候我們需要等goroutine出錯(cuò)后處理。
  • 比如序列化TEXT或XML的例子,可以將對(duì)象的邏輯完全和存儲(chǔ)分離,避免對(duì)象的邏輯中隨處可見存儲(chǔ)對(duì)象的代碼,維護(hù)性可以極大的提升。另外,兩個(gè)向量的耦合還可以理解,如果是多個(gè)向量的耦合就難以實(shí)現(xiàn),比如要將對(duì)象序列化為支持注釋的JSON先存儲(chǔ)到網(wǎng)絡(luò)有問(wèn)題再存儲(chǔ)為TEXT文件,同時(shí)如果是程序升級(jí)則存儲(chǔ)為XML文件,這種復(fù)雜的邏輯實(shí)際上需要很靈活的組合,本質(zhì)上就是空間的多個(gè)向量的組合表達(dá)出空間的新向量(新功能)。
  • 當(dāng)對(duì)象出現(xiàn)了自己不該有的特性和方法,會(huì)造成巨大的維護(hù)成本。比如如果TEXT和XML機(jī)制耦合在一起,那么維護(hù)TEXT協(xié)議時(shí),要理解XML的協(xié)議,改動(dòng)TEXT時(shí)竟然造成XML掛掉了。使用時(shí)如果出現(xiàn)自己不用的函數(shù)也是一種壞味道,比如Copy(src, dst io.ReadWriter)就有問(wèn)題,因?yàn)閟rc明顯不會(huì)用到Write而dst不會(huì)用到Read,所以改成Copy(src io.Reader, dst io.Writer)才是合理的。

由此可見,Orthogonal是接口設(shè)計(jì)中非常關(guān)鍵的要素,我們需要從概念上考慮接口,盡量提供正交的接口和函數(shù)。比如io.Reader、io.Writerio.Closer是正交的,因?yàn)橛袝r(shí)候我們需要的新向量是讀寫那么可以使用io.ReadWriter,這實(shí)際上是兩個(gè)接口的組合。

我們?nèi)绾尾拍軐?shí)現(xiàn)Orthogonal的接口呢?特別對(duì)于公共庫(kù),這個(gè)非常關(guān)鍵,直接決定了我們是否能提供好用的庫(kù),還是很爛的不知道怎么用的庫(kù)。有幾個(gè)建議:

  1. 好用的公共庫(kù),使用者可以通過(guò)IDE的提示就知道怎么用,不應(yīng)該提供多個(gè)不同的路徑實(shí)現(xiàn)一個(gè)功能,會(huì)造成很大的困擾。比如Android的通訊錄,超級(jí)多的完全不同的類可以用,實(shí)際上就是非常難用。
  2. 必須要有完善的文檔。完全通過(guò)代碼就能表達(dá)Why和How,是不可能的。就算是Go的標(biāo)準(zhǔn)庫(kù),也是大量的注釋,如果一個(gè)公共庫(kù)沒(méi)有文檔和注釋,會(huì)非常的難用和維護(hù)。
  3. 一定要先寫Example,一定要提供UTest完全覆蓋。沒(méi)有Example的公共庫(kù)是不知道接口設(shè)計(jì)是否合理的,沒(méi)有人有能力直接設(shè)計(jì)一個(gè)合理的庫(kù),只有從使用者角度分析才能知道什么是合理,Example就是使用者角度;標(biāo)準(zhǔn)庫(kù)有大量的Example。UTest也是一種使用,不過(guò)是內(nèi)部使用,也很必要。

如果上面數(shù)學(xué)上有不嚴(yán)謹(jǐn)?shù)恼?qǐng)?jiān)徫?,我?shù)學(xué)很渣。

Links

由于簡(jiǎn)書限制了文章字?jǐn)?shù),只好分成不同章節(jié):

  • Overview 為何Go有時(shí)候也叫Golang?為何要選擇Go作為服務(wù)器開發(fā)的語(yǔ)言?是沖動(dòng)?還是騷動(dòng)?Go的重要里程碑和事件,當(dāng)年吹的那些牛逼,都實(shí)現(xiàn)了哪些?
  • Could Not Recover 君可知,有什么panic是無(wú)法recover的?包括超過(guò)系統(tǒng)線程限制,以及map的競(jìng)爭(zhēng)寫。當(dāng)然一般都能recover,比如Slice越界、nil指針、除零、寫關(guān)閉的chan等。
  • Errors 為什么Go2的草稿3個(gè)有2個(gè)是關(guān)于錯(cuò)誤處理的?好的錯(cuò)誤處理應(yīng)該怎么做?錯(cuò)誤和異常機(jī)制的差別是什么?錯(cuò)誤處理和日志如何配合?
  • Logger 為什么標(biāo)準(zhǔn)庫(kù)的Logger是完全不夠用的?怎么做日志切割和輪轉(zhuǎn)?怎么在混成一坨的服務(wù)器日志中找到某個(gè)連接的日志?甚至連接中的流的日志?怎么做到簡(jiǎn)潔又夠用?
  • Interfaces 什么是面向?qū)ο蟮腟OLID原則?為何Go更符合SOLID?為何接口組合比繼承多態(tài)更具有正交性?Go類型系統(tǒng)如何做到looser, organic, decoupled, independent, and therefore scalable?一般軟件中如果出現(xiàn)數(shù)學(xué),要么真的牛逼要么裝逼。正交性這個(gè)數(shù)學(xué)概念在Go中頻繁出現(xiàn),是神仙還是妖怪?為何接口設(shè)計(jì)要考慮正交性?
  • Modules 如何避免依賴地獄(Dependency Hell)?小小的版本號(hào)為何會(huì)帶來(lái)大災(zāi)難?Go為什么推出了GOPATH、Vendor還要搞module和vgo?新建了16個(gè)倉(cāng)庫(kù)做測(cè)試,碰到了9個(gè)坑,搞清楚了gopath和vendor如何遷移,以及vgo with vendor如何使用(畢竟生產(chǎn)環(huán)境不能每次都去外網(wǎng)下載)。
  • Concurrency & Control 服務(wù)器中的并發(fā)處理難在哪里?為什么說(shuō)Go并發(fā)處理優(yōu)勢(shì)占領(lǐng)了云計(jì)算開發(fā)語(yǔ)言市場(chǎng)?什么是C10K、C10M問(wèn)題?如何管理goroutine的取消、超時(shí)和關(guān)聯(lián)取消?為何Go1.7專門將context放到了標(biāo)準(zhǔn)庫(kù)?context如何使用,以及問(wèn)題在哪里?
  • Engineering Go在工程化上的優(yōu)勢(shì)是什么?為什么說(shuō)Go是一門面向工程的語(yǔ)言?覆蓋率要到多少比較合適?什么叫代碼可測(cè)性?為什么良好的庫(kù)必須先寫Example?
  • Go2 Transition Go2會(huì)像Python3不兼容Python2那樣作嗎?C和C++的語(yǔ)言演進(jìn)可以有什么不同的收獲?Go2怎么思考語(yǔ)言升級(jí)的問(wèn)題?
  • SRS & Others Go在流媒體服務(wù)器中的使用。Go的GC靠譜嗎?Twitter說(shuō)相當(dāng)?shù)目孔V,有圖有真相。為何Go的聲明語(yǔ)法是那樣?C的又是怎樣?是拍的大腿,還是拍的腦袋?
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
禁止轉(zhuǎn)載,如需轉(zhuǎn)載請(qǐng)通過(guò)簡(jiǎn)信或評(píng)論聯(lián)系作者。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容