您的位置:首页 > 编程语言

从代码到模式(二) 对象的结构和表现

2010-08-12 13:31 218 查看
上面一文中,我们描述了对象的创建和销毁。从试图去掉对象初始化过程中的硬编码开始,我们逐渐推导出了几个各个GoF中的创建型模式。现在,有了对象,我们看看对象的行为。

我们从最基本的需求开始,如何使得对象在运行过程中行为可变?

这是一个非常一般的问题,我们从传统的方法说起。简单地说,对象的行为取决于对象的状态,而对象的状态由构成对象的成员的状态决定。这是毫无疑问的。所以明显的答案是改变对象的状态,最直观的是提供设置对象状态的接口,这样,通过操纵对象状态(直接修改或者对象某一行为的副作用),我们可以期望对象可以表现出不同的状态。从实现的角度看,对象的行为实现中包含了对对象状态的处理,因而可以对于同样的外部刺激(接收到同样的消息)给出不同的反应。

因为OO强调的是针对接口编程,而不是实现,所以上面的描述可以进一步表述为“对于特定的型的实现,如果在运行是改变其状态?”。敏感的OO程序员已经可以意识到,多态不就是解决这个问题的嘛!正是如此。如果我们可以使用不同的对象实现同样的型,自然可以提供不同的外部表现行为。多态正式应对此需要的技术。请注意其中的关键,是现实的需求催生了多态技术成为OO的基础,而不是相反。

OO之上论者喜欢强调“一切皆对象”。尽管这一说法受到不少的怀疑和批评,其表现的实现还是值得我们关注。研究一下一个对象如何接受一个消息。最简单的形式是

foo->bar(7);

使用OO术语描述为:想对象foo发送消息bar,消息参数是整数7。所以从概念上讲其等价于以下调用

send_message(foo, bar(7)); // 假设可以从整数直接构造消息本身

使用这个等价的表达,我们其实可以看出,这里,消息本身已经是另一个类型的对象了。当消息本身可以表达不同的信息,我们自然可以期望接受消息的对象可以给出不同的行为。如果把要发送消息的对象已经消息本身组合起来,作为一个独立的对象,那么这个对象已经有了执行一个请求的全部信息,可以在任何方便的时候执行请求。而作为一个整理的对象,可以利用其它机制在各个对象之间传递,而对象的调用却被完整地隐藏起来。消息的发送者在构造了该对象以后再也不用关心后续是怎么处理的了(有时候如果它关注执行的结果,那么可以令该对象之执行完毕的时候再通知它既可)。这意味这对象的发送和接受解耦了。在分布式系统中,莫不如此。这是什么呢?Command模式

现在看对象行为的实现。一般来说,对象行为实现为一系列的指令,提供或者不提供输出。无论如何,对于特性的行为实现,指令序列是固定的。那么除了上面提到的方法,怎么让行为结果随着需求的不同而变化呢?

编程领域有句俗话,说的是“任何问题都可以通过引入一个额外的中间层解决”。什么意思?

假设我们所有这些行为指令放在一个函数中,引入一个中间层是什么意思?两次函数调用?对,也不对。说它不对,是因为对调用一次本质上不能解决任何问题;说它对,是因为这个第二次的函数调用提供了一个机会,你可以通过任意你喜欢的方式调用这个函数,比如使用函数指针!。

任何有经验的C程序员都明白,稍微正式一点的程序都会用到函数指针,如异步支持,OO实现,命令解析等等。通过动态置换第二次调用函数指针,我们可以确保对于相同的接口给出不同的行为。其实,这是一种行为plug-in。可以可以把任何的行为实现动态插入。

C++中,函数指针的wrapper一般叫做functor,就是那种提供了函数调用操作符的对象(又是对象!)。现在情况简单了,我们使用一个型来定义某种行为接口,然后实现这种接口,最后,把该型的弱引用(对应于C++中的指针)作为成员。这样,在需要的时候,我们可以通过设置不同的实现来获得不同的行为了。似乎有点复杂,下面是代码的例子:

struct do_something { virtual void operator(int) = 0; };

class foo

{

do_something* functor_;

public:

void set_functor(do_something* f) { functor_ = f; }

void bar(int v) { functor_(v); }

};

这是什么呢? Strategy模式!一般来说,各种不同的strategy或者算法是相互独立的,可以根据不同的情况在运行时配置。

初看起来,Command模式和Strategy模式结构很像,都是使用单独的对象封装行为本身。但是Strategy强调的是行为的可互换,而Command强调的则是消息发送者与消息接受者的隔离。注意,意图才是决定设计选择的关键。

在上面的代码中,如果行为接口已经定义了高层次的执行绪序列,那么代码看起来是这个样子:

struct do_something

{

void operator(int v)

{

if (!do_check(v)) return

do_impl(v);

}

protected:

virtual void do_impl(int) = 0;

virtual bool do_check(int) = 0;

};

class my_impl : public do_something

{

protected:

virtual void do_impl(int) {...}

virtual bool do_check(int) {...}

};

class foo

{

do_something* functor_;

public:

void set_functor(do_something* f) { functor_ = f; }

void bar(int v) { functor_(v); }

};

客户代码可以这样:

foo f;

f.set_functor(new my_impl);

f.bar;

这种上次代码定义了实现框架,下层代码完成实现具体步骤的叫做什么呢?Template Method模式

假设后来需要要求你对输入参数做额外的检查。那该怎么办呢?OO的基本原则之一是OSP,及open to extention but close to modification。按照此原则,我们可以如下实现:

class my_impl_v2 : public my_impl

{

protected:

virtual bool do_check(int v)

{

if (!my_impl::do_check(v)) return false;

//do you own check here

}

};

这样的方法,GoF称之为Decorate模式。你给要继承的接口增加了功能,这是Decorate模式的最主要特征。另一个特征是,Decorate模式不会改变要修饰的接口。
不改变子对象接口的另一种情况是你需要限制而不是增强接口的某些功能。这样做的目的一般是通过导出该接口,使得该接口可以直接成为你自己的接口。GoF把这种使用对象的方法叫做Adapter模式。注意,这里Adapter模式强调是是行为适配
更常见的使用Adapter模式的情况则是你需要以你自己的接口导出一个子对象的接口。这很类似于把三根插针的开关转接为两根插针的开关。这里Adapter模式强调的是接口适配
尽管导出了子对象的接口为外部所用,倒是你需要提供一定的保护,只有在满足条件的情况下才真正转发消息给子对象。这是GoF所谓Proxy模式的一种情况。Proxy的另一种情况是常见的为了计算性能的延迟初始化。这样,字对象其实是在需要的时候才被加载的,而客户感知不到这一点。这叫做虚拟Proxy。分布式计算中,对象往往存在于远端。你可能要把对象请求编码成XML发送到对端计算机的某个预定端口。然后接受返回接口。这叫做远程代理。专门用于资源管理的引用计算技术,GoF命名为“智能指针”,恰好和C++中的名字一样。
按照接口设计原则,你已经有了一个完整的设计,提供了非常好的接口。有一天,你突然发现,你的代码客户中,很多人使用你的接口都是为了做同样的工作,而做这个工作需要以相似的方式调用多个已提供的接口。从简化客户代码的角度考虑,你觉得应该提供一个官方版本的实现了。这个新的接口与原先的接口共同存在,客户可以调用你新的接口,也可以调用原先的接口。嗯,不用奇怪,GoF也给了把这种串联聚合公共接口为另一个常用公共接口的方法一个名字,叫做Facade模式
GoF的书中,每一个模式都使用到了继承。做何解释呢?其实很简单,针对接口编程,而不是实现编程。实现是接口是实现,而只能使用继承。

对象的结构先到这里,下面谈对象间关系。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐