我们用钻头,目的不是为了钻他两下,而是为了想要一个窟窿眼。
面向对象也一样,用OOP只是手段,写出好维护的代码才是目的。
不是为了面向对象而强行面向对象,是通过吸收面向对象的精华,写更优秀的代码。
1.面向对象拆解
面向对象能流行,因为确实很优秀。
- 可复用,不用子类多写代码,父类方法就能给子类方法复用。
- 灵活扩展,尽管父类已经定义了主体逻辑,但子类可以自由选择怎么实现。
- 好维护,符合开闭原则,对添加子类开放,对修改父类关闭。对子类的改动不用担心影响全局(不可能一点都不改吧)。
那go语言跟普通面向对象语言差异这么大,是怎样仍然完美拥有这些优点呢。
1.1映射
如果把 Java 类拆解到go里,属性就是struct,方法就是interface。但构造方法不在其列。
比如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(){}
// 构造函数 返回值类型是interface
func NewBird() IBird{
return &Bird{}
}
go 语言里,返回值类型是重点。返回值类型不是定义的struct,而是interface。
那么能不能不返回定义的 interface,而是返回定义的 struct呢?
答案是不行。这就涉及到两种语言对代码复用的实现方式。
1.2继承和组合
在java这类面向对象的语言上,复用是通过继承的方式来实现的。
子类继承父类,子类完全可以代替父类来使用。
class A{
public void show(){}
}
class B extends A{}
public void letShow(data A){
data.show();
}
letShow(new B());
上述操作是完全没问题的,因为 B 也是 A的一种。
但是在go里,上述就行不通了。
go的复用是通过组合的方式来实现的。没有父类子类的概念,而是超集的概念。
超集可以执行子集的方法,但是不支持作为子集类型被传入。
type Base struct{}
func (b Base) Show() {}
type Super struct {
Base
}
func callBase(b Base) {}
super := Super{}
// 可以执行子集的方法
super.Show()
// 但不支持作为子集类型被传入
// Cannot use 'Super{}' (type Super) as type Base
// callBase(Super{})
所以你来我往大家操作的类型都是 interface。
1.3殊途同归
但回过头仔细想想,一般情况下,Java里所有的属性都建议设为private,不对外开放。外部只能调用方法来处理。跟go里也差不多。
这种机制在java里只是写起来有些死板,但是在go里,直接就被定死了,想要灵活,想要复用就只能返回 interface。
这样一想,写java的时候念头都通达了。OOP的时候不用再想着和谁干点什么,而是想着找个能干的就行,管他是谁呢。
2.面向对象实战
众所周知,百闻不如一见,百看不如一干。所以我们以一个线上需求实践一下。
需求:将多个数据源提供的数据入库,各个数据源提交来的字段不一样,但最终落地的数据字段是一致的。
2.1 代码
下面的代码不是很规范,用了几个魔数,类型还用了map。忽略细节,看本质。
真正写代码不会有人这样写的。
真正写代码不会有人这样写的。
真正写代码不会有人这样写的。
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))
}
其实还可以封装得再给力一点,比如
- 分到不同的文件,改动一个逻辑的时候尽量不影响其他逻辑。
- 干掉那个Switch,让他自己动(反射、map或者init)。
后面有机会再说。
2.2分析
封装了,代码反倒更长了。
所谓一寸长,一寸强,有谁会拒绝更长的呢。
复用性:只要在 AbstractExtractor 名下定义的方法, Extractor1 和 Extractor2都能调用。
灵活扩展:如果要增加一种数据源,可以采用近似于新加子类的方式操作
好维护:假如 Extractor2 和 Extractor1 某个地方不一样,自己改自己的就行了,不用担心影响全局。
上面不就是一个典型的工厂模式吗
2.3拓展
那么好好的面向对象怎么不能用了,就算用了经典的面向对象,现有的特性应该也可以完全保留。
好端端的,为什么非要用这种方式拆开呢?
业务还没写好,就不想这种终极问题了。