《C++面向对象软件设计及构建》文章翻译:4.6 更复杂的静态聚合 ,4.6 More Complex Static Aggregation
在本小节中,会实现一个StopWatch类,用于展示一种更复杂的聚合。在这种更复杂的聚合中,那些聚合起来的子对象的功能更多,并且,要做到正确地构造出这样一个聚合的话,更具有挑战性。在考虑该如何构造这样一个更精细的聚合之前,会先描述一个通用的设计问题,这其中,牵涉到,这个聚合中的那些内部子对象,要在多大程度上被这个聚合的公有接口所暴露出来。这个问题,被称作 间接控制问题 。
间接控制问题
在使用聚合时,一个狠重要的设计问题就是,要在多大程度上,将对被封装的对象的间接控制反映到聚合类的公有接口上去。间接控制,指的是,对象的用户,能够通过聚合类的公有接口对子对象的具体组织形式或操作方式进行影响的能力。在StopWatch示例中,将面临着以下这些间接控制问题:
•.在窗口中,要在什么位置(Location)显示那两个按钮(Button)和那个文字框(Textbox)?
•.在窗口中,那些按钮(Button)与那个文字框(Textbox)之间应当具有什么样的位置关系?按钮(Button)位于一侧?按钮(Button)位于下方?按钮(Button)位于上方?
•.定时器的时间间隔是多少?长度是固定的吗?用户可以选择吗?由程序来设置吗?
•.文字框(Textbox)中可以出现多少数字?
•.出现在窗口中的那些按钮(Button),它们的名字是什么?
•. 这个StopWatch对象,一定要从时刻零开始吗?还是说,可以给它赋予一个初始值?
•. 这个Clock对象,能否被程序所启动/停止?只能由用户来操作?还是说二者都可以?
如妳所见,这些问题,牵涉到StopWatch 类中所有被封装的子对象了。
理想情况下,不应当允许对于被封装的对象有任何的间接控制,然而,这种理想并非总是合理的,也并非总是可行的。然而,也需要注意,过度地允许对被封装的对象进行间接控制,也将会削弱聚合的好处。在极端情况下,外层的聚合类完全放弃对于子对象的控制,这样,就导致,它本身变成一个弱封装,只是用于将各个子对象集合到一个分组中而已。一个好的类,它的设计者,必须达到一个平衡,既能提供对于被封装的子对象的足够多的间接控制,以便能够在多种不同的应用中被使用,同时又不能提供过多的间接控制,以致于丢失了聚合的好处。
聚合类的设计者,在处理子对象的控制问题时,有多种选择可以做。首先,可以设计多个类似的类,每个类各自提供不同程度的控制能力。这种方式,付出的代价是,需要创建及命名多个类,其好处是,对于那些可能需要不同程度地针对子对象拥有控制能力的程序员来说,提供了一系列的选择机会。第二, 聚合类的接口中,可以使用默认参数,这样,当这些参数未被指定时,控制权将又回归到聚合类本身。尽管这种方式同样提供了一系列的选择机会,但是,参数列表将会更加复杂,更加难以设计。取决于那些默认参数的顺序,在实际使用过程中,用户可能会发现,必须使用相对于预期来说更多的控制能力。第三,可以定义一个或多个辅助类,用于指定控制信息。这种辅助类的一个实例,会被作为参数传递给聚合类。这个参数,也可以拥有一个默认值,或者,聚合类也可以提供一个默认构造函数,使得用户能够做到不获取任何间接控制能力。具体对于StopWatch类来说,用户界面组件的布局信息,可放置在一个StopWatchLayout 类中,这个类将能够包含对于StopWatch 的所有位置及标签信息。StopWatchLayout 的默认构造函数将为这个信息提供标准的(默认)值。StopWatch 类的构造函数中,将会有一个StopWatchLayout 参数,这个参数的默认值,即是,由StopWatchLayout 类的默认构造函数所构造的一个StopWatchLayout 对象。
以下给出了StopWatch 类的定义。在它的公有接口中,提供了以下方法:在程序的级别上启动及停止 这个StopWatch;指定一个框(Frame),使得StopWatch 能够将自身的用户界面部件(TextBox和Button)显示于其中;以及,向StopWatch 查询,它当前的消逝时间。
|
class StopWatch { private: Button startButton; Button stopButton; Clock clock; Counter clockCount; Message clockDisplay; Panel buttonPanel; Canvas canvas; public: StopWatch(Frame& frame, Location where, int interval = 1000); void ButtonPushed(char* buttonName); void Tick(); int ElapsedTime(); ~StopWatch(); }; |
注意,在这个设计中,对于之前所提到的控制问题,做出了一些决策。这并不是,这些决策就是最正确的决策,只是说,它们比较合理,并且能够起到示例作用。
StopWatch类的方法,可以实现如下:
|
void StopWatch::ButtonPushed(char* buttonName) { if (startButton.IsNamed(buttonName)) clock.Start(); else if (stopButton.IsNamed(buttonName)) clock.Stop(); } void StopWatch::Tick() { clockCount.Next(); } int StopWatch::ElapsedTime() { return clockCount.Value(); } StopWatch::~StopWatch() {} |
这砣代码,展示了,StopWatch 类中的那些方法,是如何通过操作内部子对象来实现自己的效果的。例如,为了让StopWatch能够针对按钮的按下事件做出响应,以启动或停止自身,就在ButtonPushed 方法中简单地调用Clock 子对象的Start()或Stop()方法。类似地,StopWatch 的elapsed 方法,也只是简单地向Counter 子对象查询其值。
StopWatch 类的构造函数并未在此处包含进来,我们将会在下一节展示那个方法,到时,会单独地说明与子对象的构造相关的概念。
构造子对象
聚合类的构造函数,必须确保,它的那些子对象都被正确地初始化了。例如,外界的预期是,当某个StopWatch 对象被构造完毕时,它的所有的子对象都应当已经被正确地构造完毕了,并且是可用的了。
子对象的构造函数,可能会以以下三种形式来与外层聚合类的构造函数关联起来:
•.独立:子对象的构造函数是固定的,与外层聚合类的参数无关。
•.直接:子对象的构造函数,直接依赖外层聚合类的构造函数中的一个或多个参数。
•.间接:子对象的构造函数,依赖着从外层聚合类的构造函数中的参数中计算出来的一个或多个值。
下面,用图片表示了这些关系。
|
|
在独立(independent)那种情况中,子对象拥有一个固定的构造函数,它并不以任何方式依赖于外层聚合类的构造。例如,在StopWatch的设计中,无论StopWatch 对象的其它属性被构造成什么值,那个Counter对象,一定会被初始化为零。同样地,Start按钮、Stop按钮和Message对象,在构造过程中也都不会引用到StopWatch 构造函数中的任何值。
对于直接(direct)的情况,则由StopWatch 类中的Clock 子对象来展示。在此处,Clock 对象的构造函数参数,即,定时器时间间隔,是直接从StopWatch 构造函数的参数中获取的。对于,这个值,未作任何改变。
对于间接(indirect)的情况,是由Canvas 和Panel 的构造来展示的。在StopWatch的构造函数中,有一个Location参数,这两个用户界面对象的位置,将会相对于这个参数来放置。假设各个用户界面子对象按照下图来布置:
|
|
根据上面的布局图,将能够确定出各个用户界面子对象的位置,如下表所示。在这些子对象的位置的计算过程中,需要调用StopWatch 构造函数的参数(where)的方法(Xcoord和Ycoord),再进行简单的加法计算,再构造一个新的(匿名)Location 对象。
用户界面子对象的位置(Location)和形状(Shape) |
|
Message: |
位于Canvas 中,Location(60,10) |
StartButton: |
位于Panel 中,Location(10,10) |
StopButton: |
位于Panel 中,Location( 70,10) |
Canvas: |
位于Frame 中,Location(where.Xcoord() + 10, where.Ycoord() + 20) |
Panel: |
位于Frame 中,Location(where.Xcoord() + 10, where.Ycoord() + 60) |
在C++中,子对象的构造函数,是按照以下一般形式来放置的:
ClassName::ClassName( <参数列表> ) : < 子对象构造函数列表 >
{ <构造函数 主体 > }
其中,<子对象构造函数列表>,是由逗号分割的,子对象构造函数的列表。这个列表中,只能包含子对象的构造函数。
根据前面所做的布局决定,以及刚刚介绍的C++语法,就可以按照如下的形式写出StopWatch 类的构造函数。
|
Location StartButtonLocation = Location(10,10); Shape StartButtonShape = Shape(50,20); Location StopButtonLocation = Location(70,10); Shape StopButtonShape = Shape(50,20); Location ButtonPanelLocation = Location(10,60); Shape ButtonPanelShape = Shape(130,40); Location CanvasLocation = Location(10,20); Shape CanvasShape = Shape(130,30); Location ClockDisplayLocation= Location(60,10); StopWatch::StopWatch(Frame& frame, Location where, int interval) : // 子对象构造函数列表 clockCounter(0), clock("StopWatchClock", interval), buttonPanel( frame, "ButtonPanel", Location(where.Xcoord() + ButtonPanelLocation.Xcoord(), where.Ycoord() + ButtonPanelLocation.Ycoord()), ButtonPanelShape), startButton("Start", StartButtonLocation, StartButtonShape), stopButton ("Stop", StopButtonLocation, StopButtonShape), canvas ( frame, "StopWatchCanvas", Location(where.Xcoord() + CanvasLocation.Xcoord(), where.Ycoord() + CanvasLocation.Ycoord()), CanvasShape), clockDisplay( "0", ClockDisplayLocation) { // 构造函数主体 buttonPanel.Add(stopButton); buttonPanel.Add(startButton); canvas.Clear(); clockCount.ConnectTo(clockDisplay); clockDisplay.DisplayIn(canvas); clock.Start(); } |
任务
1. 向StopWatch 类中添加一个文字标签(一个Message子对象),使得,这个文字标签显示在已有的那个用于显示StopWatch 的数值的Message 对象的上方。向StopWatch 的构造函数中添加适当的构造函数参数,以便向该个文字标签提供一个字符串值。
2. 实现 并测试在4.2 小节中提到 的 SimpleTimer 类。
3. 实现 并测试在前文所说的 InternalTimer 类。
4.修改StopWatch类,使得,对于用户界面子对象的布局,给出更多的控制能力,具体做法就是,在 StopWatch 的构造函数中加入一些默认参数值。
5. 实现 并测试前文所说的 StopWatchLayout 类。
6.实现并测试StopWatch 类的一个修改版本,使得,用户能够在不修改代码的情况下,对布局进行自定义。具体地,这个自定义功能的实现,应当是,在构造StopWatch 对象的时候,从某个文件中读取布局信息。
未知美人
未知美人
Your opinionsHxLauncher: Launch Android applications by voice commands