StupidBeauty
Read times:3038Posted at:Sat Oct 14 10:29:03 2017 - no title specified

《C++面向对象软件设计及构建》文章翻译:3.5一个简单的关联关系,3.5 A Simple Association

关联关系 ,是面向对象的系统中,最基本的结构之一。关联关系,指的是,一组互相独立的、互相交互的、互相合作的对象。这些对象是互相独立的,因为,它们必须被单独构造;在该个关联关系中,每个对象,除了自己的角色之外,还都有自己的身份,以及可见性。这些对象之间是互相交互的,因为,关联关系的整个目的,就是,让各个对象知道彼此的存在,或者互相连接。在关联关系中,每个对象,都可以使用它所连接到的任何其它对象的方法。技术上说,以指针或引用的方式来传递对象,这种方式,即被用来在对象之间建立连接。最后,这些对象是互相合作的,因为,它们都被配置为组成一个系统,这个系统中的各个组件之间形成合作关系。

以关联的方式来构建系统,能够提升软件的复用性。如果,关联关系中的那些对象,可以使用已有的类来创建,那么,不需要狠多的额外编程,即可构造出这个系统。因为,只需要简单地创建出那些对象,并将它们关联起来,即可构建出系统。这种构建系统的方法,通常被称作即插即用技术,因为,构建者只需简单地将这些对象插接到一起,再使用(试验、验证、评估)这个系统即可。一个设计良好的类,应该能够潜在地被狠多不同的关联关系所使用(复用)。即使是在为特定系统开发类的时候,类的设计者们,通常仍然会为当前正在被设计的类预估尽可能多的使用场景。

关联关系的结构,取决于,系统中的职责是怎样在关联关系中的对象之间划分的。通常改变不同对象之间的职责划分,就可以以不同的结构来实现多个具有相同的整体行为的系统。在面向对象编程中,创建一个设计,通常就意味着,确定单个对象或类之间的职责划分。类似地,理解了某个特定类的行为,通常就能帮助理解那个类所对应的职责。改变某个类的职责,就会影响到那个类的实现、那个类的接口,以及,那个类的对象与其它合作对象之间的交互方式。

我们会使用一个带有两个类的简单示例,来展示,一个由关联关系而创建出来的系统,以及,改变系统中的职责划分会对系统的结构产生什么影响。尽管这个“系统”非常小,但是,所展示的概念,却是宏大的。相同的概念,在更大的系统中,以不同的尺度呈现。

这个示例,牵涉到一个狠小的系统 – 闪烁文字。闪烁文字,狠简单,就是,显示一个字符串,并且让它闪烁。网页上,经常见到这种闪烁的元素,有的是文字,有的是图片,其目的是吸引用户的注意力。闪烁效果,具体实现方式是,交替地在屏幕上显示及擦除这个字符串。这两个动作之间的时间间隔,决定了,这个字符串是闪烁得快还是慢。

在此,我们会展示及说明这个简单系统的三种变化版本。在这个简单系统的各个变动版本中,用来代表闪烁文字这个抽象概念的类,会逐渐承担更多职责。在这个过程中,我们会观察到,这种变动,对于这个类的接口以及系统中其它元素的影响。

步骤1:最小职责

对于这个问题的第一个解决方案,只给PrimitiveMessage 类分配特别少的职责。这个类,是对闪烁文字的抽象,它仅仅体现闪烁文字这个事物所表现出来的文字属性。PrimitiveMessage类,只负责记住文字内容。PrimitiveMessage 所记住的文字,必须在构造时提供,可通过GetText 来查询,或者通过SetText 来修改。

PrimitiveMessage

class PrimitiveMessage

{

  private:

  public:

           PrimitiveMessage(char *text);

     void  SetText(char* newText);

     char* GetText();

          ~PrimitiveMessage();

};

以下,展示了,用于显示一个闪烁的"Hello World!"文字字符串的完整代码。显然,在代码中,有一些重复内容,它们的作用是,跟踪与这两个字符串相关的信息;这是因为,我们为PrimitiveMessage 类所设想的职责过少。以下会对每个部分的职责进行分析。

利用PrimitiveMessage 类实现的闪烁文字

Frame window("Blinking Text", Location(100,100), Shape(200,200));

PrimitiveMessage greeting("Hello World!");

Location greetingLocation(20, 50);

int onoff;         // 文字是否可见:可见=1,不可见=0

void OnStart()

{ window.Clear();

  window.DrawText(greeting.GetText(), greetingLocation);

  onoff = 1;

}

void OnTimerEvent()

{ if (onoff == 1) // 文字可见

  { Shape greetingShape = window.TextShape(greeting.GetText());

    window.Clear(greetingLocation, greetingShape);

    onoff = 0;

  }

  else           // 文字不可见

  { window.DrawText(greeting.GetText(), greetingLocation);

    onoff = 1;

  }

}

void OnPaint()

{ if (onoff == 1) // 文字可见

     window.DrawText(greeting.GetText(), greetingLocation);

}

在以上展示的闪烁文字系统中,有三个组成部分:PrimitiveMessage对象Frame对象;和那些简单编程环境函数(OnStartOnTimerEventOnPaint)。下表中,展示了,这三个组成部分的 职责。在分析了这些职责之后,妳可能会得出一个结论,那就是,那些简单编程环境函数承担了太多职责,而那些PrimitiveMessage 对象却承担了太少的职责。显然,可以合理地给出以下论点:PrimitiveMessage对象,应当,更多地跟踪那些与它的文字相关的信息,以及,更多地做出那些与它的文字的管理相关的动作。

组成部分

职责

PrimitveMessage对象

记录要显示的文字

Frame对象

绘制/擦除指定位置的指定文字

简单编程环境函数

什么时候绘制/擦除文字
文字的位置/形状
文字的状态(是否可见)
在哪个Frame中显示文字

步骤2:更多职责

用来代表闪烁文字这个抽象的那个类,其职责被增加了,使得,那些与文字相接相关的信息和处理动作,都成为这个类的一部分。于是,这个类,就会负责维护文字字符串的位置(Location)和形状(Shape)信息,负责擦除及绘制文字,也就是承担所有与文字密切相关的职责。对于这个变更,将导致一个隐含的变动,那就是,这个类必须能够访问到某个Frame 对象了,这样才能够:(1)在这个Frame 对象中绘制及擦除自身;以及(2)确定文字的形状(Shape),因为,必须利用Frame 类的TextShape 方法来从某个Frame 对象处获取到这个信息。变更后的职责分布,如下表所示。

组成部分

职责

Message对象

要显示的文字
文字的位置/形状
要在其中显示文字的Frame

Frame对象

在指定位置绘制/擦除指定的文字

简单编程环境函数

什么时候绘制/擦除文字
文字的状态(是否可见)

加入这些职责之后,就会改变这个类的接口,使得它符合之前开发的Message 类的特点。Message 类中的DisplayIn 方法,可用来与某个Frame 类对象进行关联。Message 对象就是在这个Frame对象里显示自身的,并且从这个Frame 对象里得知它的当前文字的形状(Shape)。由于Message类承担了管理自身位置(Location)的职责,所以,还有一个用于改变其位置(Location)的方法,也会成为Message 类的接口的一部分。

Message

class Message {

  private:

                //封装的实现

  public:

      Message (char *textString, Location whereAt);

      Message (Location whereAt);

void  DisplayIn (Frame&   whichFrame);

void  MoveTo (Location newLocation);

void  SetText(char* newText);

char* GetText();

void  Clear();

void  Draw ();

     ~Message ();

};

注意DisplayIn方法,以传递引用的方式来传递参数对象whichFrame;类名"Frame"之后紧跟的&符号,即表明以引用的方式传递对象。

利用Message 类实现的完整的闪烁文字系统,如下所示。由于职责上的变更,所以,Message类将要承担更多的关于自我管理的职责。其结果就是,OnTimerEvent()和OnPaint()方法的代码就变得简单了。

使用Message 类实现的闪烁文字

Frame window("Message Test", Location(100,100), Shape(200,200));

Message greeting("Hello World",  Location(20, 50));

int onoff;

void OnStart()

{ window.Clear();

  greeting.DisplayIn(window);

  greeting.Draw();

  onoff = 1;

}

void OnTimerEvent()

{ if onoff){greeting.Clear(); onoff = 0; }

  else     {greeting.Draw();  onoff = 1; }

}

void OnPaint()

{ if (onoff) greeting.Draw();

}          

这个版本的闪烁文字系统,跟之前那个未使用Message 类的版本相比,简单得多。Message类中,包含着一个辅助功能,使得Message 对象能够更好地管理自己。于是,要想从屏幕上擦除文字的话,只需要告诉Message 对象清除(Clear)自身即可 - 不需要直接操作Frame 对象。

步骤3:完整职责

我们甚至可以向闪烁文字抽象中赋予更多职责。注意,在Message 类的设计中,需要由简单编程环境函数来记住文字的状态(也就是说,文字当前是否可见)。如果将这个职责也移到闪烁文字类本身中去的话,那么,修改过的类,重命名为BlinkingMessage,其接口,如下所示。

BlinkingMessage

class BlinkingMessage {

  private:

                //封装的实现

  public:

      BlinkingMessage (char *textString, Location whereAt);

      BlinkingMessage (Location whereAt);

void  DisplayIn (Frame&   whichFrame);

void  MoveTo (Location newLocation);

void  SetText(char* newText);

char* GetText();

void  Blink();

void  Redraw();

     ~BlinkingMessage();

};

注意,在BlinkingMessage类中,Draw()Clear()两个方法,已经被替换成单个方法Blink()。这种替换,是为了强调,是由这个类的对象,而不是由这个类的用户,来决定,文字是要显示还是擦除。另外,需要注意,加入了一个Redraw()方法,这样,在无需知道文字是否可见的情况下,即可要求BlinkingText 刷新显示。在下面的OnPaint()方法中,即可看到,为何Redraw()方法是有必要的。

第三个版本,也即是闪烁文字系统的最终版本,如下所示,用到了BlinkingMessage 类。由于BlinkingMessage类会负责跟踪自己的状态,所以,在下面的代码中,已经不再需要"onoff"变量了。

利用BlinkingMessage 类实现的闪烁文字系统

Frame window("Message Test", Location(100,100), Shape(200,200));

BlinkingMessage greeting("Hello World",  Location(20,50));

void OnStart()

{ window.Clear()

  greeting.DisplayIn(window);

  greeting.Blink();

};

void OnTimerEvent()

{ greeting.Blink();

}

void OnPaint()

{ greeting.Redraw();

}          

对三种实现进行比较

在实现三个版本的闪烁文字系统的过程中,开发出来的这些类,展示了,在这些类的功能和复用性之间做权衡是多么困难。从这些示例来看,似乎BlinkingMessage类是最好的,因为,通过使用这个类,可以以最自然的方式来解决闪烁文字问题。然而,BlinkingMessage类的一个缺点就是,仅仅当文字是固定的并且需要闪烁时,才适合使用这个类。如果文字会变化,或者需要不闪烁地显示,那么,BlinkingMessage 类提供的功能就不合适了。换句话说,BlinkingMessage类,牺牲了一定程度的通用性,换来了,在解决特定问题时,一定程度的易用性。从这个角度来看,BlinkingMessage类是过度特化了。过度特化的类,在以下情况下,会工作得狠好:它们是专门用于处理手头上的问题的;并且,不大可能被(复)用于其它与它本身处理的问题类似但并不完全相同的问题。从这个角度来看,Message类达到了更好的平衡,因为,它的接口,适合于用在一些差异更大的问题中,只是它并不是完美地适合于其中任何一个问题。换句话说,Message类,通过限制其特化程度,从而提升了自己的复用性。这并不是说,Message类一定是处处都比BlinkingMessage类要好 - 如果妳正在处理的问题中,需要重复用到对于闪烁文字的抽象,那么,显示应当使用BlinkingMessage 类。最后,尽管PrimitiveMessage类看起来是三个版本中最差劲的一个,但是,在特定的情况下,例如,只需要存储一行文字,而不需要将它显示在窗口中的话,那么,BlinkingTextMessage类都不适合了,因为它们都包含有不需要的功能。在这种情况下,就可以优先选择PrimitiveMessage 类了。在这种评估中,所体现出来的是,有狠多因素的影响,导致设计过程变得狠复杂,而要确定“最好”的设计其实是狠难的。

这三个类, PrimitiveMessage Message BlinkingMessage ,表明了,需要一种比关联关系更强大的概念。 凭什么让设计者必须从这三种可能性中选择一种? 凭什么不能将这几种选项都保留,任人使用? 这种特性是狠值得期待的,因为,从某种意义上说, Message 类是对 PrimitiveMessage 类的特化, BlinkingMessage 类是对 Message 类的特化。 这些问题,可利用 层次关系 这个概念来解答, 这个概念,可用来开发一组相关的类集合。

简单计数器与定时器之间的关联关系


为了展示该如何构建具有关联关系的系统,会引入若干个新的类。这些类包括:一个简单的增加或减少的计数器;若干种类的按钮;以及,一个简单的定时器。尽管这些类都狠简单,但是,通过合理的配置,它们能够形成狠多有趣的系统。这些类,也是非常专门化定义出来的,因为,只使用了C++语言的一部分来定义它们。在日后学到的那些技巧,将用于扩展这些类的通用性。

我们会构建出由三个或四个对象组成的简单系统,它们能够对离散的事件进行计数,例如,独立的用户界面动作,或者,定时器发出的定时器事件。此处会使用已有的MessageFrame类。另外,还会定义两个新的类,CounterTimer,并将它们关联起来,组成一个小的系统。日后,将研究更复杂的示例。

Counter类,代表的是,一个简单的整数计数器,它能够向上计数或向下计数,具体取决于构造时的参数。Counter会在某个Message 中显示自己的当前值,如果那个Message对象本身被显示在某个Frame 对象中的话,那么,Counter 对象的这个值就会出现在屏幕上了。Reset方法,可用于将该个Counter 对象恢复到初始状态。Counter类的定义如下:

Counter

  class Counter {

     private:

                        // 封装的实现位于这里

     public:

          Counter (int start, int end); // 从start 开始,朝着end向上或向下计数

          Counter();                    // 从零开始向上计数

     void Next();                       // 增加/减少1个计数

     void Reset();                      // 重置成初始状态

     void Reset(int nowThis);           // 重置成特定的值

     void ConnectTo(Message& msg);      // 在此处显示当前值

         ~Counter();                    // 析构函数

   };

在第一个构造函数中,如果"start"小于"end",则该个Counter(计数器)的值每次增加1,否则它的值每次减少1。第二个构造函数,定义的计数器,会向上计数,每次增加1,且没有上限。Counter 的当前值,会显示在由ConnectTo 方法指定的Message 对象中。Counter 的当前 值,是由Next 方法来增加或减少的。每当通过Next 方法改变了Counter 的值,该个Counter 对象所连接到的那个Message 对象(如果连接了的话),就会被对应地更新,具体地是使用Message 对象的ChangeMessage 方法。Reset方法,会导致Counter对象恢复到它的初始状态。

以下代码中,展示了,一个简单的系统,它会对鼠标左键点击事件进行计数。在这个系统中,使用一个Counter来记录鼠标左键的点击事件次数,使用一个Message对象来显示Counter 对象的当前值,使用一个Frame对象来让用户可以看到显示的内容。

Frame window("Counter", Location(100,100), Shape (200,200));

Message countDisplay("", Location(10,10));

Counter clickCount;

void OnStart() {

countDisplay.DisplayIn(window);

clickCount.ConnectTo(countDisplay);

}

void OnPaint() {

countDisplay.Draw();

}

void OnTimerEvent() {}

void OnMouseEvent() {char *frameName, int x, int y, int buttonState) {

if (buttonState & leftButtonDown) {

clickCount.Next();

}

}

在这个示例中,需要注意到两个显而易见的点:

  • •.OnStart函数,将整个系统中各个部分连接起来。Message对象被关联到Frame对象,而Counter对象被关联到Message对象

  • •.OnMouseEvent几乎没做什么处理:每当探测到鼠标左键点击事件时,它只是简单地调用Counter对象Next方法

每当Counter 对象的Next 方法被调用时,会在系统的各个部分之间触发一系列的动作,使得:Counter对象的内部计数值增加;它所对应的Message对象的展示发生改变;Message对象会改变自己在用户可见的Frame 对象中显示的内容。

Clock类,是对系统内部定时器的抽象。Clock类,按照固定的时间间隔来增加某个Counter 的计数值。Clock的精度(也就是说,定时器的时间间隔),是在构造时设置的。Clock,可被启动,可被停止。Clock 类的定义如下:

Clock

  class Clock {

     private:

                // 封装的实现位于这里

     public:

           Clock (int interval);        // 时钟事件("ticks")之间的间隔,以毫秒为单位

      void ConnectTo(Counter& count);  // 在每次时钟事件("tick")中修改

      void Start();                     // (重新)启动时钟

      void Stop();                      // 停止Clock

   };

构造函数中,会指定连接的时钟事件("ticks")之间的间隔,以毫秒为单位。在每次时钟事件中,Clock会调用该个Clock 连接到的那个Counter 的Next 方法。ClockCounter之间的这种连接关系,是由ConnectTo 方法来建立的。StartStop方法,可用于控制该个Clock。

以下示例,展示了,如何使用Clock 和Counter 来构建一个简单的定时器系统:

Frame   window ("Timer", Location(100,100), Shape(200,200));

Message label("Seconds:", Location(10,10));

Message display("", Location(100,10));

Counter seconds;

Clock   timer(1000);

void OnStart() {

timer.ConnectTo(seconds);

seconds.ConnectTo(display);

display.DisplayIn(window);

timer.Start();

}

void OnPaint() {

display.Draw();

}

void OnTimerEvent() {}

void OnMouseEvent() {char *frameName, int x, int y, int buttonState) {}

这个示例中,创建了一个时间间隔为一秒的Clock,它连接到一个从0 (零)开始向上计数的Counter。这个Counter的值,由一个显示于window 这个Frame 中、标签为"Seconds"的Message 来表示。


任务

  1. 1.使用CounterMessageFrame类,编写一个程序,对鼠标左键点击事件进行计数,并在发生鼠标右键点击事件时重置计数值。

  2. 2.使用CounterMessageFrame类,编写一个程序。它包含两个计数器,其中一个显示鼠标左键点击事件的次数,另一个显示鼠标右键点击事件的次数。使用Message对象来在屏幕上以标签区分出两个计数器的值。

  3. 3.使用CounterMessageFrame类,编写一个程序,它会实现一个简单的定时器。使用OnTimerEvent函数来更新Counter 对象的值。

  4. 4.使用CounterMessageFrame类,编写一个程序,它会实现一个简单的定时器。使用OnTimerEvent函数来更新Counter 对象的值。使用鼠标左键点击事件来控制,是否要在OnTimerEvent中更新Counter对象。第一次鼠标左键点击,会启动计时(之后发生的OnTimerEvent函数调用,会使用Counter 对象的Next 方法来更新该Counter 对象)。第二次鼠标左键点击事件,会停止计时。之后,鼠标左键点击事件,会交替地启动及停止计时。

  5. 5.使用ClockCounterMessageFrame类,编写一个程序,它会使用两个Message,以两种不同的时间间隔来显示出已过去的时间。其中一个Message,显示某个每秒增加一次的Counter 对象的值。第二个message(消息对象),显示某个每隔十分之一秒增加一次的Counter 对象的值。使用另外两个Message来标识出,这两个变化的值分别表示"1 Second"(1秒)和"1/10 Second"(十分之一秒)。使用两个不同的Clock对象

DO WHAT WE WANT 做自己

先上第一道开胃小菜:

杨澜除了吴征之外,还有其他情人,其中有一个在上海崇明县,是开高尔夫球场的,杨澜你不用抵赖,崇明那边做生意的人多少知道这个事,反正崇明那么大,高尔夫球场就那么几个,很容易找出来的,除非你能堵住所有崇明做生意人的口。

Your opinions
Your name:Email:Website url:Opinion content:
- no title specified

HxLauncher: Launch Android applications by voice commands