~怪^_*兽~

虚荣锁身躯 心灵给酒醉 脆弱人类 懒问何为对
(怪兽乐园Q群:75375912

导航

面向对象软件设计原则(五) —— 应用示例

  现在我们看看一个实际的例子,从不同设计原则的角度来观察它的设计。这个例子是信息处理中心-数据传输控制组件的消息处理模块,首先看看它的结构图、类实现伪代码和主体程序伪代码:

 

                        图3-1 消息处理模块结构图

 

程序3-1 类实现伪代码

///////////////////////////////////////////////////////////////////////////////////
Command 类
///////////////////////////////////////////////////////////////////////////////////

/* 多线程的难点在于线程的管理和线程的同步

以下的伪代码很好地完成了这两方面:
1: 用Windows线程池实现线程管理
2: 用CCommand对象实现线程同步
*/

class CCommand
{
public:
CCommand()
{
::InterlockedIncrement(&sm_ExecutingCommandCount);
}

virtual ~CCommand()
{
::InterlockedDecrement(&sm_ExecutingCommandCount);
}

virtual int Execute() = 0;

void SetContent(const String& content)
{
m_sContent = content;
}

// 正在处理的命令数量
volatile static int sm_ExecutingCommandCount = 0;
// CSWMRG类实现“多个线程同时读,但不能同时读写或同时写”的功能
static CSWMRG sm_SWMRG;

protected:
// 处理消息的个CCommand子类只需重载该方法
virtual int Process() = 0;

private:
String m_sContent;
};

/*
子类只需要重载Process()来处理各种请求(解析m_sContent、处理m_sContent)就OK!!!
可以当作单线程程序一样放心操作
不必担心缓存的同步和对象的释放等问题
*/

// SCP命令继承该类
class CReadCommand : public CCommand
{
virtual int Execute()
{
sm_SWMRG.WaitToRead();
int retval = Process();
sm_SWMRG.Done();
return retval;
}
};

// 更新命令继承该类
class CWriteCommand : public CCommand
{
virtual int Execute()
{
sm_SWMRG.WaitToWrite();
int retval = Process();
sm_SWMRG.Done();
return retval;
}
};

 

程序3-1 主体程序实现伪代码

///////////////////////////////////////////////////////////////////////////////////
主体程序
///////////////////////////////////////////////////////////////////////////////////

void ProcessMsg(const String& header, const String& content)
{
// CCommandFactory根据消息头生成相应的CCommand对象
CCommand* pcmd = CCommandFactory::GetCommand(header);
// 设置CCommand对象的内容
pcmd->SetContent(content);

::QueueUserWorkItem(ThreadFunction, pcmd);
}

void ThreadFunction(PVOID pv)
{
CCommand* pcmd = reinterpret_cast<CCommand*>(pv)
int ret = pcmd->Execute();
delete pcmd;
}

 

   程序中包含main、command、scp、notice和cmdfactory四包。其中,main包包含应用程序的高层策论,执行程序的主体代 码;command包是一个抽象包,它包含CCommand、CReadCommand和CWriteCommand三个抽象类;scp包包含了所有处理 SCP协议的命令对象;notice包包含了所有处理缓存更新通知的命令对象;cmdfactory包包含CCmdFactory类,这个类根据消息头生 成CCommand类的相应子类对象。

  main包中的程序代码要用到CCommand类和CCommandFactory类,所以它依 赖于command包和cmdfactory;scp包中的类继承于CReadCommand类所以它依赖于command包;notice包中的类继承 于CWriteCommand类所以它依赖于command包;cmdfactory包中的CCmdFactory类要用到CCommand抽象类及其所 有实现子类,所以它依赖于command、scp和notice包。

 

  下面从面各个向对象设计原则的角度来观察它的设计:

1. 单一职责原则(SRP

   CCommand类及其子类子负责执行命令(Execute方法),而选择命令的工作由CCommandFactory类负责。试想如果把选择命令的工 作放在CCommand中会有什么后果:每增加一个CCommand子类会导致CCommand的命令选择函数修改,由于CCommand类的修改导致所 有依赖于它的代码(所有CCommand子类和使用CCommand的客户代码)都需要重新编译,即使它们没有作过任何改动。

2. 开放-封闭原则(OCP

  当有新的命令要处理时,我们可以新增一个CCommand类的子类用来对这个命令进行处理,另外再对CCommandFactory进行相应修改就可以了。而程序的主体代码不用修改。

3. Liskov替换原则(LSP

  任何CCommand类的子类替换CCommand基类都使得针对CCommand编写的主体程序功能不变。

4. 依赖倒置原则(DIP

  主体程序代码(高层模块)不依赖于各个命令的实现子类(低层模块),两者都依赖于CCommand(抽象)。

  CCommand(抽象)不依赖于各个命令的实现子类(细节),各个命令的实现子类(细节)依赖于CCommand(抽象)。

5. 接口隔离原则(ISP

  (本例没有体现)

6. 重用发布等价原则(REP

  command包的所有类都是可以重用的,其他包的所有类都是不可重用的。

7. 共同重用原则(CRP

   command包是可重用的,但是可能有人会说,command包有小许违反共同重用原则,因为有些代码可能只用到command包中的 CReadCommand类(如scp包),而另外一些代码可能只用到command包中的CWriteCommand类(如notice包)。是不是应 该把command包拆分出readcommand包和writecommand包呢?

  这是一个设计权衡的问题。拆分出来当然可以,问 题是有没有必要?会带来什么好处?command包中之所以要在CCommand类中派生出CReadCommand类和CWriteCommand类是 因为它除了执行命令外还有另外一个目的――同步读写操作。所以它做了这样的假设:使用command包的程序必定有对读写操作进行同步的需求,也就必定会 同时用到CReadCommand和CWriteCommand这两个类。

  那么scp包只用到CReadCommand类,而 notice包只用到CWriteCommand又怎么解释呢?其实这只是个巧合问题,如果我们增加一个SCP协议包用来进行某些更新操作,它就会用到 CWriteCommand了,同理,如果我们在notice包中增加一个处理只读通知的命令,它也会用到CReadCommand。其实我们可以把 scp包和notice包组合成一个大包。这样就很明显地看到这个大包同时用到了CReadCommand和CWriteCommand类。对了,为什么 不把scp包和notice包组合起来而要分开呢?

  这是另一个设计问题!当用scp包处理SCP协议时各处理命令之间可能会在其他方面有共同的抽象(如SCP报文数据结构等);同理notice也一样(如通知的数据格式)。所以scp包和notice包之所以分开是基于其他原因,和这个主题无关。

8. 共同封闭原则(CCP

   把scp包和notice包分开而不是组合在一起还有什么好处呢?设想当SCP协议格式作了修改,它只影响所有处理SCP协议的命令类,处理通知消息的 命令类没必要因为一个与它无关的修改而跟着一起重新发布;同理,当通知消息的格式作了修改时,也不应该影响处理SCP协议的命令类。

9. 无环依赖原则(ADP

  从结构图中可以看到,依赖关系中不存在环。

10. 稳定依赖原则(SDP

   命令选择规则是非常不稳定的,每当新增一个命令或者业务规则发生改变时它都必须要作出相应修改,所以cmdfactory包应该位于依赖关系的高 层;command包是非常稳定的,无论业务规则变化或者其他的改动都不会影响到它,所以它应该位于依赖关系的低层。也就是说朝着稳定的方向进行依赖。

11. 稳定抽象原则(SAP

  command包是一个非常稳定的包,同时它又是一个抽象包(包含抽象类);main、scp、notice和cmdfactory包是非常不稳定的,同时它们也是具体的。


 

是否已经完美?

  通过上述的解说,是否说明了这样的设计已经接近完美了呢?可能是,也可能不是。

 

   大家有没有注意到,因为main包用到了CCommandFactory类,所以一个依赖关系从main包连到cmdfactory包?而 cmdfactory包又依赖于scp和notice包。这样就造成了main包中包含高层策略的主体代码传递依赖于scp和notice这两个包含实现 细节的包;当scp或notice包有改动时会令到cmdfactory包的CCommandFactory类修改,由于CCommandFactory 类的修改从而导致main包的程序主体代码部分必须重新编译。

  单从main包中的代码很难发现main包对scp包和notice包的 依赖,在main包的代码中只看到它依赖于command包和cmdfactory包,看不到它使用了任何一个scp包或notice包中的类。但是在关 系图中这个依赖关系是一幕了然的。嗯……有时图的表现力的确好于代码,题外话了~^_^~。

 

言归正传,我们怎样才能消除上述依赖关系呢?简单!—— DIP。修改后的结构如图3-2所示:

 

图3-2修改后的消息处理模块结构图

 

  这样,cmdfactory包不再依赖于scp和notice包,无论对scp或notice包作了什么修改,cmdfactory包和main包都不必重新编译。

 

   问题是这样的修改到底有没必要?如果scp和notice包的改动比较频繁或者main包的程序主体代码需要较长的编译时间,又或者scp和 notice包以共享库或DLL的形式提供,上述的修改是必要的;否则,当程序的规模较小,程序主体代码需要的编译时间可以忽略的情况下,可以不进行修 改。毕竟修改后的程序结构增加了一个抽象层意味着增加了复杂性。


 

设计原则与设计模式

  关于设计模式的介绍请参考[GOF95]中描述的23个设计模式。设计模式的确是个好东西。我们应该怎样把面向对象设计原则与设计模式结合起来呢?

 

  我通常的做法是先不考虑设计模式,一切简单至上。毕竟使用了设计模式就或多或少地增加了软件的复杂性(闻到一些复杂性的臭味没?)。随着程序规模的扩大,需要应用面向对象设计原则对软件中的模块进行抽象和解耦时,再把它回归为模式。

CodeProject

posted on 2012-02-28 23:26  ~怪^_*兽~  阅读(3816)  评论(10编辑  收藏  举报