Fork me on GitHub
Build your own CAB(Composite Application Block) Part #2 – The Humble Dialog Box

自己动手写CAB(Composite Application Block) #1 —— 前言

作者:Jeremy Miller 翻译:Yanwei

昨天本人提出了一个不负责任的观点。这个观点是,如果你要写一个可维护的,复杂的WinForms界面,并不需要Composite Application Block(CAB)。我觉得,开发人员如果掌握了CAB的底层设计模式,并且挑选一个不错的IoC/DI工具,就可以开发一个满足需求的设计。我甚至感觉,这个开发人员,如果对CAB功能很熟悉,就能够很容易地开发出一个性能更好的设计。为了证明我的这个观点,并且满足很多人对这个主题的好奇心,我将把DevTech上关于WinForms模式的演讲总结一下,写成一系列文章,来向大家展示,为了写出可维护的WinForm代码,而采用的一些设计思路。即使你坚持使用CAB(也没关系),我希望,你能跟着我一起揭开CAB的神秘面纱。在分析实现中的一些模式后,能帮助你把CAB玩的更加得心应手。如果你觉得在某些方面,CAB比我的做法更好,请一定告诉我。

我最喜欢的作家之一是大仲马。他写过《三个火枪手》和《基督山伯爵》。《三个火枪手》是那个时代的肥皂剧,被写成很多章节连载在当时的杂志上。同样的,我也将把我的文章写成很多小章节。每一章节的长度就是我上下班做火车的时间内写出的长度。

关于文章中出现的术语——我是一个“跟屁虫”,所以我就用Martin Fowler关于企业设计模式丛书中的术语。

不就是UI么?

不就是UI么?最近这种言论不常见了,但我刚开始参加工作的时候,很多人都觉得,写UI代码很简单,且没有技术含量,真正的程序员是不应该关注的。我经常想起“小菜一碟”这个成语。这种态度不对,因为在很多系统中,UI代码比服务器端代码复杂的多。在如何设计UI代码上,多花时间好好琢磨一下,是绝对有必要的。下面我来好好论述一下:

  • 写UI要花大量时间成本。不仅仅是指开发成本,根据我的经验,UI代码bug最多,因此维护成本最大。我们可以为UI代码尽可能多的写单元测试,来减少bug。黑TDD的人经常拿UI当例子,叫嚣有些代码就是不能测。他们错了,但是UI确实不好测。你需要多花点力气组织代码,让它们更容易测。
  • 林子大了,什么样的用户都有,UI代码面临的是五花八门的输入数据。想要系统不崩溃,就一定得花点心思处理各种变态的输入数据。
  • 界面频繁更改。我的经验再一次告诉我,比起后台逻辑,前端界面更容易变动。让UI能够拥抱变化,绝对是有意义的,只不过,需要好好设计。
  • UI代码大部分都是事件驱动的,而事件驱动的代码debug起来很恶心。所以让代码解耦,可测试,你就不用debug了,或者至少让debug变得简单一些。

设计模式之我见

我知道,很多人都对设计模式不屑一顾,认为设计模式是“繁文缛节”,不接地气。但我认为,在设计UI的时候,设计模式异常重要。在讨论设计的时候,设计模式给我们提供了通用的术语。这些模式凝聚了前人的智慧。这个系列文章中涉及到的模式,让我们在面对,怎样使UI代码职责分离的问题上,提供了很好的思路。我这么说,是因为...

UI代码中有很多不同的职责

一件近乎搞笑的事情是,DevTech上的每一个演讲者,包括我在内,至少有一个PPT页面赫然写着:单一职责(SRP)。再啰嗦一下,单一职责说的是任何一个类,“只能有一个理由去变化”。也就是说,你应该努力让你的每一个类只拥有一个内聚的职责,也就是说,只关心一件事情。我上面提到过,UI代码很容易变得很复杂。口说无凭,下面我们来列一下,一个最基本的界面,会包含哪些职责:

  • 用户看到的静态界面
  • 绑定事件,处理用户输入
  • 根据业务逻辑,验证用户输入
  • 不同用户,有不同的权限
  • 不同用户,有不同的定制
  • 界面的切换,以及相关信息的传递
  • 与后台交互

这些都是多么重要的事情啊!如果这些代码我分而写之,那么问题就会被各个击破。我也非常想把这些代码分而测之。回到单一职责,如果我把这些职责都放在各自的代码中,就能更好的拥抱变化,易于维护。我们跪求的就是把这些职责分开的思路和策略。

当然,如今开发WinForms,最常见的分离职责的模式是...

View自治模式

你可能不会踱着步子,念念有词“我正在用‘View自治模式’写应用程序 ”,但一说你心里就明白。所谓View自治模式,指的是把界面的显示和界面的事件封装在一起。在WinForms开发中,View自治指的就是封装良好的Forms and UserControlls。当然这样的封装,对于只有几个UI界面的小应用非常合适。但是由于显示和行为耦合在了一起,应用越复杂,这种设计,在维护方面,就会越力不从心。当然在我看来,最大的问题,其实相比POCO类,这种自治View很难被单元测试。而且因为和WinForms内部实现机制耦合在一起,View自治的代码也会更加难读懂。当然你可以在内存中实例化WinForms对象,对这些对象进行单元测试,但写这样的测试困难重重,得不偿失。

 
分类: 道听途说

自己动手写CAB(Composite Application Block)  #2 —— 低调的对话框

作者:Jeremy Miller 翻译:Yanwei

话说上一回,达达尼昂为了阻止德温特小姐卑鄙的计划,奋起直追, 跨越了整个法兰西的长度,却突然发现落入“View自治模式”陷阱。陷阱里面有迥然不同的职责盘根错节。达达尼昂作为一个机智的剑士,敏锐的发现了分离不同职责的方法,他发动了攻击……

低调的对话框

我必须强调,软件设计的第一要务就是解耦,各个击破!想吃大象,一次只能只一口。要学会怎样把一个复杂的问题分写成为一系列可以轻松达成的小问题。当我做页面的时候,我就会一个任务做完,再做下一个,而不是并行的做所有的任务。职责分离带来的最直接好处是,编程序变得简单了,但是还有另一个重要的目的。由于UI代码很难debug,而且根据经验,很容易改变,所以我真的,非常非常希望,用尽可能多的自动化单元测试,来覆盖前端展现层。

据前人经验,除非认真对待,否则UI代码很难测。为UI写自动化测试得不偿失。但是受Michael Feathers的文章The Humber Dialog Box启发,过去几年不同的框架的崛起,UI难测的情况已然改变!

下面我将用经典的“低调对话框”的案例来解释这个观点。假设你有一个窗口,用来展示若干条数据,还要实现下面的需求:

如果用户修改了数据,但未保存,当尝试关闭窗口时,就会弹出一个对话框,告诉用户,未保存的数据会丢失。如果用户希望保留这个改变,就不能关闭窗口。如果数据没有被修改,那么关闭窗口时,不应弹出对话框。

真没什么难度,但这种需求能够让客户用起来方便很多。写出来的代码大概会像下面这样:

复制代码
1 private void ArrogantView_Closing(object sender, CancelEventArgs e)
2         {
3             // 这里不用考虑怎样知道是否存在脏数据
4             if (isDirty())
5             {
6                 bool canClose = MessageBox.Show(“Ok to discard changes or cancel to keep working”) == DialogResult.OK;
7                 e.Cancel = !canClose;
8             }
9         }
复制代码

代码不复杂,接下来考虑怎样用我们的集成测试框架写一个自动化回归测试。对Message.Show()的小小调用和那个弹出的模态对话框,成为我们自动化测试深深的痛(我之前写出过这样的测试,不过我强烈建议,你还是别试了,继续往下看吧!)。在自动化回归测试的时候,观察对话框是否被关掉让人郁闷,不过我觉得最让人崩溃的是,为了测这个逻辑,你不得不打开这个窗口,改点数据,触发关闭窗口的请求。你想跑一下你关心的这一点代码,就得做这么多工作。

现在,我们把这个功能重写一下,把对话框变得“低调”一点。在我展示代码之前,先讲一讲代码背后的思想。第一件事,让View减减肥。WinForms的UserControl或Form中的任何代码,都比一个POCO类难测。一个“低调”的View,应该只包装真正的界面展示的代码。进一步考虑,我不像让View的实现细节,暴露给其他的代码,因此,我需要一个接口,来隐藏的View的实现细节。那么,这里的View,抽象接口的代码可能会像这样:

1 public interface IHumbleView
2     {
3         bool IsDirty();
4         bool AskUserToDiscardChanges();
5         void Close();
6     }

这是个“被动”的View,意味着,除非View外部发请求,否则,View本身不做任何事情。后面的文章我会深度讨论事件绑定的细节,现在,我们就假设View把用户事件传递到某个地方做处理。

低调的View的目标之一就是分离职责。大多数的设计,我们会把不同的职责分配到不同的代码。这个例子中,我们想要把view中的行为逻辑抽出来,放到非展示层的类中。如果我们把View瘦了身,把与展示层无关的代码全部都抽出来,那么这些抽出的代码(可能是逻辑行为或授权之类的),必须放在一个地方。这个例子中,我们把这些逻辑放在一个Presenter类中:

复制代码
 1 public class OverseerPresenter
 2     {
 3         private readonly IHumbleView _view;
 4  
 5         public OverseerPresenter(IHumbleView view)
 6         {
 7             _view = view;
 8         }
 9  
10         public void Close()
11         {
12             bool canClose = true;
13             if (_view.IsDirty())
14             {
15                 canClose = _view.AskUserToDiscardChanges();
16             }
17  
18             if (canClose)
19             {
20                 _view.Close();
21             }
22         }
23     }
复制代码

 

让我们分析一下Close()方法。一些用户行为可能触发了这个方法。在方法内部,我们先检查IHumberView的dirty状态,然后询问用户,在关掉窗口之前是否丢弃更改。与之前代码差不多,只不过现在我们能够为之写单元测试了——需要我们的好朋友RhinoMocks的一点点帮助。

复制代码
 1 [TestFixture]
 2     public class OverseerPresenterTester
 3     {
 4         [Test]
 5         public void CloseTheScreenWhenTheScreenIsNotDirty()
 6         {
 7             MockRepository mocks = new MockRepository();
 8             IHumbleView view = mocks.CreateMock<IHumbleView>();
 9  
10             Expect.Call(view.IsDirty()).Return(false);
11             view.Close();
12  
13             mocks.ReplayAll();
14  
15             OverseerPresenter presenter = new OverseerPresenter(view);
16             presenter.Close();
17  
18             mocks.VerifyAll();
19         }
20  
21         [Test]
22         public void CloseTheScreenWhenTheScreenIsDirtyAndTheUserDecidesToDiscardTheChanges()
23         {
24             MockRepository mocks = new MockRepository();
25             IHumbleView view = mocks.CreateMock<IHumbleView>();
26  
27             Expect.Call(view.IsDirty()).Return(true);
28             Expect.Call(view.AskUserToDiscardChanges()).Return(true);
29             view.Close();
30  
31             mocks.ReplayAll();
32  
33             OverseerPresenter presenter = new OverseerPresenter(view);
34             presenter.Close();
35  
36             mocks.VerifyAll();
37         }
38  
39         [Test]
40         public void CloseTheScreenWhenTheScreenIsDirtyAndTheUserDecidesNOTToDiscardTheChanges()
41         {
42             MockRepository mocks = new MockRepository();
43             IHumbleView view = mocks.CreateMock<IHumbleView>();
44  
45             Expect.Call(view.IsDirty()).Return(true);
46             Expect.Call(view.AskUserToDiscardChanges()).Return(false);
47  
48             // No call should be made to view.Close()
49             // view.Close();
50  
51             mocks.ReplayAll();
52  
53             OverseerPresenter presenter = new OverseerPresenter(view);
54             presenter.Close();
55  
56             mocks.VerifyAll();
57         }
58     }
复制代码

 

我知道你肯定会问,通过这个例子,我学到了什么?让我试着给你个答案:

  • 正交性。我们把行为逻辑从View中拿了出来。我们可以独立的改变View和Behavior的逻辑。赚大了。
  • 界面的行为逻辑更容易被理解。这个观点从Reg Braithwaite’s Signal to Noise Ratio in code来(简单讲就是说,展示了代码的用途,但并没有展示实现细节)。当我想要理解界面的行为时,我只想知道大概的行为。看到(object sender, CalcelEventArgs e)里面的任何实现细节,都是一种干扰。相反,对于实现层面的代码,也是一个道理。
  • 界面的行为更容易被修改和测试。相信上面低调的View已经足够说明了。如果明天需求改了,改成了用户可以自定义喜好,永远不弹出对话框怎么办?如果我的代码是“低调”风格的,我就可以把实现代码和单元测试一并改了。除非手动测试,否则根本不需要启动UI界面进行查看。单元测试比继承测试的反馈周期短得多。反馈周期短意味着写代码的效率更高。
  • 通过增加小粒度的单元测试,我们就可以显著的降低测试人员发现bug的概率。我们就可以用自动化测试搞定一般的测试用例,以便让测试人员关注于临界测试和探索性测试。
  • 聪明的读者会发现我们没有给View写单元测试。我会在在后面的文章中测试View本身。基本的思路就是我们会通过增加小粒度的测试,来测试前端显示的逻辑,无论是View还是Behavior。而且,你的View应该很简单,以至于很难搞出bug。

一些愚见

毫无辩驳,第一个低调的View来自最原始的MVC架构,与许多软件开发领域中的好东西一样,它也来自Smalltalk社区。就像我之前提到的,你可以读读历史Model View Controller (MVC) pattern and the formulation of the Model View Presenter (MVP) patterns。它涵盖了这一系列的大部分东西。

达达尼昂用强大的低调对话框干掉了他的敌人,但是他知道,他现在需要信得过的朋友的帮助了,因为邪恶的德温特女人已经知道了他在追赶。幸运的是,达达尼昂的三个伙伴正在赶来。尼昂嘴角微微一笑,想象着朋友到来的场景:

  • 1.Supervising Controller —— 我为View处理棘手的案例
  • 2.Passive View —— 你让我做什么,我就做什么
  • 3.Presentaion Model —— 我做我想做的

达达尼昂已看到视线边缘的尘土飞扬,一个客人已经到来……

 

 
分类: 道听途说
posted on 2012-09-25 09:35  HackerVirus  阅读(278)  评论(0编辑  收藏  举报