再造一个WinZip
——C++流技术面向对象分析与设计
*********************************************************************
面向对象分析(OOA)与编程(OOP)技术已经成熟,但直到现在,有不少程序员仍不能主动应用面向对象的思想来设计开发软件。在本文中,我将以一个文件分割与合并工具的开发设计过程为例,介绍程序员如何用OOA和OOP进行程序设计。
一 功能构想和总体的实现方案
在设计一个软件之前,一定要弄清这个软件提供什么样的功能。设计文件分割合并工具的主要想法始于将一个大文件分割为小块以方便传输,必要时又可以再合并起来。我将这个工具软件的功能用UML表示如下:
图 1 系统用例图可以看到,这个工具软件的功能与WinZip几乎完全一样。
功能确定,就开始考虑系统设计问题。应该指出,规模不同的软件系统,其设计阶段是不一样的,对于现在这个小工具软件,可以直接考虑其实现技术。
在C/C++中,将文件看成是一个字节流,并提供了好几种方式对文件进行操作:
l C语言方式:用fopen()、fclose()等打开和关闭文件,然后,直接在内存中对字节流进行处理;
l STL方式:用标准模板库(STL)中的iostream类;
l 特定开发环境所提供的类:如VC中的CFile,CMemFile等类;
C语言方式不符合OOP风格,STL功能强大,但在本例中开发工作量相对较大。我用的开发工具是C++Builder,其所提供的TStream类也能够满足要求,所以从实际出发,采用现成的VCL类库中的流系列类来开发此软件。
从系统功能出发,显然必须首先提供流的基本操作函数,实现流的打开、合并、抽取等操作,为此设计了FileStreamOpt类完成这些功能。下面展示了FileStreamOpt类的接口:
class FileStreamOpt
{
public:
__fastcall FileStreamOpt(); //构造函数
__fastcall ~FileStreamOpt(); //析构函数
//按指定的文件名和打开方式打开流
TFileStream * __fastcall OpenFileStream(String FileName, String OpenMode);
//关闭一个流
void __fastcall CloseStream(TStream *pStream);
//向一个流pStreamFirst的尾部追加一个流pStreamSecond
bool __fastcall AppendStream(TStream* pStreamFirst, TStream * pStreamSecond);
//抽取pStream流从Begin开始的Size字节的流内容存入文件NewFileName中
bool __fastcall ExtractStream(TStream* pStream, String NewFileName, int begin, int size,bool OverCover);
//将流pStream中Begin开始的Size字节删除
bool __fastcall CutOutStream(TStream* pStream,int begin, int size);
};
需要指出的是,为了实现通用性,接口采用了流基类指针参数TStream *,这样处理的结果是这些函数可以通过多态适用TStream类的所有子类。
二 数据结构
由于一个文件包中要存放若干个文件,显然必须有一个数据结构用来保存文件的有关信息,最容易想到的就是定义一个Struct,然后,向文件中写入这个结构就行了。但更合理的是生成一个文件信息类(FileInfo)。其接口如下:
class FileInfo : public TComponent
{
private:
String FFileName;
int FFileSize;
bool FNeedMerge;
int FMergeNo;
int FMergeTotalNum;
String FFilePath;
public:
__fastcall FileInfo(TComponent *AOwner);
FileInfo& operator=(FileInfo& a);
__published:
//文件名
__property String FileName = { read=FFileName, write=FFileName };
//文件路径
__property String FilePath = { read=FFilePath, write=FFilePath };
//文件大小
__property int FileSize = { read=FFileSize, write=FFileSize };
//是否需要合并。即这是拆分过的文件。
__property bool NeedMerge = { read=FNeedMerge, write=FNeedMerge };
//一个文件可分割的文件总数。
__property int MergeTotalNum = { read=FMergeTotalNum, write=FMergeTotalNum };
//要合并文件的文件顺序号
__property int MergeNo = { read=FMergeNo, write=FMergeNo };
};
一个FileInfo类的实例(对象)对应一个文件流对象。在保存文件包时,将包中所有FileInfo对象依次写入包文件中,在最前面写入对象数(也就是文件包中所包含的文件总数,int类型),后面就是各文件的实际数据。打开文件包时,先读取第一个int数据,然后是一个循环,读出所有的FileInfo对象数据,在内存中重建这些对象。
包中各个文件流按次序首尾相接形成一个文件包流,每个文件的具体位置可以通过它在文件包中的次序和在它之前的所有文件的大小计算出来。
这样一来,当软件运行时,增删文件体现为对FileInfo对象的增删和对文件流的插入和删除操作。
现在马上又出现了一个问题:这些FileInfo对象怎么管理?要自己定义一个数组或链表吗?最合适的方法是用STL提供的容器——List和Vector均可选:
l Vector:动态数组,缺点是在中部增加和删除元素时要移动其余的元素,因而效率不如List高;
l List:增加删除元素非常快,但不支持随机存储,如果要按某个序号来访问元素,则必须从头开始,或用find()方法。
在我这个软件中,如果规定增加文件时始终在Vector尾部追加FileInfo对象,就不会有移动对象的问题。删除文件时当然需要移动容器中的元素,但这并不是最常用的操作,而且一个文件包中存放的文件不会太多,删除文件所花的时间主要在文件流操作上而不是Vector内部元素的移动上,所以最终决定采用Vector管理FileInfo对象,这样一来,以后就可以用一个文件对应的FileInfo对象在容器中的索引来使用这个文件。
值得注意的是STL要求自定义的对象必须重载赋值运算符,所以我在FileInfo类中重载了“=”运算符。
FileInfo& FileInfo::operator=(FileInfo& a)
{
//重载赋值运算符
//注意:为避免编译时出现二义性错误,
//要将返回值和参数都设置为引用
FFileName=a.FileName;
FFilePath=a.FilePath;
FFileSize=a.FileSize;
FMergeNo=a.MergeNo;
FMergeTotalNum=a.MergeTotalNum;
FNeedMerge=a.NeedMerge;
return *this;
}
下面考虑FileInfo对象的存储问题:在MFC的单文档结构中,可以用“<<”和“>>”向流中存放对象,但我在BCB中并没有使用MFC的框架,当然,我可以自定义一个由STL中流对象继承而来的类来实现这个功能,事实上,BCB在TStream这个所有流对象的基类中已经实现了ReadComponent()和WriteComponent()方法,因此,只要使FileInfo对象继承自TComponent,这个对象就可以被保存到流中去,现在可以不必编写额外的代码了。
要指出的是,需要保存的信息必须设计成BCB中的property,并将其放在published部分,对于Public部分的数据成员,WriteComponent()不会保存其值,这点似乎不太爽,但这是想偷懒使用别人封装好的东西的必然代价,不是吗?所幸这点麻烦比自己去编写一个类似的方法要小多了。
用一个FileInfo类来代表一个文件的信息,增强了程序的可扩展性,我只需增加类的数据成员就可以加进新的信息,比如可以为每个文件增加一个文件属性和一个描述信息,而它的保存和读取代码都不用改变,也不用考虑它写入文件时的尺寸大小,这点Borland公司的程序员在设计ReadComponent()和WriteComponent()方法时已经考虑到了。
三 设计功能类:FileCutMerge
这个类实现了软件的基本功能,它的接口如下:
typedef std::vector<FileInfo *> FileInfos;
class FileCutMerge:public TComponent
{
private:
//操作流对象指针
FileStreamOpt * pStreamOpt;
//当前文件包流指针
TFileStream* pPackage;
//用于指向文件信息对象的指针
FileInfo *pInfo;
//用于保存文件信息对象的容器指针
FileInfos * pVector;
//当前包中文件总数
int FileNum;
//文件内容开始的指针
int FileBegin;
//文件包文件名
String PkgFileName;
public:
//获取文件包中指定文件流与文件包流开头的距离
int __fastcall GetFileOffset(unsigned int num);
//类构造函数1
__fastcall FileCutMerge ( String PackageFileName,bool NeedCreate , TComponent* AOwner);
//向包中增加一个函数
bool __fastcall AddFile( String FileName);
//类析构函数
__fastcall ~FileCutMerge();
//获取文件包中文件数
int __fastcall GetFileNum();
//获取文件包中指定文件的FileInfo对象
FileInfo * __fastcall GetFileInfo(unsigned int num);
//将指定文件展开到磁盘上
bool __fastcall ExtractFile(unsigned int num,String path,bool OverCover);
//展开所有的文件
bool __fastcall ExtractAllFiles(String path,bool OverCover);
//删除一个文件
bool __fastcall DeleteAFile(unsigned int no);
//将文件包分割
bool __fastcall SplitFile(String FileName,String Path, int size);
//将分割后的文件包复原
bool __fastcall MergeFile(String MergeFileName);
//读取文件包文件大小
int __fastcall GetPackageSize();
//类构造函数2
__fastcall FileCutMerge(TComponent* Owner);
//打开一个文件包
bool __fastcall OpenPackage(String PackageFileName,bool NeedCreate);
//新建一个文件包
bool __fastcall NewPackage(String PackageFileName);
//包文件名
__property String PackageFileName = { read=PkgFileName, write=PkgFileName };
//将包文件存入磁盘
bool __fastcall SavePackageToDisk();
};
因为这个类中要对流作很频繁的操作,所以将StreamOpt类包容进来,在构造函数中创建它,在析构函数中销毁它。
又由于文件与FileInfo对象一一对应,所以,也将FileInfo容器包容进类中(即类私有成员FileInfos * pVector;)
这样一来,要操作文件包的调用者根本无须考虑流操作的具体细节,直接操作FileCutMerge对象就行了。这就很好地体现了OOP的优点:信息封装,复用性好(只需new一个FileCutMerge对象就可以实现操作文件包的功能,不用时再Delete掉)
这样,我们完成了系统的功能设计,如下图所示。