[摘要]软体样式(Design Pattern ) 之应用 ※ 高焕堂 自从1991年以来﹐样式观念和理论逐渐成为物件导向(OO)领域中最热门的话题之一。本文探讨如何使用样式﹐解决软体上的常见问题。 什么是样式? 顾名思意﹐样式是人们遭遇到特定问题时﹐大家惯用的应付方式。样式可用来解决问题﹐而且是有...
软体样式(Design Pattern )
之应用
※ 高焕堂
自从1991年以来﹐样式观念和理论逐渐成为物件导向(OO)领域中最热门的话题之一。本文探讨如何使用样式﹐解决软体上的常见问题。
什么是样式?
顾名思意﹐样式是人们遭遇到特定问题时﹐大家惯用的应付方式。样式可用来解决问题﹐而且是有效、可靠的。掌握愈多样式﹐运用愈成熟﹐就愈是杰出的设计专家。
依据样式理论大师亚历山大(Christopher Alexander) 之定义﹕
「样式是某外在环境(Context) 下﹐对特定问题(Problem)的惯用解决之道(Solution)」
例如﹐在平坦的校园里(Context) ﹐下述两项需求是互相冲突的﹕
◎学生需要交通工具代步。
◎校园空气必须保持清洁。
就产生问题(Problem) 了﹐该如何化解呢﹖在一般校园里﹐惯用解决之道(Solution)是﹕规定在校园中只能骑自行车﹐不能骑有污染的机车和汽车。此解决之道是否有效﹐是取决于外在环境(Context) 。例如﹐在不平坦的清华大学校园中﹐上述解决之道就无效用了。于是另找解决方案来处理这问题了﹔例如﹐提供电动机车供学生在校园中使用。
自从1991年以来﹐亚历山大的样式理论逐渐应用于OO软体的设计上﹐可解决软体设计上的问题。例如﹐软体设计时﹐常见下述问题﹕
软体模组(Module)之间的相依性太高﹐不易个别抽换模组﹐软体的「软」性就降低了。
可利用样式解决这种问题﹐求增强模组之间的独立性﹐提高软体的弹性(Flexibility) ﹐降低软体的维护费用﹗
设计样式(Design Pattern)
在Erich Gamma 等人合着的﹕
"Design Patterns: Elements of Reusable Object-Oriented Software"
一书列出23种软体样式﹐可解决软体设计上的特定问题。本文举例说明其中的3 种样式﹐看看它们的应用情形。
1. Factory Method 样式
2. Abstract Factory 样式
3. Builder样式
首先拿个简单程式来叙述「问题」之所在﹕
//// p1.cpp
class Integer {
int value;
public:
Integer() {value=100;}
void DisplayValue()
{cout << value << endl;}
};
class Document {
Integer *pi;
public:
Document() { pi = new Integer(); }
void DisplayData()
{pi->DisplayValue();}
};
void main() {
Document d;
d.DisplayData();
}
//// end
两类别的相依性很高﹐原因是﹕Document类别直接使用Integer 字眼﹐且使用两次。于是﹐问题是﹕
「若必须将Integer 类别名称改为Float 时﹐得更换 Document类别中的Integer 字眼」。
亦即﹐抽换Integer 类别时﹐会牵连到Document类别﹐抽换过程将不会很顺畅﹗藉由抽象类别﹐可解决部分问题,如下﹕
//// p2.cpp
class Data {
public:
virtual void DisplayValue()=0;
};
class Integer : public Data {
int value;
public:
Integer(){value=100;}
virtual void DisplayValue()
{ cout << value << endl; }
};
class Document {
Data *pd;
public:
Document(){ pd=new Integer(); }
void DisplayData()
{ pd->DisplayValue(); }
};
void main() {
Document d;
d.DisplayData();
}
//// end
删除掉1 个Integer 字眼﹐已长进些了﹐不是吗﹖但问题尚未全部解决﹗Integer 字眼仍留在Document类别内﹐还是难分难解﹗
Abstract Factory样式
使用Abstract Factory样式可解决上述问题﹗它将new Integer() 指令包装(Encapsulate) 起来﹐Integer 字眼就不再出现于Document类别中。
样式类别将Document与Integer 两类别隔开来﹐令其互相独立﹐以利于个别抽换。
图1样式是协调者
样式好象马路的安全岛或分道栏:
图2 样式像马路的分道栏
依照Erich Gamma 书中的Abstract Factory样式﹐应定义如下类别﹕
//// p3.cpp
//// Server Classes
class Data {
public:
virtual void DisplayValue()=0;
};
class Integer : public Data {
int value;
public:
Integer(){ value=100; }
void DisplayValue()
{ cout << value << endl; }
};
//// Pattern Classes
class Factory {
public:
virtual Data *CreateDataObject()=0;
};
class IntFactory : public Factory {
public:
virtual Data *CreateDataObject()
{ return new Integer(); }
};
////Client classes
class Document {
Data *pd;
public:
Document(Factory *pf)
{pd = pf->CreateDataObject();}
void DisplayData()
{pd->DisplayValue();}
};
void main() {
Document d(new IntFactory);
d.DisplayData();
}
//// end
这Abstract Factory样式内含Factory Method样式。上述Document类别不再使用Integer 字眼了﹐这个好现象﹐就是样式创造出来的效果,如图3 所示。
因为Client部分仅使用1 次IntFactory字眼﹐且不会用到Integer 字眼﹔于是﹐就容易抽换Integer 类别了。例如﹐也定义Float 类别﹐以及定义FloatFactory类别如下图4 所示。
假设有一天﹐必须将Integer 改为Float 时﹐只需更动main()函数如下﹕
....
void main()
{
Document d( new FloatFactory );
d.DisplayData();
}
....
只更动一个指令而已﹐其它都保持原状﹐很棒吧﹗
上述的Factory 类别及其子类别﹐合起来称为Abstract Factory样式。其中的CreateDataObject()虚拟函数之用法则称为Factory Method样式。两者共同创造出Client部分与Server部分之独立性。
图3Abstract Factory样式之角色
图4Client 类别与Server之介面
Abstract Factory样式之原理
图4 表达了﹕Document类别只使用抽象类别Factory 和Data之名称﹐并使用这些抽象类别之函数。抽象类别定义了其子类别之共同介面(Interface),而子类别定义其内涵(Implementation)。于是﹐Document类别之设计者只需知道Server之介面﹐不需了解其内涵。只要介面保持稳定﹐Server之内部更动时﹐并不影响Client类别之内容﹐就创造出弹性和独
立性。这充分表现OO设计的精神﹕
"Program To Interface"
(针对介面﹐而非针对内涵)
使用Builder 样式
从上述Abstract Factory样式之应用中﹐可归纳出两个基本动作﹕
图5 Client与Server类别相依性高
1.把Server类别之名称(例如﹐new
Integer )包装于具体类别(Concrete
Class) 之中。
2.利用Factory Method样式﹐将虚拟函
数定义于抽象类别内﹐供Client 类别
使用之。
这方法也可应用于较复杂的组合类别(Composite Class) 上﹐如上图5 所示。
Document类别负担组合(Composition) 之工作﹐亦即诞生Age 及Score 物件﹐并将它们组合成StudentRecord 物件。所以Document类别直接使用StudentRecord 、Age 及Score 名称﹐且负责诞生物件﹐如下﹕
//// p4.h
class Age {
int value;
public:
Age( int v ) : value(v){}
void DisplayValue()
{ cout << value << endl; }
};
class Score {
float value;
public:
Score(float v)=value(v){}
void DisplayValue()
{cout << value << endl;}
};
class StudentRecord {
char name[10];
Age *pa;
Score *ps[2];
int index;
public:
StudentRecord(char *na)
{strcpy(name, na);
index=0;
}
void SetAge(Age *pAge)
{pa=pAge;}
void AddScore(Score *pscore)
{ps[index++]=pscore;}
void Display()
{cout << name << endl;
pa->DisplayValue();
for( int i=0; i<index; i++ )
ps->DisplayValue();
}
~Student()
{delete pa; delete ps[]; }
};
//// p5.cpp
#include "p4.h"
class Document
{
//....
StudentRecord *pst;
public:
Document(char *na)
{pst=new StudentRecord(na);
pst->SetAge(new Age(20));
pst->AddScore(new Score(90.5));
pst->AddScore(new Score(88.8));
}
//....
};
void main() {
Document d("John");
d.Display();
}
//// end
Document类别含有new Age 等指令﹐应将之包装起来﹐并使用Factory Method样式﹐将虚拟函数摆入抽象类别中﹐如下图6。
图6Builder样式之角色
上述图5 与图6 的分别是﹕图6 使用Builder 样式时﹐Document只使用Builder 类别定义之介面﹐未用到Age 和Score 类别。所以只要Builder 保持不变﹐就可抽换StudentRecord 、Age 等具体类别之内函﹐而不会牵连到Document之内容。此外﹐SRBuilder 具体类别包装了StudentRecord 之组合「过程」﹐Document之设计者不必考虑到如何将Age 、Score 等物件组合成为StudentRecord 大物件﹐就创造了Document与StudentRecord 之间的独立性。假若有一天﹐必须改变其组合之「过程」﹐并不必更动Document及其它Client类别之内容﹐弹性由然而生了。以C++ 表达Builder 样式如下﹕
////p6.h
class Builder {
public:
virtual void BuildSR()=0;
virtual void BuildAge(int v)=0;
virtual void BuildScore(float s1)=0;
virtual void BuildScore(float s1, float s2)=0;
virtual StudentRecord *GetResult()=0;
};
class SRBuilder {
StudentRecord *curr;
public:
void BuilderSR(name)
{curr=new StudentRecord(name);}
void BuildAge(int v)
{curr->SetAge(v);}
void BuildScore(float s)
{curr->AddScore(s);}
void BuildScore(float s1, float s2)
{curr->AddScore(s1);
curr->AddScore(s2);
}
StudentRecord *GetResult()
{return curr;}
};
////p7.cpp
#include "p4.h"
#include "p6.h"
class Document {
Builder *pb;
public:
Document(Builder *builder)
{pb=builder;}
void BuildStudentRecord(char *na)
{pb->BuildSR(na);
pb->BuildAge(20);
pb->BuildScore(98.5, 80.25);
}
void Display()
{ pb->GetResult()->Display(); }
};
void main() {
Document d( new SRBuilder );
d.Display();
}
//// end
从上可知﹐理想的软体师应不断追求软体的弹性﹐软体才能生生不息。软体欠缺「弹性」﹐是传统软体危机的主因﹐只要是在某前提下(Context) ﹐样式就能提供有效的解决之道。
……