StupidBeauty
??Read times:18????Posted at:Wed Oct 4 03:24:20 2017?? - no title specified

《C++面向对象软件设计及构建》文章翻译:3.2以复制的方式传递对象,3.2 Communicating Objects by Copy

用于传递信息的对象,可以以复制的形式,作为某个接收者对象的某个方法中的输入参数使用,也可以作为某个方法的返回值使用。下面,我们对Frame 类的接口进行修改,使得它更具有面向对象的特点,并展示前面所说的两种形式。

将对象作为输入参数

以下示例中,使用了两个新的类。在Frame 类之前的定义中,框框的位置和形状,是由四个整数值来表示的。然而,这两个概念,都可以用类来表示,如下表所示。

Location

  class Location {                     // 版本1

     private:  

        // 封装的实现位于这里

     public:

        Location(int x, int y);         // 指定的位置

        Location();                     // 默认位置

     int Xcoord();                      // 返回x轴坐标

     int Ycoord();                      // 返回y轴坐标

   };

Shape

  class Shape {                        // 版本1

     private:  

        // 封装的实现位于这里

     public:  

        Shape(int width, int height);   // 指定形状

        Shape();                        // 默认形状

     int Height();                      // 返回高度

     int Width();                       // 返回宽度

   };

Location 类中的Xcoord、Ycoord 方法,还有Shape 类中的Height 和Width 方法,通常被称为“取值”("accessor")或“查询”("query")方法。因为,通过这些方法,可以间接地访问到或查询到该对象的状态信息。

这些新的类的对象,可以按照以下方式来声明:

Location nearTop(20, 20), nearCenter(500, 500);

Shape    smallSquare(50, 50);

Shape    largeSquare(500, 500);


这两个类,直接为我们带来两个好处(狠快,还会说到,有另外五个好处)。首先,它们表达了位置和形状的概念。这正是面向对象编程想要实现的目标 – 构建一些类,让它们表达某些应用场景中的概念。一个好的类,不需要有一个看着就狠重要的接口:LocationShape类,是普通的类,它们各自表达了一个简单 但是有用的概念。其次,以上的声明,展示了,带有名字的对象,可以向读者传递有用的信息,以表达出程序猿的用意。"largeSquare",这个名字,更明确地表达出了作者的意图,即,某个窗口会比较大,并且是方形的。这就比在参数列表中传入两个整数500 和500 更明确,更不用说再加上另外两个整数以及一个或更多其它的参数值了。

如今,可以对Frame 类进行重新定义,现在可以用上Shape 和Location 的定义,如下图所示。

Frame (版本3)

class Frame {                         // 版本3

  private:  

          // 封装的实现位于这里

  public:

          Frame(char* name, Location p, Shape s);       // 准确的描述

          Frame(char* name, Shape s, Location p);       // 准确的描述

          Frame(char* name, Location p);                // 默认形状

          Frame(char* name, Shape s);                   // 默认位置

          Frame(char* name );                           // 只有名字

          Frame();                                      // 全部使用默认值;

     void MoveTo(Location newLocation); // 移动窗口

     void Resize(Shape    newShape);    // 改变形状

     void Resize(float factor);         // 按照比例增大/缩小

          ...                           // 其它方法

};


定义Shape 和Location 类,所带来的第三个好处,可在上面的头两个重载构造函数中看到。由于ShapePostion是互不相同的两个类,所以,能够定义不同的构造函数,按照不同的顺序来使用它 们作为参数。如果妳使用四个整数值来表示形状和位置信息,就做不到这一点了。第四个好处,可在重载构造函数"Frame(char* name, Shape s);"中看到,在Frame 类之前版本(版本2)的定义中,并不存在(也无法存在)这个函数。当妳使用四个整数来表示位置和形状信息时,仅仅带两个整数输入参数的构造函数,会表达出什么意思?取决于四个参数的顺序,它可能表示了缺少形状信息,或者表示了缺少位置信息,但它无法同时表示两个意思!然而,引入不同的类,将形状的两个整数和位置的两个整数区分开来之后,表示这两种意思的重载都是可行的了。

Frame对象,可按照以下方式来创建:

Frame  smallTop    ("Square Near Top", nearTop,    smallSquare);

Frame  largeCenter ("Big at Middle", nearCenter, largeSquare);

Frame  someWhere   ("Big Somewhere", largeSquare);

Frame  someSize    ("At Middle", nearCenter);

Frame  anyKind     ("Name Only - Rest Defaults");


在那些Frame 对象的声明中,展示了Shape 和Location 类的最后三个好处。

五个好处是,在使用Shape 和Location 类的情况下,那些声明语句更具有可读性。第六个好处,那些ShapeLocation对象(例如,largeSquarenearCenter)可被重用,这样就能够避免需要额外地记住 中心附近的点 的具体坐标。第七个好处,通过改变那些Shape 和Location 对象(例如,nearTop)的声明,就能够对应地改变那些Frames 的声明。不需要再在整个代码中寻找那些声明Frame 对象的地方,并修改它们的整数参数值了。

LocationShape类,它们的用途,比我们最初设计时预期的还要多。由于Location类表达了一个合理的抽象概念(在一个二维坐标系统中的一个点),所以,在任何一个出现了这种坐标系统的地方,都是可以用到它的。例如,Location类帮助了记录一个窗口会在屏幕上出现的位置,那么,同样地,它可以被其它的界面条目使用,以记录,该界面条目应当放置在窗口中的什么位置。

那些显示在Frame 中的文字和图形条目,也使用了位置和形状的概念。例如,DrawText方法,指定了,特定的文字,要显示在特定的位置。还有,DrawLine方法,指定了一条线段的两个端点(两个位置)。Frame 类中的这些方法,以及类似的方法,都可以从Location 和Shape 类中获益,如下图所示。

Frame (版本3)其它部分

class Frame {                         // 版本3(其它部分)

     private:  

            ...

     public:

            ...                         // 其它方法

       void DrawText(char *text, Location loc);

       void DrawLine(Location end1, Location end2);

       void DrawCircle(Location center, int radius);

       void Clear();

       void Clear(Location corner, Shape rectangle);

           ...

   };


注意Clear方法,同时用到了LocationShape类,因为,这个方法中,需要指定Frame 中某个矩形区域的位置和尺寸。这就展示了上面所说的观点:LocationShape类狠有用,只要任何地方需要利用二维的坐标系统指定位置,或者任何地方需要使用矩形形状数据,就可以用这两个类,它们可用于指定Frame 在屏幕上的放置信息,也可以用于指定Frame 中显示的条目的放置信息。

以复制的方式返回对象: Frame示例( 接上个示例 )

方法的执行结果,也可以以对象的方式来返回。返回一个对象,而不是某个单个的原子类型,就使得,该个方法能够在结果中传递一个复杂的实体。下面给出了两个示例,以展示,如何以复制的方式来返回对象。其中一个示例使用的是Frame 类,另一个示例使用两个新的类。

Frame 类中的TextSize 方法,应当被重新定义,以便返回一个对象作为它的结果。TextSize方法,用于计算指定的某个文字字符串所占据的矩形区域的尺寸。其计算结果,取决于:该个Frame 所使用的字体;字符串的长度;以及字符串中的具体字符(某些字符,例如 w m ,比其它字符要宽,例如 i t)。在之前版本的Frame 类中,是这样声明这个方法的:

class Frame {                        // 版本1

...

public:

...

void TextSize (char *msg, int& width, int& height);

...

};

此处,尺寸信息是以两个单独整数值的形式返回的。然而,这个定义中,有两个问题:

  • •. 这个方法,并未明确地表达出,TextSize 方法的职责就是返回某个矩形区域的尺寸,同时,它也未表明是否要使用Shape 对象来传递这个信息;

  • •.TextSize的参数,并没有跟Frame 类中其它相关方法(例如,Clear方法)的参数相匹配。

以下示例中,会显示一个文字字符串,再擦除它,这个示例展示了,TextSize 方法的参数与Clear 方法的参数不匹配:

Frame    display;

int width, height;

char *msg = "Hello World!";

Location msgLocation(50,50);

...

display.DrawText(msg, msgLocation);

...

display.TextSize(msg, width, height);

Shape msgShape(width, height);

display.Clear(msgLocation, msgShape);

注意,在此处,程序猿必须显式地创建msgShape 对象。必须这样做,才能将TextSize 方法修改过的两个整数值组织成 Clear 方法所要求的形式(一个Shape对象)。

TextSize方法,可重新定义为返回一个Shape 对象,如下所示:

class Frame {                   // 版本3(其它部分)

...

public:

...

Shape TextSize(char *msg);

...

};

注意,这个定义,就更清晰地表达了TextSize 方法的职责:计算并返回一个对象(属Shape),它描述了屏幕上某个矩形区域的尺寸。

利用TextSize 方法的这个新的定义,就可以将之前那个显示再擦除文字字符串的示例写成更简洁的形式,如下所示:

Frame    display;

char *msg = "Hello World!";

Location msgLocation(50,50);

...

display.DrawText(msg, msgLocation);

...

Shape msgShape = display.TextSize(msg);

display.Clear(msgLocation, msgShape);

注意,如今,TextSize 方法的返回结果,与Clear 方法所要求的参数相匹配了。还有一点需要注意的是,msgShape 对象的声明,可以放在代码中第一次使用msgShape对象的地方。妳也可以在比较靠前的地方写出这个声明,如下所示:

Shape msgShape;

...

msgShape = display.TextSize(msg);

...

一些程序猿倾向于将声明语句放置在第一次使用的位置,尤其是当这是该对象的唯一一次使用时,因为这样能够帮助提高可读性。另外一些程序猿呢,倾向于将所有的声明放置在开头部分,尤其是当这个对象会在代码中多处多次使用时,因为这样只需要看一个地方就能够轻易地找到任何对象的声明。在某种程度上说,这只是个人品味的不同。

修改后的Frame

Frame 类中,所有那些能够在以复制方式传递信息的改进中得到好处的方法,都已经被重新定义了。下表中,将之前的那个单个的修改都收集到一起了。显然,使用了LocationShape类之后,Frame 类的可读性和可用性得到显著改善。

Frame(版本3)

 class Frame {                         // 版本3

     private:  

          // 封装的实现位于这里

     public:

           Frame(char* name, Location p, Shape s);      // 准确描述

           Frame(char* name, Shape s, Location p);      // 准确描述

           Frame(char* name, Location p);               // 默认形状

           Frame(char* name, Shape s);                  // 默认位置

           Frame(char* name );                          // 只提供名字

           Frame();                                     // 全部使用默认值;

     int   IsNamed(char* aName);                // 这是妳的名字吗?

     void  MoveTo(Location newLocation);        // 移动这个窗口

     void  Resize(Shape    newShape);           // 改变形状

     void  Resize(float factor);                // 按照比例增大/缩小

     void  DrawText(char *text, Location loc);  // 显示文字字符串

     Shape TextSize(char *msg);                 // 计算某个字符串的形状

     void  DrawLine(Location p1, Location p2);  // 绘制线段

     void  DrawCircle(Location center,

                      int radius);              // 绘制

     void  Clear();                             // 擦除整个Frame中的内容

     void  Clear(Location corner,

                 Shape rectangle);              // 擦除矩形区域

          ~Frame();

   };

以复制的方式来返回对象:文件对话框示例

第二个关于在方法中返回对象作为结果的示例,用到的是,代表着磁盘上某个文件的File 类,以及其它三个提供了不同的对话框方法以便从用户处取得某个文件名的类:FileQueryFileChooserFileNavigator

File类,表达的概念是,文件系统中,某个有名字的、可被阅读的文字实体。这个类的定义如下所示。

File

 class File {

  private:

                                // 封装的实现位于这里

  public:

       File(char* fileName);           // 代表某个指定了名字的文件

       File();                         // 目前尚未得知其名字的文件

 char* Name();                         // 返回文件的名字

 int   Exists();                       // 文字存在吗?

 void  View();                         // 可滚动的查看窗口

 void  Edit(char* editor);             // 使用“编辑器”("editor")来编辑文件

 void  Delete();                       // 从文件系统中删除这个文件(删除了就没有了!)

      ~File();                         // 释放文件

};

在构造函数中,可以为该文件指定一个名字,而Name 方法可用于查询这个名字。由于文件对象在创建过程中可以不提供名字,并且,用户可能输入一个不存在的文件的名字,所以,提供了Exists方法,它会返回某个值,表明,该文件是否存在于文件系统中。

View方法,会在屏幕上打开一个窗口,在该窗口中,可以查看该文件的内容。用户可以在查看文件内容时水平滚动及竖直滚动,但文件内容只可被查看,不可被修改。可使用Edit 方法来对文件进行修改,它的参数是,要用来做出修改动作的编辑器的名字。可使用Delete 方法来将文件从文件系统中删除。在执行Delete 方法之后,这个File 对象仍然存在,但是它所代表的文件本身将不复存在。

FileQuery类,会向用户显示一个对话框。它会提示用户输入某个文件的名字。FileQuery对象,会返回一个File对象,它代表着由用户指出其名字的那个文件。FileQuery类的定义如下所示:

FileQuery

class FileQuery {

 private:

                        // 封装的实现位于这里

 public:

        FileQuery( char* path, char* filter );    // 指定目录路径,并使用过滤器

        FileQuery( char* path );                  // 指定目录路径,使用默认过滤器

        FileQuery( );                             // 全部使用默认

   File AskUser();                                // 通过对话框从用户那里得到文件信息

  ~FileQuery();

};

FileQuery 的构造函数中,可以提供一个目录路径(例如,"/home/user")以及预期的文件名的模式。这里的模式,会使用传统的Unix通配符。例如, *.ps ,这样一个过滤器,表示的是,任何带有 .ps 后缀的文件名。如果不提供这两个参数,那么,路径的默认值就是当前工作目录,而过滤器的默认值就是任意文件(也就是说, *)

FileQuery的使用自由度是狠高的。这里的路径和过滤器信息,只是给予用户的提示,它们并不是强制的。用户可以随意输入任意文件名。稍后,我们会说明一种替代的、更严格的用于从用户那里获取文件名的方法。

FileQuery 类的主要成员函数,即是AskUser 方法。这个方法,返回一个File 对象,它关联到用户在由AskUser 方法所显示的对话框中输入的文件名。

以下示例,展示了File 和FileQuery 类的用法:

FileQuery query("/home/kafura", "*.ps");

File file = query.AskUser();

file.View();

在这个示例中,FileQuery对象与用户进行交互,并返回一个File 对象,该对象会被显示出来,供用户查看。

特定类的对象,可被多个其它类所返回。以上定义的FileQuery 类,只是在与用户交互的对话框中返回某个File 对象作为结果的其中一种方式。FileQuery 类中所使用的技巧,具有一个弱点,就是,它严重地依赖于用户能够正确地记住文件的名字,以及,依赖于用户能够正确地输入那个名字。

另外两个会返回File 对象的类,使用了选择及导航技巧。选择,表示的是,会向用户显示一个文件列表,让用户从中选择一个。导航,表示的是,用户能够遍历文件树,以找到自己想要的文件。以下这两个利用选择和导航技巧的类,也会使用目录的路径和过滤器。

FileChooserFileNavigator类的定义如下所示:

FileChooserFileNavigator

class FileChooser {

  private:

                                // 封装的实现位于这里

  public:

         FileChooser(char* path, char* filter);   // 在某个路径中寻找,并使用过滤器

         FileChooser(char* path);                 // 在某个路径中寻找,不使用过滤器

         FileChooser();                           // 在当前工作目录中寻找,不使用过滤器

    File AskUser();                               // 通过对话框获取文件信息

        ~FileChooser();                           // 清理

   };

class FileNavigator {

  private:

                                // 封装的实现位于这里

  public:

         FileNavigator(char* path, char* filter);   // 从某个路径开始,并使用过滤器

         FileNavigator(char* path);                 // 从某个路径开始,不使用过滤器

         FileNavigator();                           // 从当前工作目录开始,不使用过滤器

    File AskUser();                                 // 通过对话框获取文件信息

        ~FileNavigator();                           // 清理

   };

在FileQuery、FileChooser和FileNavigator这三个类的公有接口中,可以看到面向对象编程的一个重要特点:尽管它们的名字不同,但是,它们使用了相同的接口。构造函数的参数是相同的,AskUser 方法的返回值也是相同的。每个类,都向程序提供了相同的功能,只是它们各自以不同的方式来实现这个功能。然而,这些类之间的这种相似性,还无法允许在程序中透明地对它们进行使用。由于类型检查规则的存在,到目前为止,在我们已经学习的C++语言中,还无法在不修改源代码的情况下将其中一个类直接替换成另一个类。日后,我们将尝到,还有有一些有效的办法,可以对具有这种相似性的类进行组织及操作的。

任务

  1. 1.编写一个程序,它创建一个尺寸为 200 x 200 的窗口,放置在屏幕中心附近,并将妳的完整名字显示在窗口的中心附近。

  2. 2.编写一个程序,它创建一个尺寸为 200 x 200 的窗口,放置在屏幕中心附近,并将妳的名显示在窗口顶部正中间,将妳的姓显示在窗口底部的正中间。

  3. 3. 扩张/收缩线:编写一个程序,它在屏幕中心附近显示一个长度为 200 的水平线,这条线会按照以下方式来交替地收缩及扩张:最开始,每次的定时器事件,会导致这条线在其两侧都变短;其效果是,这条线看起来正在向其中点收缩。收缩会持续到这条线的长度变成0()为止。然后,每次的定时器事件,会导致这条线在其两侧都变长;其效果是,这条线看起来正在从其中点向外 扩张。无限地重复这种收缩及扩张。通过试验,确定一个适当的收缩及扩张幅度,使得它对于妳来说看起来“自然”。

  4. 4. 文件查看1:利用FileQuery 类来编写一个程序,它会显示出用户指定的文件的内容。最开始,应当向用户显示一个窗口,其中显示文字内容"Click Here to View a File"(“点击这里以查看一个文件”)。当用户在窗口中点击鼠标左键时,使用一个FileQuery对象来获取一个File,然后将它显示出来以供查看。

  5. 5. 文件查看2:利用FileChooser类来编写一个程序,它会显示出用户指定的文件的内容。最开始,应当向用户显示一个窗口,其中显示文字内容"Click Here to View a File"(“点击这里以查看一个文件”)。当用户在窗口中点击鼠标左键时,使用一个 FileChooser对象来获取一个File,然后将它显示出来以供查看。

  6. 6. 文件查看器3:利用 FileNavigator类来编写一个程序,它会显示出用户指定的文件的内容。最开始,应当向用户显示一个窗口,其中显示文字内容"Click Here to View a File"(“点击这里以查看一个文件”)。当用户在窗口中点击鼠标左键时,使用一个 FileNavigator对象来获取一个File,然后将它显示出来以供查看。

  7. 7.利用FileChooser 类来编写一个程序,它会对用户指定的文件进行编辑。

  8. 8.利用 FileNavigator类来编写一个程序,它会对用户指定的文件进行编辑。

  9. 9.利用 FileNavigator类来编写一个程序,它会删除用户指定的文件。

Sandy

未知美人