Pulpcode

捕获,搅碎,拼接,吞咽

0%

对象与对象的关系

回顾

在我上学的时候,一聊到设计模式都是很有格调的话题,仿佛设计模式就是解决软件开发的精髓,但是很长一段时间,我从骨子里是排斥设计模式的,因为我觉得自己连面向对象的知识都没有领悟,谈论设计模式还为时尚早,我尤其排斥那本被吹到爆炸的《大话设计模式》,那本设计模式看上去给你用形象的比喻讲明白了一个东西,但实际上你真正开始写代码的时候,发现自己什么都写不出来。

这篇博客是想退一步思考,总结一些面向对象的知识,比如思考“对象与对象的关系”到底是怎样的。

和大部分人一样,初学面向对象的时候,都会铺天盖地的听到一些词汇,比如“面向对象的三大特性是继承,封装,多态。”,再比如一切都是对象。但如果你仅仅记住这几句话,真的很难领悟面向对象这件事。我记得自己真正对面向对象有过一次深入体会是在读完《c++编程思想》,这本书的第一章是关于介绍oop的,作者通过一个面向过程的c代码到c++的oop书写方式,引出了面向对象的一些精髓,比如面向对象其实是一种思想,早期在用c的时候,也可以用面向对象的思路去写(struct+函数),但这种代码是单向绑定的,支持面向对象的代码的好处是完成方法与数据的双向绑定。而不像struct那样,不知道谁使用了此数据结构。

这些关于封装的知识理论上被称为基于对象,但说句实在的,对于很多人而言,能在系统中用封装的思想将系统建模就已经很不容易了,这可能听上去有点夸张,但仔细想想,对于一个人,一个苹果,一张订单让你去建模,你可能很容易就建了,但如果是类似Controller,Action,Execute这些看上去像是动作的对象呢?

再比如一些复杂的系统,如何抽象出领域模型,让系统能够清晰的交互,这些都不是那么容易的。再说继承与多态,其实继承本身和多态不像是处于同一平行线的概念。这个继承,在java中其实分继承接口和继承类,而继承类则主要是为了代码复用,只不过这种复用是静态的,而多态更像是动态的复用。

在c++中没有这么分类,c++提供的oop看上去更像是为数学公式准备的,所有只有继承类这么一说,只不过你定义一个只有虚函数的类,这个类就成接口了。而且c++的继承还包括公有继承,保护继承,私有继承等等继承权限,还有友元和多重继承。这些东西不是没用,而是一般人用不来,所以在java中都选择了精简。

在说回面向对象本身,这种编程思想说来说去都可以总结为以下几点。

  1. 所有数据都应该隐藏在它所有的类内部。
  2. 类的使用者必须依赖类的公有接口,但类不能依赖它的使用者。
  3. 不要把实现细节放到类的公有接口中。
  4. 面向对象的方法签名,其实算是消息协议,面向对象的本质其实是对象与对象之间发送消息。

完成了上面关于oop的回顾介绍之后,我就要引入这篇博客的主要内容了,对象与对象之间的关系。

使用关系

简单的来说,我这样理解面向对象,每个对象封装了自己的状态和协议,而一个对象总想使用另一个对象。总结出一个对象如何使用另一个对象的方式,就基本归纳了面向对象建模的所有场景。

我很喜欢拿汽车和加油站的关系来举这个例子,这个例子是我从《OOD启示录》这本书上读到的。

汽车要如何使用加油站呢?

首先它可以作为参数传递给加油站:

1
2
3
4
Class Car{

void get_gasoline(GasStation gasStation);
}

或者它作为Car的一个属性,虽然啊这看上去有点怪,好像这个车只能去指定地点加油。

1
2
3
4
5
6
7
8
Class Car{
private GasStation gasStation;

void get_gasoline(){
gasStation...
}

}

不过更怪的是土豪模式,直接new一个加油站出来。

1
2
3
4
5
6
Class Car{
void get_gasoline(){
GasStation gasStation = new GasStation();
}

}

还有一种退一步的方式,是从另一个地图对象来获得加油站对象,不过这又引出了另一个问题,这个地图对象是从哪来的?

1
2
3
4
5
Class Car{
void get_gasoline(){
GasStation gasStation = map.getGasStation();
}
}

以上的这些都是一个对象想使用另一个对象的方式,我要特别提醒一下那个作为一个属性的方式,同样是定义一个类的属性,比如一个person有一个name属性,那就是它拥有一个属性,算包含关系,而如果是一个person有一个country属性,这个时候就是关联关系了,就像是数据库里面的关联字段一样。不过更官方的叫法,其实将对象与对象的关系分为四种:依赖关系(Dependency),关联关系(Association),聚合关系(Aggregation),组合关系(Compostion)。其中把作为局部变量,方法参数,或者调用一个类的静态方法称为依赖关系。而关联关系,聚合关系,组合关系,都用成员变量的方式表现,但是它们所表达的对象声明周期和耦合度是不一样的。

继承

除了使用一个对象,还可以通过继承的方式来获得这份“代码”,不过汽车继承一个加油站看上去太奇怪了。前面提到了,多态是动态的获得,而继承更像是静态的获得,就好像编译器把这份代码给你拷贝过去了一样。设计模式的书常常把继承说的一文不值,说“用组合不用继承”,但如果两个类真有某种父子关系,使用组合而不是继承看上去是很怪的,而且维护性也很差,比如你子类继承了你的父类,那只要给父类添加一个方法,那子类是默认就获得了,但是如果你使用了组合,你就不能方便的拥有这些方法,要一个个的子类去引用这个新的实现。

多重继承

提到继承就不得不提到多重继承,这个东西在java这种语言中被禁用了,很多人说java可以实现多个接口,但是接口和继承类完全不是一个概念,继承类是重用,那接口有能啥重用的地方?充其量是定义协议而已。多重继承这个思想本身没有问题,比如一个木头门继承于门和木头两个父类。这样任何一个父类添加一个新的属性或方法,都会默认被木头门继承,这是很自然的建模。而一个木头门有两个属性分别是门和木头,这个就很诡异。多重继承比较被人抨击的主要是那种钻石继承,父类之间签名相同之类的问题,不过有些语言会提供“限制版”的多重继承,来既让你使用多重继承,又不会提升代码的复杂度。比如Mixin(混入类)。

元类

最后在说说元类,这个概念来源于元编程,什么是元编程,就是编写生成代码的代码。比如DSL,比如c++中的模板,元类呢就算是用来创建类的类。这个我在python中偶尔会使用一下。一个简单的入门是你可以用type来创建一个类对象(type就是元类)。

1
Foo = type("Foo", (object,), {"hello": hello})

不过大多数的使用场景是设置类的__metaclass__属性,而在创建这个类的时候,扩展这个类。在python的orm中会遇到这种设计。

比如你定义了这样一个model

1
2
3
4
5
6
7
class Person(Base):
# 表的名字:
__tablename__ = 'person'

# 表的结构:
id = Column(String(20), primary_key=True)
name = Column(String(20))

但是你却像想使用正常属性一样使用它们。

1
2
3
4
person = session.query(Person).filter(User.id=='5').one()
# 打印类型和对象的name属性:
print 'type:', type(person)
print 'name:', person.name

其原因就在于Model的Metaclass中定义了在创建这个类的时候,如何载入这些类属性,并“生成”你想要的那种请求方式。