异步执行与进度显示的可扩展结构模型——设计模式的应用:策略模式和装饰者模式的结合
本文通过日常开发中遇到的"异步执行和进度显示"的场景探讨策略模式与装饰者模式的应用。也许很多人都会遇到过这么一个场景:需要执行一系列的耗时很长的操作,在这过程中还需要不断地向界面显示操作的进度,通常以进度条、或者进度条+进度消息等形式展现。具体场景例如:文件的传送、下载,大量内容的搜索,程序的安装过程等等。在些场景里有一些共性,就是都可以分成两个部分:业务操作的执行、操作进度的显示。而由于一些特定环境,这两部分操作还不能放在一起执行,必需分开按异步方式运行,也就是分别在不同的线程中执行。Winform桌面程序就是一个例子:窗体程序需要不断地刷新,如果任务执行和显示都同处于窗体线程中执行,就会导致程序出现挂死的状态(鼠标变成漏斗状,窗口拖动时在目录留下大量白色块,无法刷新和查看窗口内容),必需以后台线程执行任务操作,使窗体线程保持界面刷新。
至此,我们得出了基本的需求:异步执行两种类型的操作:业务操作执行、进度信息显示。这就是场景的共性之处,与共性相对的是特例,每个人开发时面对的“业务操作”都不进相同,“进度显示”的界面也各不相同,这也是接下来面向对象设计的出发点:通过将特例抽象为共同接口。于是我们可以定义两个类型:后台任务BackgroundTask、进度视图IProgressView ,如下图。
BackgroundTask 具有三个方法:RunAsync //异步执行任务; ReportProgress //显示进度; Cancel //取消执行。
IProgressView 具有三个方法:Started //已经启动; ProgressChanged //进度已经变化; Completed //任务已经结束;
BackgroundTask 和 IProgressView 的方法都是上面场景中都会遇到的,因此上面的定义都很好的抽象了各自所代表的对象。任务需要将过程通知给视图,那对应的,BackgroundTask 应该持有 IProgressView 的实例。而不管具体的视图是什么样的,只要实现 IProgressView 接口提供给 BackgroundTask, 就能够获得进度通知。这样,很自然地就实现了一个经典模式:策略模式。
然而, IProgressView 接口对于实际需要的视图来说,还是过于抽象了,在具体的开发中还需要自定义不少的代码才能实现。为了提供进一步的便利,定义一个更具体化的视图的实现 FormProgressView,以简化以 Winform 窗体为视图的开发。如下图:
上图的 FormProgressView 是一个以 C# 语言特性所绘的类图, TFORM 是一个泛型参数。FormProgressView 本身也实现了 IProgressView 接口,同时其内部通过 View 属性持有一个 IProgressView 的实例,这个实例的实际类似是由 TFORM 泛型参数指定,是从 System.Windows.Forms.Form 继承并实现 IProgressView 接口的窗体,FormProgressView 将所有的显示的调用转交给该窗体实例执行。FormProgressView 类通过截获了 Started 通知以实现在处理开始后创建并显示窗体,之后在将通知转交给窗体,并在窗体关闭后负责销毁窗体。这样,根据这一结构,开发者就可以完全关注于具体用于呈现进度的窗体外观的设计了,而无需关注如何获取通知,窗体实例化及销毁,以及异步执行时跨线程的 Winform 控件访问的封送调用问题。由于这些与窗体外观无关的内容对于任何具体视图实现而言都是相同的,不需要改变,于是它们都完全被封装在了 FormProgressView 和 BackgroundTask 中,真正实现“对扩展开放,对修改关闭”的开闭原则。而这又是一个经典模式:装饰者模式。
至此,完成了一个基本的设计模型而已,以下将以 C# 代码实现上面的模型,对于其它语言的实现也是类似的。
- BackgroundTask 的实现
在上面没有提及关于 BackgroundTask 对于不同的具体任务操作的扩展性,而这也同样可以通过策略模式实现,但为了实现上的简便,直接使用 C# 语言里的委托来实现,即下图代码中的 TaskHandle 委托。
1 using System;
2 using System.Collections.Generic;
3 using System.Linq;
4 using System.Text;
5 using System.ComponentModel;
6 using System.Windows.Forms;
7 using System.Threading;
8
9 namespace HHQ.Foundation.WinForm.Components
10 {
11 /// <summary>
12 /// 要异步执行的任务的委托;
13 /// </summary>
14 /// <param name="sender"></param>
15 /// <param name="args"></param>
16 public delegate void TaskHandle(BackgroundTask sender, AsyncTaskArgs args);
17
18 /// <summary>
19 /// 后台任务;
20 /// 任务是 TaskHandle 委托的实例,在一个独立的后台线程被执行;
21 /// 如果 BackgroundTask 被用于 Winform 程序中,那么应该注意避免在任务中对在调用者线程中创建的 Winform 控件,因为 BackgroundTask 没有做任何跨线程调用控件的封送;
22 ///
23 /// 不过可以放心的是,如果提供的 IProgressView 的实现中包含在调用者线程中创建的控件,则可以在 ProgressChanged 和 Completed 方法中放心调用这些控件;
24 /// 因为 BackgroundTask 采用启动时调用者线程的 SynchronizationContext.Current 提供的同步对象进行封送调用;
25 /// 如果当前缺少同步上下文(即 SynchronizationContext.Current == null),则通过线程池中执行的视图通知;
26 /// </summary>
27 public class BackgroundTask
28 {
29 /// <summary>
30 /// 要异步执行的任务;
31 /// 注意:TaskHandler 属性不是一个事件,一个 BackgroundTask 只能指定一个 TaskHandle 作为 TaskHandler,
32 /// 指定新的值将覆盖以前设置的 TaskHandler ;
33 /// </summary>
34 public TaskHandle TaskHandler { get; set; }
35
36 private IProgressView _progView = NullProgressView.View;
37 /// <summary>
38 /// 进度通知视图;
39 /// </summary>
40 public IProgressView ProgressView
41 {
42 get
43 {
44 return _progView;
45 }
46 set
47 {
48 if (value == null)
49 {
50 _progView = NullProgressView.View;
51 }
52 else
53 {
54 _progView = value;
55 }
56 }
57 }
58
59 // 标识是否是用户调用 TryCancel 或 Cancel 方法要结束任务的执行;;
60 private bool _cancellationPending;
61 /// <summary>
62 /// 标识是否是用户调用 TryCancel 或 Cancel 方法要结束任务的执行;;
63 /// 只读属性;
64 /// </summary>
65 public bool CancellationPending
66 {
67 get { return _cancellationPending; }
68 }
69
70
71 // 指示当前是否正在运行;;
72 private bool _isRunning;
73 /// <summary>
74 /// 指示当前是否正在运行;;
75 /// 只读属性;
76 /// </summary>
77 public bool IsRunning
78 {
79 get { return _isRunning; }
80 }
81
82
83 /// <summary>
84 /// 内部的线程同步锁;
85 /// </summary>
86 private readonly object _syncLocker = new object();
87
88 private SynchronizationContext _syncCtx;
89
90 private Thread _thrd;
91
92 /// <summary>
93 /// 创建后台任务;
94 /// </summary>
95 public BackgroundTask()
96 {
97 _isRunning = false;
98 }
99
100 /// <summary>
101 /// 创建后台任务;
102 /// </summary>
103 /// <param name="task">任务处理委托</param>
104 /// <param name="progressView">进度通知视图</param>
105 public BackgroundTask(TaskHandle task, IProgressView progressView)
106 {
107 _isRunning = false;
108 TaskHandler = task;
109 ProgressView = progressView;
110 }
111
112 /// <summary>
113 /// 执行后台任务;
114 /// 方法将等待后台线程完成后才返回;
115 /// </summary>
116 /// <param name="argument">任务参数</param>
117 public void Run(object argument)
118 {
119 StartTask(argument, true, Timeout.Infinite);
120 }
121
122 /// <summary>
123 /// 异步执行任务;
124 /// 方法在启动线程之后将立即返回;
125 /// </summary>
126 /// <param name="argument">任务参数</param>
127 public void RunAsync(object argument)
128 {
129 StartTask(argument, false, 0);
130 }
131
132 /// <summary>
133 /// 开始执行任务;
134 /// </summary>
135 /// <param name="argument">任务参数</param>
136 /// <param name="waitForCompleted">是否等待完成</param>
137 /// <param name="waitTimeout">等待超时时长</param>
138 private void StartTask(object argument, bool waitForCompleted, int waitTimeout)
139 {
140 lock (_syncLocker)
141 {
142 if (IsRunning)
143 {
144 throw new InvalidOperationException("任务已经执行!");
145 }
146 _isRunning = true;
147 _cancellationPending = false;
148 }
149 if (TaskHandler == null)
150 {
151 throw new InvalidOperationException("未指定要执行的任务!");
152 }
153 _syncCtx = SynchronizationContext.Current;
154 _thrd = new Thread(DoTask);
155 _thrd.IsBackground = true;
156 _thrd.Start(argument);
157 if (waitForCompleted)
158 {
159 _thrd.Join(waitTimeout);
160 }
161 }
162
163 /// <summary>
164 /// 任务执行线程的入口;
165 /// </summary>
166 /// <param name="arg"></param>
167 private void DoTask(object arg)
168 {
169 AsyncTaskArgs args = new AsyncTaskArgs(arg);
170 bool canceled = false ;
171 Exception error = null;
172 try
173 {
174 NotifyStarted(args);
175
176 TaskHandler(this, args);
177
178 canceled = args.Cancel;
179 }
180 catch (ThreadAbortException ex)
181 {
182 canceled = CancellationPending;
183 error = ex;
184 Thread.ResetAbort();
185 }
186 catch (Exception ex)
187 {
188 canceled = CancellationPending;
189 error = ex;
190 }
191 finally
192 {
193 _isRunning = false;
194 _thrd = null;
195 NotifyCompleted(canceled, args.Result, error);
196 }
197 }
198
199 /// <summary>
200 /// 触发 ProgressView 的 Started 通知;
201 /// </summary>
202 /// <param name="args"></param>
203 private void NotifyStarted(AsyncTaskArgs args)
204 {
205 BackgroundTask sender = this;
206 if (_syncCtx != null)
207 {
208 _syncCtx.Post(delegate(object arg)
209 {
210 ProgressView.Started(sender, args);
211 },
212 null);
213 }
214 else
215 {
216 ThreadPool.QueueUserWorkItem(delegate(object arg)
217 {
218 ProgressView.Started(sender, args);
219 },
220 null);
221 }
222 }
223
224
225 /// <summary>
226 /// 触发 ProgressView 的 Completed 通知;
227 /// </summary>
228 /// <param name="canceled"></param>
229 /// <param name="result"></param>
230 /// <param name="error"></param>
231 private void NotifyCompleted(bool canceled, object result, Exception error)
232 {
233 if (_syncCtx != null)
234 {
235 _syncCtx.Post(delegate(object arg)
236 {
237 ProgressView.Completed(canceled, result, error);
238 },
239 null);
240 }
241 else
242 {
243 ThreadPool.QueueUserWorkItem(delegate(object arg)
244 {
245 ProgressView.Completed(canceled, result, error);
246 },
247 null);
248 }
249 }
250
251 /// <summary>
252 /// 触发 ProgressView 的 ProgressChanged 的执行;
253 /// </summary>
254 /// <param name="percent"></param>
255 public void ReportProgress(int percent)
256 {
257 ReportProgress(percent, null);
258 }
259
260 /// <summary>
261 /// 触发 ProgressView 的 ProgressChanged 的执行;
262 /// </summary>
263 /// <param name="percent"></param>
264 /// <param name="state"></param>
265 public void ReportProgress(int percent, object state)
266 {
267 if (_syncCtx != null)
268 {
269 _syncCtx.Post(delegate(object perc)
270 {
271 int p = (int)perc;
272 ProgressView.ProgressChanged(p, state);
273 },
274 percent);
275 }
276 else
277 {
278 ThreadPool.QueueUserWorkItem(delegate(object perc)
279 {
280 int p = (int)perc;
281 ProgressView.ProgressChanged(p, state);
282 },
283 percent);
284 }
285 }
286
287 /// <summary>
288 /// 请求取消;
289 /// 此方法不会终止后台线程,而是仅将 CancellationPending 置为 true,由后台执行的方法通过检查该属性自行退出;
290 /// </summary>
291 public void TryCancel()
292 {
293 lock (_syncLocker)
294 {
295 if (!IsRunning)
296 {
297 throw new InvalidOperationException("当前任务未运行!");
298 }
299 _cancellationPending = true;
300 }
301 }
302
303 /// <summary>
304 /// 强制中断任务线程;
305 /// 此方法将使 CancellationPending 置为 true 后,对任务线程执行 Abort 操作;
306 /// 操作在任务线程上引起的 ThreadAbortedException 也将反映在 ProgressView 的 Completed 通知上;
307 /// 此方法不等待任务线程的退出就立即返回;
308 /// </summary>
309 public void Cancel()
310 {
311 TryCancel();
312
313 _thrd.Abort();
314 }
315 }
316 }
- FormProgressView 的实现
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace HHQ.Foundation.WinForm.Components
{
/// <summary>
/// 进度通知视图,用于响应 BackgroundTask 的进度告或完成的信息;
///
/// 注意:视图定义的三个通知(Started, ProgressChanged, Completed)之间的是异步执行的,
/// 能够保证的是 Started 通知肯定是第一个出现的, Completed 通知是最后出现的,ProgressChanged 通知在执行 BackgroundTask.ReportProgress 后出现,介于 Started、Completed 通知之间;
/// </summary>
public interface IProgressView
{
/// <summary>
/// 任务开始执行;
/// </summary>
/// <param name="task"></param>
/// <param name="args"></param>
void Started(BackgroundTask task, AsyncTaskArgs args);
/// <summary>
/// 进度改变通知;
/// </summary>
/// <param name="percent"></param>
/// <param name="data"></param>
void ProgressChanged(int percent, object data);
/// <summary>
/// 进度完成通知;
/// </summary>
/// <param name="cancelled"></param>
/// <param name="result"></param>
/// <param name="error"></param>
void Completed(bool cancelled, object result, Exception error);
}
}
1 using System;
2 using System.Collections.Generic;
3 using System.Linq;
4 using System.Text;
5 using System.Windows.Forms;
6 using System.Threading;
7
8 namespace HHQ.Foundation.WinForm.Components
9 {
10 /// <summary>
11 /// FormProgressView 提供了 Winform 窗体实现的进度通知视图的适配器;
12 /// 泛型参数 TFORM 必需是一个实现了 IProgressView 的 Winform 窗体的类型,并且定义一个不带参数的构造函数;
13 ///
14 /// FormProgressView 维护指定类型的窗体的实例,默认情况下,在启动任务后自动以模态显示窗体;
15 /// 在自动显示(AutoShowOnStart == true)并且以模态显示(IsModal==true)时,关闭窗体不会销毁之前的窗体实例;
16 /// 除此种情形之外,窗体的关闭都会销毁实例,在下一次报告进度或者任务重新启动时,将会使用一个新的窗体实例;
17 ///
18 /// 因此,开发者不应该依赖于窗体的特定实例而实现任务进度消息的显示;
19 /// </summary>
20 public class FormProgressView<TFORM> : IProgressView
21 where TFORM : Form, IProgressView, new()
22 {
23 private readonly object _syncLocker = new object();
24
25 private TFORM _view;
26 /// <summary>
27 /// 创建返回视图的实例;
28 /// </summary>
29 private TFORM View
30 {
31 get
32 {
33 if (_view == null)
34 {
35 lock (_syncLocker)
36 {
37 if (_view == null)
38 {
39 TFORM form = new TFORM();
40 form.FormClosed += new FormClosedEventHandler(form_FormClosed);
41 _view = form;
42 }
43 }
44 }
45 return _view;
46 }
47 }
48
49 /// <summary>
50 /// 是否在获取 Started 通知后自动显示窗体;
51 /// </summary>
52 private bool _autoShowOnStart = true;
53 /// <summary>
54 /// 是否在获取 Started 通知后自动显示窗体;
55 /// 在创建视图时通过构造函数指定,默认为 true;
56 /// 只读属性;
57 /// </summary>
58 public bool AutoShowOnStart { get { return _autoShowOnStart; } }
59
60 /// <summary>
61 /// 窗体是否以模态显示;
62 /// </summary>
63 private bool _isModal = true;
64 /// <summary>
65 /// 窗体是否以模态显示;
66 /// 在创建视图时通过构造函数指定,默认为 true;
67 /// 只读属性;
68 /// </summary>
69 public bool IsModal { get { return _isModal; } }
70
71 /// <summary>
72 /// 创建窗体进度通知视图;
73 /// </summary>
74 public FormProgressView()
75 {
76 }
77
78 /// <summary>
79 /// 创建窗体进度通知视图;
80 /// </summary>
81 /// <param name="autoShowOnStart">是否自动在启动时自动显示窗体</param>
82 /// <param name="isModal">是否以模式方式显示窗体</param>
83 public FormProgressView(bool autoShowOnStart, bool isModal)
84 {
85 _autoShowOnStart = autoShowOnStart;
86 _isModal = isModal;
87 }
88
89 /// <summary>
90 /// 窗体关闭时清空对窗体的引用;
91 /// </summary>
92 /// <param name="sender"></param>
93 /// <param name="e"></param>
94 private void form_FormClosed(object sender, FormClosedEventArgs e)
95 {
96 if (_autoShowOnStart)
97 {
98 //如果窗体是以非模态方式(Show 方法)打开的,则关闭之后会销毁,此后不能继续使用该窗体,
99 //因此需要清空对其的引用,并在需要时重建实例;
100 //在以下两种情况下调用 Close 不会释放窗体:(1) 窗体是多文档界面 (MDI) 应用程序的一部分且是不可见的;(2) 您是使用 ShowDialog 显示的该窗体。
101 if (!_isModal)
102 {
103 _view = null;
104 }
105 }
106 else
107 {
108 //由实现者自行显示窗体时,当窗体关闭后总是清空窗体的引用;
109 _view = null;
110 }
111 }
112
113 #region IProgressView 成员
114
115 public void Started(BackgroundTask task, AsyncTaskArgs args)
116 {
117 this.View.Started(task, args);
118 if (_autoShowOnStart)
119 {
120 if (_isModal)
121 {
122 this.View.ShowDialog();
123 }
124 else
125 {
126 this.View.Show();
127 }
128 }
129 }
130
131 public void ProgressChanged(int percent, object data)
132 {
133 this.View.ProgressChanged(percent, data);
134 }
135
136 public void Completed(bool cancelled, object result, Exception error)
137 {
138 this.View.Completed(cancelled, result, error);
139 }
140
141 #endregion
142 }
143 }
144
- 异步执行与进度显示的可扩展结构模型的使用
1 //创建后台任务实例;
2 BackgroundTask task = new BackgroundTask();
3
4 //指定任务进度显示的窗体视图;
5 //泛型参数 TFORM 必需是一个实现了 IProgressView 的 Winform 窗体的类型,并且定义了一个不带参数的公开的构造函数;
6 task.ProgressView = new FormProgressView<SimpleProgressDialog>();
7
8 //指定任务的具体实现;指定 TaskHandle 委托的实例;
9 task.TaskHandler = delegate(BackgroundTask sender, AsyncTaskArgs args)
10 {
11 //任务的实现;
12 //在独立的后台线程被执行;不能包含访问其它线程创建的窗体、控件的代码;
13
14 // Run 或 RunAsync 指定参数;
15 //object param = args.Argument;
16
17 //报告处理进度;
18 //sender.ReportProgress(10);
19 //sender.ReportProgress(20, "正在执行注册……");
20
21 //保存完成结果;操作结果将反映到进度视图的 Completed 通知上;
22 //不应使用抛出异常的方式结束处理,可以通过将 args.Cancel 置为 true 以通知视图任务是被取消的;
23
24 //args.Cancel = false; //true-指示是否操作未完成而取消退出;
25 //args.Result = data; //保存结果;
26 };
27
28 //以指定的参数实行任务;
29 task.RunAsync(null);
总结一下,上述的结构设计的特点主要是将异步执行与进度显示这一通用场景中的固定逻辑抽象了出来,也就是多线程异步调用和异步通知的逻辑,同时也很好地支持了扩展,即对不同的视图,不同的任务实现的扩展。在这个例子中,通过分析识别场景中的共性与个性、不变与变化,便自然形成了一个面向对象的设计,在反观设计便发现这就是经典模式的应用。
附件中是上述的组件的可直接使用的版本。
注:《设计模式》该如何应用到实际开发中是我们学习面向对象设计时应该着重思考的,出于此目的,本人想通过《设计模式的应用》系列文章记录个人在日常开发中的心得体会,这篇文章就是开端。