自己动手写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代码职责分离的问题上,提供了很好的思路。我这么说,是因为...
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来自最原始的MVC架构,与许多软件开发领域中的好东西一样,它也来自Smalltalk社区。就像我之前提到的,你可以读读历史Model View Controller (MVC) pattern and the formulation of the Model View Presenter (MVP) patterns。它涵盖了这一系列的大部分东西。
达达尼昂用强大的低调对话框干掉了他的敌人,但是他知道,他现在需要信得过的朋友的帮助了,因为邪恶的德温特女人已经知道了他在追赶。幸运的是,达达尼昂的三个伙伴正在赶来。尼昂嘴角微微一笑,想象着朋友到来的场景:
达达尼昂已看到视线边缘的尘土飞扬,一个客人已经到来……