Go語言中的面向?qū)ο?/h2>

我們用鉆頭,目的不是為了鉆他兩下,而是為了想要一個窟窿眼。

面向?qū)ο笠惨粯?,用OOP只是手段,寫出好維護(hù)的代碼才是目的。

不是為了面向?qū)ο蠖鴱?qiáng)行面向?qū)ο?,是通過吸收面向?qū)ο蟮木A,寫更優(yōu)秀的代碼。

1.面向?qū)ο蟛鸾?/h2>

面向?qū)ο竽芰餍?,因為確實很優(yōu)秀。

  • 可復(fù)用,不用子類多寫代碼,父類方法就能給子類方法復(fù)用。
  • 靈活擴(kuò)展,盡管父類已經(jīng)定義了主體邏輯,但子類可以自由選擇怎么實現(xiàn)。
  • 好維護(hù),符合開閉原則,對添加子類開放,對修改父類關(guān)閉。對子類的改動不用擔(dān)心影響全局(不可能一點(diǎn)都不改吧)。

那go語言跟普通面向?qū)ο笳Z言差異這么大,是怎樣仍然完美擁有這些優(yōu)點(diǎn)呢。

1.1映射

如果把 Java 類拆解到go里,屬性就是struct,方法就是interface。但構(gòu)造方法不在其列。

比如java類可以這樣寫

class Bird{
    private String name;
    public String getName(){ return this.name; }
    public void fly(){}
}

go 里可以這樣寫

type IBird interface{
    fly()
}
type Bird struct{
    IBird
}
func (b *Bird)fly(){}
// 構(gòu)造函數(shù) 返回值類型是interface
func NewBird() IBird{
    return &Bird{}
}

go 語言里,返回值類型是重點(diǎn)。返回值類型不是定義的struct,而是interface。

那么能不能不返回定義的 interface,而是返回定義的 struct呢?

答案是不行。這就涉及到兩種語言對代碼復(fù)用的實現(xiàn)方式。

1.2繼承和組合

在java這類面向?qū)ο蟮恼Z言上,復(fù)用是通過繼承的方式來實現(xiàn)的。

子類繼承父類,子類完全可以代替父類來使用。

class A{
    public void show(){}
}
class B extends A{}

public void letShow(data A){
    data.show();
}

letShow(new B());

上述操作是完全沒問題的,因為 B 也是 A的一種。

但是在go里,上述就行不通了。

go的復(fù)用是通過組合的方式來實現(xiàn)的。沒有父類子類的概念,而是超集的概念。

超集可以執(zhí)行子集的方法,但是不支持作為子集類型被傳入。

type Base struct{}
func (b Base) Show() {}

type Super struct {
    Base
}

func callBase(b Base) {}

super := Super{}
// 可以執(zhí)行子集的方法
super.Show()
// 但不支持作為子集類型被傳入
// Cannot use 'Super{}' (type Super) as type Base 
// callBase(Super{})

所以你來我往大家操作的類型都是 interface。

1.3殊途同歸

但回過頭仔細(xì)想想,一般情況下,Java里所有的屬性都建議設(shè)為private,不對外開放。外部只能調(diào)用方法來處理。跟go里也差不多。

這種機(jī)制在java里只是寫起來有些死板,但是在go里,直接就被定死了,想要靈活,想要復(fù)用就只能返回 interface。

這樣一想,寫java的時候念頭都通達(dá)了。OOP的時候不用再想著和誰干點(diǎn)什么,而是想著找個能干的就行,管他是誰呢。

2.面向?qū)ο髮崙?zhàn)

眾所周知,百聞不如一見,百看不如一干。所以我們以一個線上需求實踐一下。

需求:將多個數(shù)據(jù)源提供的數(shù)據(jù)入庫,各個數(shù)據(jù)源提交來的字段不一樣,但最終落地的數(shù)據(jù)字段是一致的。

2.1 代碼

下面的代碼不是很規(guī)范,用了幾個魔數(shù),類型還用了map。忽略細(xì)節(jié),看本質(zhì)。

真正寫代碼不會有人這樣寫的。

真正寫代碼不會有人這樣寫的。

真正寫代碼不會有人這樣寫的。

dddd

無封裝寫法

func saveData(request map[string]string) {
    dataToSave := ""
    switch request["version"] {
    case "source1":
        dataToSave = extractFromSource1(request)
    case "source2":
        dataToSave = extractFromSource2(request)
    }
    if dataToSave != "" {
        Save(dataToSave)
    }
}

簡單封裝寫法

type IExtract interface {
    Extract(request map[string]string) *model.data
}
type AbstractExtractor struct{
    IExtract
}
type Extractor1 struct {
    AbstractExtractor
}
type Extractor2 struct {
    AbstractExtractor
}

func GetExtractor(request map[string]string) IExtract {
    switch(request["version"]) {
    case "source1":
        return &Extractor1{}
    case "source2":
        return &Extractor2{}
    }
    return nil
}

func saveData(request map[string]string) {
    extractor := GetExtractor(request)
    if extractor == nil{
        return
    }
    Save(extractor.Extract(request))
}

其實還可以封裝得再給力一點(diǎn),比如

  • 分到不同的文件,改動一個邏輯的時候盡量不影響其他邏輯。
  • 干掉那個Switch,讓他自己動(反射、map或者init)。

后面有機(jī)會再說。

2.2分析

封裝了,代碼反倒更長了。

所謂一寸長,一寸強(qiáng),有誰會拒絕更長的呢。

復(fù)用性:只要在 AbstractExtractor 名下定義的方法, Extractor1 和 Extractor2都能調(diào)用。

靈活擴(kuò)展:如果要增加一種數(shù)據(jù)源,可以采用近似于新加子類的方式操作

好維護(hù):假如 Extractor2 和 Extractor1 某個地方不一樣,自己改自己的就行了,不用擔(dān)心影響全局。

上面不就是一個典型的工廠模式嗎

2.3拓展

那么好好的面向?qū)ο笤趺床荒苡昧?,就算用了?jīng)典的面向?qū)ο?,現(xiàn)有的特性應(yīng)該也可以完全保留。

好端端的,為什么非要用這種方式拆開呢?

業(yè)務(wù)還沒寫好,就不想這種終極問題了。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

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

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