设计模式研究:如何判断一个封装方案是好是坏
2006-12-20 16:53
639 查看
学习设计模式的人常说“封装变化”。变化,应当怎么封装?设计模式那么多种,可以采用多种封装方案,如何判断一个封装方案是好是坏?
答案:看封装后系统演化时,修改点是否唯一而定。
说得直白一些。当系统需要扩展一个功能时,你要修改的地方是多了还是少了?少了就说明封装得好。
封装的最高境界是:以后增加一个功能,只需要修改一个地方就行。
下面,我举一个封装失败的例子来说明这一点。
此例子来自一个实际应用的P2P网络应用项目。在网络项目中,需要定义多种协议数据包,以此来互相通讯。这里的变化趋势是很明显的:随着系统的发展,数据包的数量会有增减,原有数据包的字段也有可能增删。
现在的问题是,如何将这些变化封装起来?下面是一个程序员实际使用的方案。
1. 最初编写网络程序,以结构体来进行协议编码,解码
struct GPRSInboundMsg
{
PROTO_HDR Header;
UBYTE_t arUnitAddr[UNITADDR];
INT32_t iEventTime;
UBYTE_t pMessage;
};
2. 这种协议是比较常见协议,带有基本数据类型,数组,和不定长的成员,如果直接使用结构体进行协议数据的编码,解码,很容易因为字符串拷贝,内存申请,释放这些常用操作带来问题, 下面使用类来封装该协议:
class CPtoOutbMsg{
public:
void SetUnitAddr(const BYTE_t *pAddr);
void SetMessage(const BYTE *pMsg);
virtual int Pack(char *pPointer, const char usBufLen);
virtual int UnPack(const char *pPointer, const char usBufLen);
private:
PROTO_HDR Header;
UBYTE_t arUnitAddr[UNITADDR];
INT32_t iEventTime;
UBYTE_t pMessage;
};
这个方案很简单,就是用对象来代替结构体,并且将结构体的成员全用函数封装起来。
设计者解释他这样做的目的是,避免直接操作这些字段引起的内存越界等问题。从这个角度来说,设计者这样的封装是达到目的了。可惜的是,设计者忘记了封装的目的:减少修改点。
我们看看这种封装方案对系统的影响。
直接使用结构体的方案时,某个数据包增删一个字段,需要修改的地方有:
1、修改结构体;
2、修改所有引用该结构体的地方(删除字段需要,增加字段就不用了。但如果系统有某些特殊需求,如计算可变长度数据包的实际长度,那么这些地方也可能要修改。)
而经过上面封装之后,对于同样的需求,需要修改的地方有:
1、修改结构体(同上);
2、修改所有引用该结构体的地方(同上)
3、修改该数据包对象的函数(增删两个该字段相应的读写函数)
4、修改Pack函数(打包函数)
5、修改UnPack函数(解包函数)
增加数据包也是类似的,即除了增加相应结构体之外,需要相应创建一个结构体封装对象:每个字段的读写函数,打包解包函数等等。
现在很清楚了,这种封装是失败的。它为系统修改带来了不止一倍的开销。
使用这样的封装,带来的后果是比较严重的,每次协议修改,都会让修改者累个半死(想想一个字段的变动,要修改五个地方!)。程序员必定强烈反对修改协议,或者产生怠工情绪。
最后总结:
一个封装方案成功与否,看封装后系统演化时,修改点是否唯一而定。
答案:看封装后系统演化时,修改点是否唯一而定。
说得直白一些。当系统需要扩展一个功能时,你要修改的地方是多了还是少了?少了就说明封装得好。
封装的最高境界是:以后增加一个功能,只需要修改一个地方就行。
下面,我举一个封装失败的例子来说明这一点。
此例子来自一个实际应用的P2P网络应用项目。在网络项目中,需要定义多种协议数据包,以此来互相通讯。这里的变化趋势是很明显的:随着系统的发展,数据包的数量会有增减,原有数据包的字段也有可能增删。
现在的问题是,如何将这些变化封装起来?下面是一个程序员实际使用的方案。
1. 最初编写网络程序,以结构体来进行协议编码,解码
struct GPRSInboundMsg
{
PROTO_HDR Header;
UBYTE_t arUnitAddr[UNITADDR];
INT32_t iEventTime;
UBYTE_t pMessage;
};
2. 这种协议是比较常见协议,带有基本数据类型,数组,和不定长的成员,如果直接使用结构体进行协议数据的编码,解码,很容易因为字符串拷贝,内存申请,释放这些常用操作带来问题, 下面使用类来封装该协议:
class CPtoOutbMsg{
public:
void SetUnitAddr(const BYTE_t *pAddr);
void SetMessage(const BYTE *pMsg);
virtual int Pack(char *pPointer, const char usBufLen);
virtual int UnPack(const char *pPointer, const char usBufLen);
private:
PROTO_HDR Header;
UBYTE_t arUnitAddr[UNITADDR];
INT32_t iEventTime;
UBYTE_t pMessage;
};
这个方案很简单,就是用对象来代替结构体,并且将结构体的成员全用函数封装起来。
设计者解释他这样做的目的是,避免直接操作这些字段引起的内存越界等问题。从这个角度来说,设计者这样的封装是达到目的了。可惜的是,设计者忘记了封装的目的:减少修改点。
我们看看这种封装方案对系统的影响。
直接使用结构体的方案时,某个数据包增删一个字段,需要修改的地方有:
1、修改结构体;
2、修改所有引用该结构体的地方(删除字段需要,增加字段就不用了。但如果系统有某些特殊需求,如计算可变长度数据包的实际长度,那么这些地方也可能要修改。)
而经过上面封装之后,对于同样的需求,需要修改的地方有:
1、修改结构体(同上);
2、修改所有引用该结构体的地方(同上)
3、修改该数据包对象的函数(增删两个该字段相应的读写函数)
4、修改Pack函数(打包函数)
5、修改UnPack函数(解包函数)
增加数据包也是类似的,即除了增加相应结构体之外,需要相应创建一个结构体封装对象:每个字段的读写函数,打包解包函数等等。
现在很清楚了,这种封装是失败的。它为系统修改带来了不止一倍的开销。
使用这样的封装,带来的后果是比较严重的,每次协议修改,都会让修改者累个半死(想想一个字段的变动,要修改五个地方!)。程序员必定强烈反对修改协议,或者产生怠工情绪。
最后总结:
一个封装方案成功与否,看封装后系统演化时,修改点是否唯一而定。
相关文章推荐
- 设计模式研究:如何判断一个封装方案是好是坏
- 【VBA研究】如何在if中判断一个值为null的变量
- Java中类的继承,属性和方法的四种修饰符的作用范围,final关键字,java的三大特点中的2个:封装和多态,以及多态的一个设计模式,模板方法模式(template method)
- Jquery如何序列化form表单数据为JSON对象 C# ADO.NET中设置Like模糊查询的参数 从客户端出现小于等于公式符号引发检测到有潜在危险的Request.Form 值 jquery调用iframe里面的方法 Js根据Ip地址自动判断是哪个城市 【我们一起写框架】MVVM的WPF框架(三)—数据控件 设计模式之简单工厂模式(C#语言描述)
- iOS封装浅谈-一句代码弹出actionSheet,如何优雅的设计一个ActionSheetManager
- 第二章 实例研究:设计一个文档编辑器--《设计模式-可复用面向对象软件的基础》Erich Gamma
- 如何“挤”出一个交互设计方案
- 如何在一个项目中完美融入GreenDao并使用Facde设计模式操作数据库
- 我是如何学习设计模式的九:轻松一下,学习一个简单的:单例模式(最简单,但是也是最有用的)
- 正在学设计模式呵,转载一篇如何保证一个窗体的实例运行
- 设计技巧11:静态创建方法(非设计模式中的工厂方法) 利用一个静态的方法封装构建器
- 如何利用Android特点及设计模式 设计一个网络下载功能
- 如何用C实现一个类以及些许设计模式
- PHP中如何判断一个字符串是否是合法的日期模式
- .NET如何判断控件处于设计模式
- 如何做一个优秀的微服务访问安全设计方案?
- 黄聪:如何判断VS开发C#是否为设计模式,以免编译之前操作窗体设计器代码自动运行
- 如何使用三层架构设计模式去完整的实现一个功能?
- 如何设计一个模式是所有的排序方法都是稳定的
- 如何设计一个在线烧录方案?