Pulpcode

捕获,搅碎,拼接,吞咽

0%

从python到java之开始设计模式

Starting

最近要写一个系列的博客,总结设计模式。

首先我想展示我对设计模式的理解,它用来解决一个什么样的问题,同时它有哪些不足,还有我想表达一种思路就是作为支持面向对象的语言,是如何实现设计模式的。最后我想从python和java这两种语言比较设计模式的实现方式来说明这两种语言的特点。

这第一篇博客我要写一个总的概括,写一些我对设计模式的理解,后续的博客会详细讲解每一种设计模式,以及用python和java两种语言的实现方式。

设计模式并不是为面向对象服务的

我们常说的设计模式,其实是说的《Design Patterns: Elements of Reusable Object-Oriented Software》里面提到的23种设计模式,但实际上这些模式的思路在没有面向对象编程的时候,大多就已经有了,很多模式你可以从UNIX的设计思路上找到(参见《unix编程艺术》)。
这本书只能说是从面向对象的角度去实现设计模式。还有很多设计模式很常用,但是并没有出现在这23种,比如说我们常使用的表驱动模式(因为表驱动和面向对象无关)。

面向对象的局限性

面向对象比较适合和业务相关和界面相关的编程,这种思路多和对现实世界的理解挂钩,但是对于一些偏于计算,偏于数学的编程,就不适合了(比如数据处理)。我觉得面向对象的优势就是建模,(偏于现实世界的建模),这也就是我认为除了跟界面和游戏有关的编程,大多数我知道的领域使用oop都感觉很别扭。

封装,继承,多态

我们常说的面向对象的三大特性是,“封装,继承,多态。”

实际上封装这个算是基础,主要思想就是不直接暴露一个类的属性,只能通过它暴露的方法使用它,这点要比c的struct搭配一堆函数强,首先你要来回在函数间传递这个对象,还有你可以直接操纵结构体。
因此你根本不能建立依赖,就是说数据是单向绑定的,你也许知道这个函数依赖于哪个struct,但你并不知道这个struct被哪些函数所依赖。

所以这就是封装的好处,它不仅仅限制了访问,更重要的是建立了数据与方法的依赖。从这点也可以看出,面相对象是一种思想,早在c中,就有这样的思路了,只不过在天生支持面向对象的语言中写起来更容易。

再说说继承,其实这里说的继承,应该包括两部分,抽象类的继承和接口的继承,类的继承主要是提供代码的重用,但是它提供的代码重用不够灵活,是类级别的,或者有些人喜欢称它为静态重用,也就是说你要重用代码必须要继承基类。所以设计模式很大的篇幅都在提到,利用组合的方式来重用代码而不是类继承的方式。

在说说接口,在《设计模式》这本书中,把接口说成是一个类型。一个类实现了这个接口,就说明它具有此类型。所以实现接口就是为了具有此类型的特性。很多书中也在提到,继承类和继承接口是不同的。在c++中它们是杂糅在一起的,之后的oop语言都对其严格的区分,比如java:extends,implements。我理解接口就是静态语言为了静态类型检查而加入的设计。比如像python,ruby这样的动态语言就不需要去实现一个接口。还有就是接口没有任何重用的地方,不要为了灵活把可以用继承的都用接口,那你的代码什么都没有自然灵活了。

然后就是面向对象的第三个特性,也就是多态了,可以说oop的强大很大一部分是就是因为多态,很多人的观点甚至是封装和继承都是为多态服务的。我记得学c++的时候,常常把多态比喻成运行时你才知道指针上的对象是什么。所以很多时候,我也在思考,其实继承和多态,虽然常常被一起提出来,但完全不是一条线上的东西,一个是静态的,一个是动态的。

面向对象的几个设计思路

对象与对象之间的使用关系

对象与对象之间的使用思路无非就那么几种。我们用一个“人与餐厅”来举例子。

1
2
3
4
5
6
7
class Man{

};

class Restaurant{

};

人要去餐厅吃饭,那么它有如下几种方式和这个餐厅交互。

// 作为一个属性
// 但是人有必要有一个属性叫餐厅么?
// 也许是想全局bing一样,一开始就被初始化好了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class Man{
private Restaurant restaurant;

public eat(){
this.restaurnt.food();
......
}

}

// 作为方法参数
// 如果经常使用,每次都这么传递是很烦人的。
class Man{
public eat(Restaurant restaurant){
restaurnt.food();
......
}
}

// 通过另一个类获得
// 这是逃避问题,那你的另一个类又是从哪来的呢?
class Man{
public eat(Map map){
Restaurant restaurant = map.getRestaurant();
}
}

// 当场创建
// 要吃饭就当场创建一个,这肯定是土豪。

class Man{
public eat(){
Restaurant restaurant = new Restaurant();
restaurant.food();
}
}

烂设计

很多人即使开始面相对象编程,它们的很多做法依旧是struct配函数,在java这些必须要强制声明类的还好,python中这样的写法简直满天飞,数据结构在这些函数中到处传递,这个做法的坏处就是你没有建立双向绑定,仅仅是单向的依赖。而且某个函数可能偷偷的修改了你对象的状态但你却一无所知,或者说很难发现。

就拿排序而言,python中提供两种排序功能:

1
2
li.sort()
sorted(li)

第一种排序就是面相对象的风格,第二种就是我说的面相过程的思路,说道这里我就不得不提,我觉得java的排序功能并不是完全意义上的面向对象,或者这种面向对象很蹩脚,也许是我想多了。

1
Collections.sort(list);

还有我认为的一个最烂的设计就是过度设计,玩面向对象和设计模式的那群人,很喜欢把所有if else和switch都搞成可扩展的接口。实际上你只要把该封装的逻辑封装好。到了抉择的那一步,你用if还是switch,还是做一个表驱动还是完全靠外部的输入,都无所谓了,要改也改不了几行代码,这个我们在下一篇介绍几个简单的模式,你就明白了,你的模式,根本没有解决如何抉择的问题。

从python到java的oop比较

关键字

相比于c++,java的继承方式简单一些,没有什么(公有继承,受保护继承,私有继承)。

java的属性,方法倒是有公有(public),受保护(protected),私有的(private)。对于public表明该数据成员、成员函数是对所有用户开放的,所有用户都可以直接进行调用。
对于private,除了class自己,任何方式都不能使用(即使是继承的基类)。而protected,作为对象的使用时,像个私有成员,但是在继承的时候,又可以被子类访问。

java用abstract来标识抽象类或抽象方法,抽象类表示这个类只能被继承,不能够用来创建实例。抽象方法表示这些方法需要在子类中去实现。

java使用extends来继承一个类,使用implements来实现一个接口。

除了上述,java使用final来表明一个值不能被修改,或者方法不能被覆盖(重写),或者类不能被继承。还有java使用static,来标识一个方法是类方法,可以不用创建对象就使用(通过类)。

一个Java源文件中最多只能有一个public类,当有一个public类时,源文件名必须与之一致,否则无法编译。

以上这些特性都会影响我们设计模式java版本的实现方式。

相比于java,python作为动态语言,就更简单了。

在python中,类的一切属性和方法都是默认公有的,也就是说你可以自用使用。

有一种技巧是把属性定义成“__双下划线”开头的方式,但是这只不过是python内部把这种命名的变量替换成另外一种了,如果你想访问,还是可以通过某种方式访问。

python想要某种类型不需要实现接口,因为动态语言的鸭子类型,所以你只需要有那个方法就可以使用了,否则在运行时就会报错,因为python并没有静态类型检查。
当然有的人为了让某些问题能够更早的暴露出来,使用如下方式来实现一个接口,或者抽象类。

1
2
def me(self):
raise raise NotImplimentedError()

python对象的方法,都是被绑定上去的。所以所有的方法,第一个参数都是self。

python使用@staticmethod装饰器来表示一个方法是静态方法,用@classmethod装饰器来表示一个方法是类方法,之所以会区分两种,是因为python万物都是对象,类也可以作为对象做一些操作。
所以对于一个classmethod,他的第一个参数必须要是cls。

还有要提到的一点是因为python更加灵活,支持函数式编程,元编程等等,让我们在实现设计模式的时候,可以有更多的思路,而不像java那么麻烦。

其它的一些特性,我会在后面的特性中一一介绍。