再造一个WinZip ——C++流技术面向对象分析与设计(二)

    技术2022-05-11  280

     

      用户界面设计

    用户界面层的设计很简单,在BCB中用控件画出来就是了,如下图所示:

    3 文件分割合并工具的用户界面

           由于大多数人对WinZip都很熟悉,所以就用了它的图标,并模仿它的界面,这样就使用户不需要过多的学习,马上就可以使用这个软件。

    我将整个软件分为功能处理层(前面已设计完成)和用户界面层(如上图所示窗体),用户界面层无需知道底层流操作的有关细节,它只需向FileCutMerge对象发布命令就是了。

    在用户界面层编写代码来集中处理用户操作,关键之处有如下几点:

    1.关于用户主界面与功能类FileCutMerge间的关系:

    原先的考虑是一个包对应一个FileCutMerge对象,当要打开一个包时,就创建一个,再打开另一个文件包时,又新建一个。但后来发现如果在用户第一次打开或新建包时创建一个FileCutMerge对象,然后一直复用它,通过NewPackage()和OpenPackage()方法来完成后继的新建和打开包工作,直到程序关闭时再删除它,就避免了不断地创建和销毁FileCutMerge对象的花销,显然更为合理,这也是我最终所采用的方案。

    2.用户界面编码的分层原则:

    我的看法是不在窗体控件的事件处理函数中加入过多的功能性代码,而是将其抽取出来定义为一个窗体级的私有函数。

    比如单击“打开包”按钮,我只写了以下代码:

    void __fastcall TMainForm::btnOpenPackageClick(TObject *Sender)

    {

            OpenPackage();

    }

    而将OpenPackage()设计为主窗体TmainForm的一个私有函数:

    void __fastcall TMainForm::OpenPackage()

    {

    //……

    //FileCutMerge对象发打开包命令

    pFileCutMerge->OpenPackage(……);

    //……

    }

    这样处理的好处是我可以很方便地更改界面设计而不用大规模地改写代码,同时,窗体界面代码与功能代码(FileCutMerge类)的实现相分离,也更易于实现可维护性。

    提高软件的健壮性

    我主要采取了以下几个方法:

    1.  当程序基本完成后,从最底层的类开始,逐步找出每个对外接口的前条件(前提)和后条件(结果),然后,判断有无可能发生错误,并且决定这些错误应该如何处理。

    2.每一个函数都尽可能地保证是独立完备的、安全的。

       参见以下代码:

    bool __fastcall FileStreamOpt::ExtractStream(TStream * pStream, String NewFileName, int begin, int size,bool OverCover)

    {

            //从一个现成流pStream中抽取出一部分(beginbegin+size),成为新流,并存为指定的文件NewFileName

            //

            //前提:

            //1.Begin应小于流的尺寸

            //2.当文件存在时,根据OverCover决定是否替换,当不能替换时(如文件只读)

            //  给出提示后放弃操作

            //

            //结果:

            //成功返回True

            //

            //注意:

            //可能失败的原因:内存分配和操作失败

           

    if(FileExists(NewFileName)&& OverCover)

            {

                    bool ret=DeleteFile(NewFileName);

                    if(ret==false)  //可能是只读文件

                    {

                           ShowMessage("文件:"+NewFileName+"已经存在,且无法删除");

                           return false;

                    }

            }

            //有同名文件且不需要删除

            if(FileExists(NewFileName)&& !OverCover)

            {

                    ShowMessage("文件:"+NewFileName+"已经存在");

                    return false;

            }

     

            TFileStream *pNewFile;

                  //调用同一类中的另一个函数OpenFileStream创建流,失败时它会返回NULL

            pNewFile=OpenFileStream(NewFileName,"createWrite");

                  //判断对OpenFileStream()的调用是否成功?

            if(pNewFile==NULL)      //不能创建文件

                    return false;

            if(pStream->Size<begin)         //检查尺寸大小

                    return false;

            try

            {

                    //移到合适的位置

                    pStream->Seek(begin,soFromBeginning);

                    //分配内存

                    void *buf=new char[size];

                    if(buf==NULL)

                    {

                            ShowMessage("内存不足");

                            return false;

                    }

                    pStream->Read(buf,size);       //将流的内容读入缓冲区

                    pNewFile->Seek(0,soFromBeginning);

                    pNewFile->Write(buf,size);     //写入新流

                    pNewFile->Free();       //写入文件

            }

            catch(...)

            {

                    return false;

            }

            return true;

    }

    从这个例子中我们可以看到,如果在编写代码的时候就加入清晰而且规范的注释,日后要看这段代码或要调试这个函数那就轻松多了。

    上例中只是采用了简单的trycatch语句处理错误,在大项目中,错误的种类更多,一般要设计出一整套错误类,当相应的错误发生时,采用“throw 错误类对象;”的方法来处理错误。

    关于错误处理,在Tyson Gill著的《Visual Basic 6 高级编程策略与范例  ——错误编码与分层技术》一书中总结得很好,虽是针对VB的,但其实适用于任何一种软件开发语言,他认为一个健壮的软件应该:

    l         预防所有可以预料和防止的错误;比如上例中对文件已存在情况的处理

    l         处理所有可以预料但不能防止的错误;比如内存分配失败的处理

    l         捕获所有不能预料的错误。比如上例中的Catch(…)语句。

     

    3.保证以用户界面层正确的顺序调用功能类:

    人们主要是通过窗体等用户界面来使用软件,而一个结构合理的软件是由窗体接收用户命令,再调用相应的功能模块来完成数据处理工作。如果一个程序将界面代码和功能代码揉在一起,那是最糟糕的程序设计风格。

    我认为大多数软件错误都是由于用户进行了程序员设想之外的操作引发的。这些错误的操作引发了对于软件模块的错误调用,破坏了模块间的合作关系。

    程序员都怕陷入无穷无尽的对程序打补丁的死循环,其实最有效的一个办法就是在写底层模块进尽量保证它的正确性,然后再尽可能地保证用户界面层以正确的顺序调用底层模块,如果经常发现一个错误来源于底层代码的错误,那将是令人悲哀的事情。

    对于一个复杂的用户界面(窗体),其上有多个可接收用户输入的控件,比如本例中有8个按钮和一个ListView控件,用户操作的可能性就有9!种(假设ListView只有一种选中对象的操作),要保证这么多情况下软件都不出错,关键是要保证一定要以正确的顺序调用功能类。比如,在本例中如果没有选中文件对象,那么删除一个文件是无意义的,这时在代码中就不应该向FileCutMerge对象发出DeleteAFile命令。参见示例:

    void __fastcall TMainForm::btnDeleteFileClick(TObject *Sender)

    {

            //用户没有选中文件对象则退出

            if(ListView1->SelCount==0) return;

     

            //获取选中的文件

            TItemStates selected = TItemStates() << isSelected;

            TListItem *Item = ListView1->Selected;

            int num;

                   //更改光标的形状,告诉用户计算机正在进行处理

            Screen->Cursor=crHourGlass;

            while (Item)

            {

                    num=Item->Index;

                                 //真正删除一个文件

                    pFileCutMerge->DeleteAFile(num);

                                 //读取下一个选中的文件

                    Item = ListView1->GetNextItem(Item, sdAll, selected);

                    //删除文件图标

    ListView1->Items->Delete(num);

            }

            Screen->Cursor=crDefault;

    }

    这里没有必要判断是否FileCutMerge对象是否已经建立(pFileCutMerge==NULL?),因为如果文件包为空或根本没有打开文件包,那么ListView中就根本不会有对象存在。当然不排除这种情况有可能发生,写出代码保证这项工作的正确执行是程序员的责任。所以,最好在函数开头加入两句:

    //是否有有效的文件包打开

             if(pFileCutMerge==NULL)

                    return ;

    此外,这段代码还有一个不足之处,没有对pFileCutMerge->DeleteAFile(num);语句的返回值作出处理。当然,因为DeleteAFile()的全部代码是我写的,我已经在这个函数内部对可能发生的错误进行了处理,如果是调用其他人写的函数,那么加入对这些函数调用错误处理的代码是必不可少的。

    所以,我们看到向外界隐藏不必要的内部操作信息是多么的重要,因为一个对象向外界提供的功能越多,它与其他对象配合工作的方式就越多,发生错误的可能性也就越大。

    总结

    在现代的软件开发过程中,面向对象技术已成为主流的开发方法,这就要求我们程序员必须不断改进我们的思维方法,以跟上时代的步伐。就目前而言,我认为一个优秀的程序员必须树立以下的观念:

    1.  设计重于编码:搭狗窝可以马上动手,盖大楼可不能没有设计图纸;

    2.  再小的程序也要注重规范性:多写注释=少花时间看代码打补丁

    3.  尽量采用软件复用的思想:Never reinvent the wheel.”(引自C++之父Bjarne Stroustrup的名著:《The C++ Programming Language》);

    4. 具体的编程技巧和技术重要,软件的开发思想和方法更重要!

    附:

    本文所讲之例子附有全部源代码和Rose 2001的建模文件,有兴趣的读者可与我联系:

    JinXuLiang@263.net


    最新回复(0)