《C++面向对象软件设计及构建》文章翻译:3.6更复杂的关联,3.6 More Complex Associations
真实的面向对象系统中,会牵涉到来自多个类的多个对象之间的关联。商用软件系统中,可能会牵涉到数以十计的类,以及成百上千的对象。此处我们要讲的示例,倒不会达到这个数量级。然而,会使用同样的技巧来引入多个额外的类,以展示,这样的复杂的、真实的系统会是如何构成的。
我们会制作出一个使用了关联功能的真实版本的Frame 类。关联,是有必要的,因为,如果将窗口中的所有丰富功能都直接完整地放置在这个接口中的话,Frame 类就会变得极端复杂,复杂到不可忍受。例如,目前仅可以绘制两种形状(线段和圆)。但是,还有狠多形状也是常见的,包括椭圆、矩形、曲线、和多边形,并且还可能为每种形状定义多种属性,例如颜色、线条宽度、线条模式、填充模式。显然,如果打算将所有这些细节都通过接口来进行控制的话,会创造出一个极端冗长且复杂的接口。另外,Frame类还需要进行扩展,以加上多种交互元素,使得用户能够对界面进行操作。这些交互元素包括:按钮、可编辑的文本框、滑动条、复选框、可滚动的列表、单选框,以及其它元素。即使只是向类中为每种交互元素加入一到两个方法,都显然会搞出一个臃肿的、无法控制的接口。
窗口中,图形方面和交互方面的职责,可分配到三种关联的类上去:
•. Frame:显示屏幕上的一个矩形区域,可移动,可改变大小。
•. Canvas:Frame 中的某个区域,用于绘制文字和图形内容,并且对鼠标移动进行响应
•. Panel:Frame 中的某个区域,其中包含着交互式元素。
Frame类已经简化,使得,其中仅保留着那些并未放置到Canvas 和Panel 类中去的职责。Canvas类覆盖了所有与绘制功能相关的职责。Canvas 区域中的鼠标事件,如今是关联到Canvas,而不再关联到Frame。Panel类,覆盖了所有与管理交互元素相关的职责。
调整过的Frame 类,以及新的Canvas 类,其定义如下所示。日后再说明Panel 类。
Frame类(版本4) |
class Frame { // 版本4 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); // 增大/减小指定倍数 ~Frame(); }; |
注意,与绘制相关的方法(例如,DrawText、DrawLine),都从Frame 类中去除了,这些绘制方法都放置到新定义的Canvas 类中了。Frame 类中剩下的那些方法,都是用来处理Frame 本身的定义和管理的。这样,Frame类就表示的是,用户屏幕上的一个有边界的区域,该区域可按照代码的控制来改变位置和形状。Frame 的内容呢,是由其它的类(例如Canvas和Panel类)来向Frame 中添加的。
Canvas类 |
class Canvas { private: // 封装的实现位于这里 public: Canvas(Frame& fr, char* nm, Location loc, Shape sh); int IsNamed(char* aName); // 这是妳的名字吗? 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(); }; |
注意,用于绘制文字内容和图形形状的方法,移动到了Canvas 类中。另外也要注意,Canvas 对象,只能通过与单个Frame 关联的方式来构造。因此,不可能让同一个Canvas 同时位于两个不同的Frame之中,也不可能创建一个不与任何Frame 关联的Canvas 对象。
Canvas 对象与Frame 对象之间的关联,是由Canvas 的构造函数来建立的。与之相对的是,在其 它示例中,两个对象之间的关联是由某个方法来创建的,而不是由构造函数来创建(例如,Counter 和Clock 类中的ConnectTo 方法)。 构造函数,可用来建立一个稳固的关联关系(在Canvas 对象创建时创建该关联关系,并且在该个Canvas 对象的余生中不再改变)。在其它情形下,关联关系可能会更具动态性。例如,某个Message 对象,在它生命过程中的不同时刻,可能会被显示到不同的Frame中。动态关联关系,以非构造函数的方法的形式来表达就更自然,这样,在那些对象的生命过程中,可以调用这些方法来改变其关联关系。
此处会引入三个额外的类,它们用来定义交互式控制元素。利用这些新的类的对象,可以创建出一套交互性更强的界面。这三个类是:
•. Panel:之前已经提到过,它是位于Frame 中的一个区域,其中包含着交互式元素,例如Button 和TextBox 对象。
•. Button:它所抽象的是,一个简单的、可按动的、带有名字的按钮,用户在该个按钮的显示图片界线之内点击,就可以达到“按下”效果。按钮,显示出来的效果是,一个包含了该按钮的名字的矩形。
•. TextBox:提供了一种机制,使得用户能够编辑一砣文字内容,该文字内容日后可被程序读取。
这三个类,组合到一起,就提供了一套基本的控件,可让用户输入数据,以及触发由程序所进行的动作。
Panel 类的定义,如下所示。与Canvas 类类似,Panel 对象的构造函数中,也需要一个Frame 对象作为参数。Panel 在其所关联的Frame 中的位置和形状,也必须传入到构造函数中。通过重载的Add 方法,能够让任意数量的Button和TextBox出现在Panel 中。
Panel类 |
class Panel { private: // 隐藏的数据 public: Panel(Frame& fr, char *nm, Location loc, Shape sh); char* getName(); void Add(Button& button); void Add(TextBox& tbox); ~Panel(); }; |
Button 类的定义,如下所示。 Button 类的构造函数中,要求给出Button 对象的名字。名字 ,有 两个用途。首先 ,按钮的名字,会出现在屏幕上,显示在按钮(Button)的图形区域的文本标签中。因此 ,一个名为"Start"的Button 对象, 其显示出来的效果是, 一个带边框的矩形框,围绕在文字 Start 的周围。其次 ,当用户“按下”这个 Button( 也就是说,用户点击 了对应 着该个Button 的带边框矩形框 ) 的时候,函数OnPush(char*) 会被调用, 该个Button 的名字就会作为参数被传入。 OnPush(char*) 是一个新函数,它会被加入到 这个简单的编程环境中。 这样,程序 猿 就能够利用OnPush 函数的参数和Button 对象的IsNamed 方法来确定,用户究竟点击 了多个Button中的哪一个。
Button类 |
class Button { private: // 隐藏的实现 public: Button(char* name, Location loc, Shape sh); int IsNamed(char* name); ~Button(); }; |
下面所示的TextBox类,允许用户编辑和/或输入数据。每个TextBox,其显示出来的效果是,一个带边框的矩形区域,当鼠标指针被移动到TextBox 区域内部时,就会有一个文本输入光标出现。当这个光标可见时,用户就能够在TextBox 中编辑、删除或加入任意可见文字内容。TextBox会将多行文字滚动显示,因此,在任何时候,可能只有一部分文字是可见的。程序可使用 TextBox的SetText 和GetText 方法来设置或查询该TextBox 的当前值。
TextBox在构造的时候,可以传入一个可选的文字标签内容,该个文字标签会出现在TextBox 的左侧。TextBox 的形状(Shape),必须足够宽,使得,它能够容纳下文字标签以及用户可能输入的文字内容的长度。
TextBox类 |
class TextBox { private: // 隐藏的实现 public: TextBox( Location p, Shape s, char* label); TextBox( Location p, Shape s); TextBox( char* label); TextBox(); ~TextBox(); char* GetText(); void SetText(char* val); }; |
下面展示 了一个小巧的系统,它使用了所有的那些新的类。 这个系统中,向用户显示了一个TextBox,用户可在其中输入一个字符串,并且 ,当用户点击了一个显示着 Copy 字样的按钮时,TextBox 中的当前内容会被程序读取并显示到某个Canvas 区域中。注意 ,Button 和TextBox 都是位于( 与之关联 )一个Panel 中,并且 ,该个Panel 和Canvas 都是位于( 与之关联 )一个Frame 对象中。另外 ,注意,在OnPush 方法中,利用Button 类的IsNamed 方法来识别究竟 是哪个Button 对象被按下了。
一个示例系统 |
Frame window ("TestWindow", Location(100,100), Shape(500, 300)); Canvas canvas (window, "DrawAreas", Location(1, 1), Shape(100, 100)); Panel panel (window, "Controls", Location(150, 10), Shape(300, 100)); Button button ("Copy", Location(5, 5), Shape(50,30)); TextBox tbox (Location(5,50), Shape(150,30), "Enter:"); char *text; void OnStart() // 当"Start"按钮被按下时,会调用一次 { canvas.Clear(); panel.Add(button); panel.Add(tbox); text = (char*)0; } void OnPush(char *buttonLabel) { if (button.IsNamed(buttonLabel)) { canvas.Clear(); canvas.DrawText(tbox.GetText(), Location(20, 20)); text = tbox.GetText(); } } void OnPaint() { canvas.DrawText(text, Location(20,20)); } |
Clock类,可被扩展,以提升Clock 类的可用性。程序猿可能会需要使用多个不同的Clock(例如,为了对不同的事件进行时序控制,为了使用不同的时间间隔),同时,程序猿需要能够灵活地定义,当某个Clock 触发了时间事件时,应当做什么动作。为了达到这个灵活性,就需要按照如下的方式对Clock 类进行扩展。每个Clock对象,在构造时,都会指定一个名字,以用来唯一地标识该个Clock 对象。Clock的时间间隔,可通过构造函数来指定,或者,当Clock 处于停止状态时,可通过SetInterval 方法来指定。Clock,可通过Start 和Stop 方法来控制。最后,与Button 对象类似,在Clock 对象中,有一个IsNamed 方法,它的作用是,将该个对象的名字与传入的字符串参数做比较。注意,在之前的定义中,Clock 可以连接到一个Counter 对象。在每个时间间隔的结束时刻,Clock对象或者会调用它所连接到的Counter 对象的Next()方法,或者,如果它没有连接到Counter 对象的话,则会调用OnTimerEvent()函数,如下所示。
修改过的Clock类 |
class Clock { private: // 隐藏的实现 public: Clock (char* name, int interval=1000); void SetInterval(int newInterval); void Start(); void Stop(); int IsNamed(char* name); ~Clock(); }; |
简单编程环境中的一个扩展,允许程序猿定义,当某个特定的Clock 触发了一个定时器事件时,该做出什么动作。OnTimerEvent函数被重新定义,现在会传入一个字符串参数,它即是触发了此次定时器事件的Clock 的名字。
由于Frame 类中的变动的影响,Message 类也必须做些微小的修改。如今,Message 对象不再直接显示在Frame 中了;而是显示在Canvas 中。这个微小的变化,如下所示。
修改过的Message 类 |
class Message { private: //封装过的实现 public: ... void DisplayIn (Canvas& whichCanvas); ... }; |
对于Clock 对象、修改过的Message 类的对象以及修改过的OnTimerEvent 函数的使用,在以下示例中展示。这个程序,是由之前的Blinking Text Hello World 程序修改而来。在这个程序中,会使用一个Clock 对象来控制Message 对象中包含的文字内容的闪烁。Clock的时间间隔,已在构造函数中定义为500 毫秒。Clock是在OnStart 方法中启动的,它的定时器事件,是由OnTimerEvent 方法所响应,该方法会将通过参数传递进来的名字与Clock 对象本身的名字进行比较。
使用Clock的示例 |
Frame window ("Message Test", Location(100,100), Shape(200,200)); Canvas canvas (window, "Message Area", Location(10,10), Shape(180,180)); Message greeting ("Hello World!", Location(20,20)); Clock timer ("timer", 500); int onoff; void OnStart() { greeting.DisplayIn(canvas); greeting.Draw(); onoff = 1; timer.Start(); }; void OnTimerEvent(char* clockName) { if( timer.IsNamed(clockName) ) { if (onoff) { greeting.Clear(); onoff = 0; } else { greeting.Draw(); onoff = 1; } } } void OnPaint() { if (onoff) greeting.Draw(); } |
1.使用Counter、Message 和Frame 类来编写一个程序,它会实现一个简单的计时器。使用OnTimerEvent 函数来更新Counter 对象。使用鼠标左键的点击事件来控制OnTimerEvent 当前是否要更新Counter 对象。第一次鼠标左键点击事件会“开始”计时(后续的OnTimerEvent函数运行过程会更新Counter 对象,具体是通过Counter 对象的Next 方法来实现)。第二次鼠标左键点击事件会“停止”计时。也就是说,连续点击鼠标左键会轮流地开始及停止计时。
2. 按钮计时器。使用Clock、Counter、 Message、Button 、Frame 和其它类来实现一个程序,它是带有一个按钮的计时器。使用Clock 对象来触发对于OnTimerEvent 函数的调用,该函数会更新Counter 对象。使用一个显示着"Control"的Button 对象来控制,OnTimerEvent 函数当前是否要更新Counter 对象。第一次按下这个Button,就会“开始”计时(后续的OnTimerEvent函数运行过程会更新Counter 对象,具体是通过Counter 对象的Next 方法来实现 )。第二次按下这个Button,就会“停止”计时。也就是说,连续按下这个Button,就会轮流地开始及停止计时。
3.构造一个系统,其中显示三个Message,每个Message 会显示某个不同的Counter 中的值。与"Tens"所关联的那个Counter,应当在与"Units"所关联的计数器被增加10次时增加一次;类似地,对于"Hundreds"和"Tens"也是这样的关系。使用另外三个Message,用于标识"Units"、"Tens"和"Hundreds"。
4.构造一个系统,其中显示着一个一秒钟的Clock,当用户点击屏幕上不同的按钮时,可分别开始及停止该定时器。
5. 构造 一个系统,其中有两个Clock。 一个的时间间隔是1秒,另一个的时间间隔是0.1 秒。每个Clock 都连接到各个不同的计时器,并且 可被各自独立的开始( start )和停止( stop )按钮控制。两个计时 器,都会显示在屏幕上,并且带有适当 的文字标签。
6.构造一个系统,其中有一个一秒钟的Clock,它显示在屏幕上,可被开始、停止及重置。要想重置这个Clock 的话,用户必须先停止这个Clock,在TextBox 中输入一个新的时间,按下重置(reset)按钮,再按下开始(start)按钮。
未知美人
孟茜
Your opinionsHxLauncher: Launch Android applications by voice commands