设计和过度设计是一个永恒的话题
想去饭馆吃个饭,要先出门,左拐,拿单车,解锁,上车,前进,遇到一间饭馆,停车,上锁,进门,点菜,吃,付钱,原路返回。这是面向过程编程。
想去饭馆吃个饭,需要一辆单车来代步,需要一个钱包来付钱,还需要挑一间不错的饭馆来吃。这是面向对象编程的封装。
想去饭馆吃个饭,需要个代步工具,代步工具是车。单车是车,汽车也是车,小电摩也是可以,它们都有个轮子可以跑,都能让你不用走路。还需要一个钱包,功能是给钱,钱包可以是真皮包,可以是假皮包,还可以是支付宝,微信,当然裤袋也不是不行,只要里面有钱,就行。最后需要个饭馆,饭馆可以是大排档,可以是大酒楼,只要里面有东西可以吃。一个物体衍生自另一个更基本的物体,这是面向对象编程的继承。
想去饭馆吃个饭,你需要一个东西,这个东西能载人,能跑。还需要一个东西,唯一的功能是能给钱。最后一个东西功能是能给饭吃。给你辆单车,一个真皮钱包,一间猪脚饭的地址,ok,这没问题,你要的功能都全了,我还可以把单车换成汽车,或者换成卡车都行,钱包也是一样,任意换品种,只要能给钱。系统功能是一系列行为的总和,而行为动态的取决于具体的执行对象。这是面向对象编程的多态。
上述的设计是经典的而且有效的,因为遵循了一个开闭原则,对于扩展是开放的,对于修改是关闭的。你可以发明其他类型的车,只要它还是能载人能跑,就能替换任何用到车的地方。但是不可以对单车进行更改,不能将两个轮子换成三个轮子 ,不能将人力换成电动。一个是为了应对需求的变更,一个是为了保护之前的工作不被破坏。
那怎样才是过度设计呢?将车进一步解耦封装,车是由轮子,发动机,座位,车身等等组成,每个组件都给定义个接口,然后实现接口,然后继承到更大的组件,最后成为车。车还要有一堆对外的接口,加油的,充电的,还要加气的以备扩展等等。对于只想吃个饭的人来说,这是过度设计了,而对于造车的人,这确是合理的设计。所以,良好的设计和过度的设计都是相对的,不能一概而论,然而,受不良书籍和不良大学课堂的影响,滥用面向对象导致过度设计的项目却到处可见,尤其是由Java写的项目 : )
相信或不相信程序员是另一个永恒的话题
C语言选择相信,Java选择不相信,C++让你自己选择相信自己或不相信自己,动态语言如Python则选择放纵程序员。
在C里面,你可以做任何你想做的事情,也要承受任何将导致的后果。你完全可以自己实现一套封装,继承,多态,只要你相信自己并且相信你的队友。
在Java里面,除了要确认你自己不值得信任外,还要明白你的队友也是不可信任的。你要写出这样的代码:即使你队友是猪,也不会写出危害系统运行的代码(因为即使有,也会编译不通过不让有运行的机会)。基于此目的,你要做的工作成倍增加,解耦解耦,封装封装,class到处都是。也因为此,Java天生适合有猪队友参与的大项目。
其中的区别就在,语言本身语法对你限制的多少和编译器为你做的工作的多少。
语言不只是一个工具,它会影响你的思维
语言确实是一门工具,却不只是一门工具。语言本身时时刻刻在影响着你的项目设计和代码设计。
我想说的观点是,不要陷进一门语言里,以为全世界的代码都应该按一种方式来写。
我喜欢Golang的一个点,就是它提供了另外一种思考的方式。
Golang的方法
"Although there is no universally accepted definition of object-oriented programming, for our purposes, an object is simply a
value or variable that has methods, and a method is a function assiociated with a particular type."
Golang没有提供像class这样的声明方式,而是为类型提供方法的声明来实现面向对象的。
举个例子:
package main
import "fmt"
type Vehicle struct {
Name string
Seats int
}
func (v Vehicle) Run {
fmt.Println("i am running")
}
func main() {
v := Vehicle{"bicycle",1}
v.Run()
}
方法的声明比函数的声明多了个接受者(receiver): (v Vehicle)
接收者有两种形式,一种是(t T),另外一种是(t *T)。不难理解,前一种是只能引用值,后一种才能改变值。
关于接受者用哪种方式,记得这两条就好了:
类型 *T的可调用方法集包含接受者为 *T或T的所有方法集
类型T的可调用方法集包含接受者为T的所有方法
Golang的接口
沿着上面的例子
type Transportation interface {
Run()
}
func GoToEatBy(tr Transportation) {
tr.Run()
}
func main(){
v := Vehicle{"bicycle",1}
GoToEatBy(tr)
}
我们声明了一个Transportation接口,这个接口要求一个Run方法,而函数GoToEatBy需要一个实现了这个接口的对象,而不管这个对象究竟是什么类型,只要它实现了这个接口就可以。
You don't need to know what it is, but you know what it can do.
灵活运用上述两种方式,写出面向对象的代码不在话下。结构体和方法提供了封装和继承(通过在一个struct/interface嵌入其他struct/interface来实现)的功能,接口提供了多态的功能。虽然都是在实现OOP,但是和C++/Java写出来的实现却会很不一样。所以,代码,不是只有一种写法。Golang提供的方式,简洁却不简单。
不同的思维方式
有没有发现?我们是实现Vehicle的Run在先,后定义的Transportation接口。Vehicle的实现者甚至都不用知道有朝一日它被用于需要Transportation的地方,这就是被Golang发明者津津乐道之处。又要拿Java举例子了,在Java中,对接口的实现是要显示声明的,你要明明白白地说明,我现在要实现这个接口了,当你要为一个类添加对一个接口的实现的时候,你就不得不去修改源码,这就破坏了开闭原则。而在Golang中,你要为一个类型实现一个新的接口,你压根不用管原先的代码,你只要为其添加必须的方法,然后就可以了,是不是很自由?
如果让你来设计语言,你要怎么选择呢,一个类实现一个接口,究竟需不需要在类中显示的去声明,明明白白告诉编译器我知道我在做什么?
回答这个问题要先回答开头的两个大问题:
1,你希望程序员怎样用你的语言来设计代码。
2,你相不相信这些用你语言的程序员。
借用温赵轮中的老赵(他明确反对Golang的接口)举过的一个例子来说明相信不相信程序员的区别。
interface IPainter {
void Draw();
}
interface ICowBoy {
void Draw();
}
在英语中Draw同时具有“画画”和“拔枪”的含义,因此对于画家(Painter)和牛仔(Cow Boy)都可以有Draw这个行为,
但是两者的含义截然不同。假如我们实现了一个“小明”类型,他明明只是一个画家,但是我们却让他去跟其他牛仔决斗,这样
就等于让他去送死嘛。
简单地说,假如接口不能保证行为特征,则“面向接口编程”没有意义。
老赵的观点是,不能只从表面去理解一个接口,还要关注这个接口规定了的每个行为的“特征”。不然,会产生很多误用。
这其实就归结到,程序员他究竟知不知道他在干什么?如果他睡迷糊了用"小明"类型去决斗,Java的编译器会阻止他,而Golang不会。一个牺牲了灵活性,一个牺牲了安全性。
当然,我选择简洁的方式,有猪队友参与的时候例外。
原文转自谢培阳的博客