我們用鉆頭,目的不是為了鉆他兩下,而是為了想要一個窟窿眼。
面向?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ù)還沒寫好,就不想這種終極問題了。