从代码到模式(一) 对象的构建和销毁

前段时间有一点空闲,重读了GoF的经典。GoF的书之所以经典,不仅仅是在于其整理的23个模式,而是整体叙述的字字珠玑,特别是前几章。我常常和别人举的例子就是第一章开篇的几句,简直道尽了OO设计的精髓。DP说:

You must find pertinent objects, factor them into classes at the right granularity, define class interfaces and inheritance hierarchies, and establish key relationships among them. Your design should be specific to the problem at hand but also general enough to address future problems and requirements. You also want to avoid redesign, or at least minimize it

“寻找合适的对象”,“合适的粒度”,“接口”,“继承层次”,“关联关系”,“专用的接口且为将来预留扩展的余地”,等等等等,所有这些都是OO设计中每天都要面对的问题。尽管没有一致的策略告诉我们在不同的情况下应该如何决断,业界仍然总结出一些基本原理和范式,那就是设计模式。经过这十几年的发展,我们有了一个模式大观:既有通用的模式,又有专业领域的模式,如分布式模式(POSA 4th),企业应用模式(Martin)等。不过,这里要说的是另一个问题。

 

当初学习模式的时候,最大的困难是如何把模式和日常的工作联系起来。要知道,GoF的23个模式不是自己想出来的,而是从现有应用中提炼出来的,这就意味着我们日常编写的代码一定也在解决那些模式中某一个正在解决的问题,我们的任务就是提炼出来,使之规范化。基于这个想法,我决定把模式和实际的代码联系起来,从宏观的角度看看模式究竟解决了那些OO设计中面临的问题。这个过程中,我逐渐认识到模式仅仅是开发生态环境的一个必要部分而已。我们的问题部分被OO设计理念处理,部分被合适的工具处理,还有部分在被不同的设计模式处理。到此,模式的含义已经其使用就很明显了。

 

另一个当初的问题是,模式是如何组织的?GoF把模式分为创建型,行为型以及结构型。你可能问为什么没有销毁型?其实这是很好的问题。可以想象,到处模式出来的时候,整体上的整理视图还未清晰,所以还不能有机地把模式使用简单的二维表的形式表达(条件为行,该使用的模式为列)。GoF推荐的做法是查找模式的意图部分,然后匹配。这很好,但是还是不够直观啊。

 

本文假设你粗读过GoF的Design Pattern一书或者head first系列中关于设计模式的那本。

 

先来界定一些实现的基本概念。

 

OO的最最基本的思想是使用类来表达概念。我们这里要区分一下类和型的概念。型是指相关的接口定义的集合,对应于C++的纯虚类;而类则是型的实现,对应于C++的非纯虚类。对象是类的实例,一个对象只能属于一个类,但是可以有多个型(以多继承的方式实现多个接口集合)。

 

对象是OO设计中的基本单位,我们可以给对象发送消息。这对应于语言,架构以及应用实现的可能性非常广。按照消息的传递方式,可以粗分为:

调用静态的成员函数(以C/C++为例的大部分语言,类接口使用硬编码的成员函数名字,其消息定义则由成员函数的函数签名决定)

发送一个消息(Windows GUI,有专门定义的何种消息码,消息定义则是通用的LPARAM和PPARAM结构)

调用动态的方法(如JavaScript等动态语言的实现。消息实现为一个名字到实现的动态映射)

 

按照消息发送方式是否直接和消息接收方关联,可以分为:

直接耦合。消息发送者直接与消息接收者耦合,大部分的静态语言都是如此

间接耦合。消息仅仅发送给某种中间机制,有这个中间机制负责消息的路由投递以及结果回传。面向消息的中间件,大部分的分布式架构都是如此

 

按照消息结果的返回形式的不同,可以分为:

同步消息。直接耦合的大部分是同步消息;

异步消息。间接耦合的则大部分是异步的。

 

其他的可能分类还包括显式或者隐式消息发送者等等。

 

这里仔细地列出了不同的OO消息机制的表示是为了强调一点:我们不能为具体的OO实现机制,如某一门具体的语言,所限制,而应该在一个更广阔的范围内检查设计模式对OO的支持。有经验的OO设计开发人员其实可以从上述的分类中可以看出一些模式就是直接支持某一种不同的消息传递方式的。

 

和GoF的书类似,下面的例子使用C++表达。

 

创建型模式。

 

我们如何创建的一个对象呢?

对于单个对象,我们可以直接创建,无论是使用new操作符创建基于堆的对象,或者使用局部变量的方式创建基于栈的对象。如果仅仅需要一个,那就是singleton模式要解决的问题了。

通过这两种方法使用对象,我们暗含了使用了硬编码。注意到了吗,直接使用类的名字就是硬编码啊。OO的基本原则就是针对接口编程,而不是针对实现编程。类是典型的实现,而其背后的型才是接口(尽管很对时候,类和型是结合在一起的)。简而言之,我们关注的使用对象的型,而不是类。由此可见,我们可以使用如下方法创建对象:

interface some_type; 

some_type* create_object_of_type();

 

型是依附于类存在的,假设我们实现如下类:

class some_class : public some_type {...};

 

上述接口就可以实现为:

some_type* create_object_of_type() { return new some_class(); }

 

这是什么呢?Factory Method模式

 

对于更复杂一点的类,其表达和实现更为灵活。其中一种情况是所有的复杂性相关的信息都可以在编译时确定。例如针对不同类型容器类等。没错,这就是C++ template技术的用武之地!在C中,我们只能使用generic指针(void指针)。这是编译器提供的便利。无需任何设计上的付出。

 

对于不支持类型反射的语言,运行时获取类型信息要么不可能就是非常在那个困难。这样使得根据一个已存在对象的类型信息来创建新的对象非常困难,如C++。既然语言层次不能支持这样的功能,我们就要手工实现。一个最简单的办法就是让需要支持这样需要的类实现一个用于返回特定型的接口,可以从其中返回一个保证能够实现特定型的对象。例如:

class derived_type : public base_type {

    base_type* clone() { return new derived_type(*this); }

};

注意,我们针对的是接口编程,亦即base_type编程,而不是实现,所以这里直接返回型的指针。当然你也可以返回类的指针,这取决于你的需要。

这是什么呢?Prototype模式

 

更复杂的类往往是使用对象组合的结果。有些时候,这些字对象并不是相互无关的,而是相互依赖。那么我们能想到的最简单的办法是在定义类的时候,按照想换的依赖关系一次声明各个对象,确保所有被依赖的对象在依赖对象前构造。C++编译器保证了字对象的构造是按照类定义中的声明顺序构造。这样做在依赖关系相对较小的情况下还行,对于复杂的依赖关系,如循环依赖则毫无办法。另一方面,依赖于编译器而引入的隐式约定会使得程序的逻辑难以理解,所以我们一般不建议依赖这个。那么怎么办呢,我们只能在构造对象的时候自己处理依赖关系。

这里是一个例子:

A* create_A()

{

    B* b = new B();

    C* c = new C(b);

    b.initialize_A(c);

    return new A(b, c);

}

这个,我们强制指定了构造对象A需要的各种步骤。如果其中各步在另一个context中还要用到,那无可避免的就有了代码重复,例如:

 

D* create_D()

{

    B* b = new B();

    C* c = new C(b);

    b.initialize_D(c);

    return new D(b, c);

}

消除这种重复的办法就是抽象这些步骤为另一个对象的方法,饭后一次调用该对象的方法来构造出最终的对象。这种构造复杂对象的方法似乎不常用,然而在分布式计算环境中,因为有远程对象的构建和依赖管理任务,这样的代码非常常见。你可能已经猜出来了,就是builder模式,而其中的辅助类在GoF的书中叫做director。

 

对于多个对象,如果是同类对象,我们常用的是对象数组以及new[]操作符。对于异类但是又相关的对象,一种特殊的情况是有多个并行的类似对象集,而且它们遵循同样的接口继承集合。这是一种非常特别的情况,GoF使用Abstract Factory来描述。在我们的归类中,尽管不能自然地推出来,但是理解起来也是相对自然的。

 

对象怎么销毁的呢?

GoF只所以没有提及销毁型模式,可能是因为开发工具链提供了必要的构造。如基于作用域的自动销毁,确定性,非确定性的gc等等,这并不意味这个并不存在广泛使用的销毁型模式。特别是对于资源型对象的管理,对象的销毁甚至比其构造更加难以控制。

 

首先,编译器的支持是基础。对于C/C++来说,基于栈的对象可以保证自动销毁。基于堆的动态对象,则需要手工释放。这是无数编程错误之源。java等语言通过提供gc的支持来部分的简化了这些工作。

 

其次,手工的实现模式同样广泛存在。对于C++来说,最常见的资源管理技术就是耳熟能详的RAII。这个技术的基础就是C++完善的基于作用域的对象控制机制。如果希望一个对象可以看作值对象来传递,那么最常见的实现是基于引用计数的的对象复制技术在加上COW的支持。这样的技术子诸如COM,CORBA等架构中被广泛使用。

 

对于那些构造和销毁开销很大的对象,往往使用对象池的方式来管理对象的使用,如线程池。对于轻量级的工作线程,创建和销毁一个线程所需要的开销甚至大于线程执行的开销,所以一般使用线程池来维护一个可用的线程列表。对于这样的对象,仅仅在整个应用退出的时候才涉及到对象的销毁。

 

 

  • 0
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值