分离域与表示 - 第二部分
关于“分离域与表示”系列文章的第二篇。
来自 IRefactor 的转载
这是本系列文章“分离领域与表示”重构的第二篇文章。
之前的文章
上次,我们讨论了在IRefactor.CoursesView
项目中将表示层(FrmMain
)与领域对象(CoursesDS
)断开连接的方法。结果是,我们不再拥有臃肿的、包含一切的初始项目,而是得到了以下结构:
IRefactor.CoursesView
– 代表不包含领域对象的View
(CoursesView
)。IRefactor.Common
– 代表不包含UI元素的领域对象(CoursesDS
)。
现在是时候进一步分离UI和BL了。为此,我将使用MVP模式。似乎关于UI/BL分离模式的定义存在很多误解(可以看这里),我将专注于以下定义:
在我的文章中,MVP = Model-View-Presenter 将基本代表:
- Model – 嘿,我是领域模型;
我知道如何操作模型对象以执行所需的应用程序逻辑和持久化。
我不知道如何向用户可视化任何信息,也不知道如何响应用户可能采取的任何操作。
- View – 嘿,我是视图模型;
我知道如何向用户可视化信息(这些信息将由Presenter提供给我)。
我知道如何执行简单的数据绑定以及可能修改屏幕视觉布局的简单UI操作。
我不知道在需要应用程序逻辑或持久化时该做什么。
- Presenter – 嘿,我是Presenter模型;
我知道如何处理用户对View的请求(这些请求比简单的数据绑定更复杂),以及如何将这些请求委托给Model。
我知道如何查询Model,以便在需要显示任何信息给用户时,将信息委托给View。
我不知道如何以图形方式绘制控件(这是View的职责),也不知道如何执行任何应用程序逻辑来推导出该信息(这是Model的职责)。
那些眼睛敏锐的人可能会注意到这里使用了Supervising Controller(这不会立即在重构的精神下对代码进行剧烈更改。稍后,可以将View转换为Passive View,同时继续重构)。
重构步骤
- 将
IRefactor.CoursesView.FrmMain
类重命名为CoursesView
。进入FrmMain
,右键单击它,然后使用Refactor » Rename命令轻松重命名它。 - 在
IRefactor.CoursesView
中创建一个名为CoursesPresenter
的类。 - 在
CoursesPresenter
类中添加一个指向CoursesView
的指针(请注意,视图是只读的)。public class CoursesPresenter { private readonly CoursesView view; }
- 在
CoursesPresenter
中添加一个接收CoursesView
实例的构造函数。public CoursesPresenter(CoursesView view) { this.view = view; }
- 编译解决方案并执行单元测试。
- 现在我们需要将用户交互从视图委托给Presenter。我们可以通过继承
EventArgs
并创建CoursesEventArgs
类来实现,或者我们可以让CoursesPresenter
直接查询CoursesView
并获取所需数据。这里,我将直接获取CoursesDS
领域对象。向CoursesView
添加以下内容:public CoursesDS Courses { get { return coursesDS; } }
- 让我们从
Save
事件的委托开始。如果您仔细查看coursesBindingNavigatorSaveItem_Click
事件处理程序,您会发现该方法具有两个不同的职责。它处理所需的数据绑定,然后执行数据访问操作以保存CoursesDS
领域对象。为了分离关注点,让我们使用另一个重构步骤,称为“提取方法”。选择数据访问代码,右键单击它,然后使用Refactor » Extract Method命令将代码提取到一个名为“Save
”的新方法中。// ... this.Validate(); this.bsCourses.EndEdit(); // Changed from auto generated code. Save(); // ...
private void Save() { if (coursesDS.HasChanges()) { CoursesDS.CoursesDataTable changes = this.coursesDS.Courses.GetChanges() as CoursesDS.CoursesDataTable; if (changes != null) { taCourses.Update(changes); } } }
- 编译解决方案并执行单元测试。
- 在分解了
coursesBindingNavigatorSaveItem_Click
方法后,我们突然意识到Save
方法不属于CoursesView
类,因为它执行数据访问操作。无论如何,此操作应该在领域模型(业务逻辑)内部。在此期间,我们将该方法推送到Presenter中。 - 在
CoursesPresenter
中,创建一个名为Save
的新方法。该方法将从CoursesView
检索CoursesDS
领域对象,并将该对象保存到数据库中。public void Save() { CoursesDS coursesDS = view.Courses; //... }
- 编译解决方案并执行单元测试。
- 将
CoursesView.Save
方法中的所有代码复制到CoursesPresenter.Save
方法中,并调整新代码以适应其新的“位置”(请注意CoursesTableAdapter
需要重新定义)。public void Save() { CoursesDS coursesDS = view.Courses; if (coursesDS.HasChanges()) { CoursesDS.CoursesDataTable changes = coursesDS.Courses.GetChanges() as CoursesDS.CoursesDataTable; if (changes != null) { using (CoursesTableAdapter taCourses = new CoursesTableAdapter()) { taCourses.Update(changes); } } } }
- 编译解决方案。
- 现在,有趣的部分来了;将
CoursesView.Save
方法中的所有代码放在注释中,并声明一个调用其Save
方法的CoursesPresenter
对象。private void Save() { //if (coursesDS.HasChanges()) //{ // CoursesDS.CoursesDataTable changes = // this.coursesDS.Courses.GetChanges() // as CoursesDS.CoursesDataTable; // if (changes != null) // { // taCourses.Update(changes); // } //} CoursesPresenter presenter = new CoursesPresenter(this); presenter.Save(); }
- 编译解决方案并执行单元测试。
- 瞧!您已成功将数据访问方法从视图移至Presenter。通过持续的重构,您可以将该方法进一步推送到数据访问层。
快速总结
- 我们引入了一个新的Presenter类,名为
CoursesPresenter
。 - 我们将
Save
方法(执行数据访问操作)从视图移到了Presenter类中。(别担心,我们将在下一篇文章中从CoursesView
中删除Save
方法。) - 同样的操作也应该应用于
Load
方法(FrmMain_Load
)。这里不展示,只需遵循相同的原理。
这是当前IRefactor.CoursesView
项目的示意图。
显然,这与我们之前描绘的MVP模式不同。未来的文章将解释重构为MVP模式的额外步骤,通过应用
- 事件 – Presenter通过订阅View来处理复杂的用户事件
- 接口 – Presenter只能通过接口操作View。否则将破坏View的封装。
参考文献
请参阅此处,了解关于智能客户端开发的精彩总结。