分离域与表示 - 第一部分
如何分离域与表示。
来自 IRefactor 的转载
在软件开发人员手中,Visual Studio 可以成为一把瑞士军刀。
您想要几个小时内就能有一个功能齐全的应用程序?没问题,先生!
- 创建一个 Windows Forms 应用程序。
- 添加相关的数据源(如下图 1 所示:
Courses
表)。 - 将生成好的实体从“数据源”拖放到相应的窗体上。
- 添加最少的代码来按所需顺序显示窗体。
- 编译并执行。
Voilà!我向您展示一个“企业级应用程序”!
虽然它看起来很业余,但它允许实现功能齐全的 CRUD 操作。
但是,权力可能导致腐败……
最近,我遇到了一些使用上述方法实现的rible项目。不用说,这些项目都是“意大利面条式代码”——所有的 UI 元素、视觉状态、业务逻辑和数据访问都集中在几个窗体中。
(别以为这是罕见情况!有很多公司试图用这种“快速而肮脏”的方法来“节省”时间。)
我是一个务实的软件工程师,我相信“快速而肮脏”的方法在某些情况下至关重要。
如果您要演示一个展示您能力的演示应用程序;请随意使用必要的工具,尽可能快地实现它。
然而,如果您使用上述方法开发一个实际的应用程序,那将是一条通往地狱的捷径。慢慢地,但肯定地,“快速开发”会花费越来越多的时间。
- 客户的按钮点击事件与员工的事件点击非常相似,但会关联和更新不同的 UI 元素——您会在两个地方重复相同的逻辑。
- 新需求为某个向导流程添加了新条件——您会添加一个“
if
”语句来处理这些条件。有时,由于已经存在许多“if
”语句,您会添加一个“switch
”语句。 - 新需求要求保存更多数据——您会为方法添加更多参数。您甚至会进一步将这些参数添加到每个需要更改的地方(窗体/方法)。
- 类和方法变得越来越庞大,充斥着 UI 元素、视觉状态转换和业务逻辑。下次尝试阅读代码时,您需要至少几个小时来集中精力梳理一团乱麻。
负责上述项目的项目经理告诉我:
“我想要的任何变更请求,都带有不切实际的时间估算。当我问开发人员为什么需要这么长时间时,他回答:我的天啊——您知道修改那段代码需要多大的工作量吗?您明白我需要多长时间来重新测试应用程序,仅仅是为了验证我没有搞砸任何东西吗?”
看起来很明显,如果项目一开始就按照 UI、业务逻辑和数据访问层清晰分离的方式编写,那么维护产品生命周期会更容易。但显然情况并非如此,那么我们能做什么呢?
我们可以应用的一个工具是:重构。
重构是在不改变软件外部行为的情况下,改进其内部结构的进程。通过执行自动单元测试,重构确保了外部行为的完整性。我们以渐进的小步骤(重构步骤)改进代码,每次更改后都进行编译和单元测试。
在接下来的几篇文章中,我将演示一种称为“分离领域与表现”的重构,该重构显然可以打破 UI 和 BL 之间的紧密耦合。在完成重构过程本身的演示后(这需要我几篇文章),我还会解释如何创建单元测试套件,以验证外部行为的完整性。
备注
- 我将重构图 1(上图)中的应用程序。
该应用程序包含一个视图(
FrmMain
)和一个领域对象(CoursesDS
)。 - 虽然示例应用程序包含一个
Form
和一个DataSet
,但相同的技术也可应用于更复杂的情况。只需识别彼此适合的对象组,并为每个组应用以下分离步骤。 - 建议下载示例IRefactor.CoursesView项目,并在查看代码的同时,通过重构步骤进行操作。
- 在接下来的文章中,我将介绍如何通过向 MVP(或 MVC)模式进行重构来增强这种分离。
- 写关于重构过程比实际实现它更难。不要被吓倒,解释每一个小步骤(并提供截图)比实际操作需要更长的时间。分离领域与表现(直到 MVP)大约需要我 10 分钟的工作。
重构步骤
- 当最初定义
Irefactor.CoursesView
项目时,Visual Studio 为 Courses 数据源生成了一个名为CoursesDS
的DataSet
。正如您所见,Irefactor.CoursesView
项目混合了 UI(FrmMain
)元素和领域(CoursesDS
)元素。图 2 - 让我们开始将
CoursesDS
领域对象从 UI(View
)中移除。创建一个项目
IRefactor.Common
,并将CoursesDS DataSet
复制到其中。(将
CoursesDS
从一个项目拖到另一个项目,Visual Studio 会处理剩余的。)由于
CoursesDS
是 VS 自动生成的类,因此还应复制存储在IRefactor.CoursesView
项目设置中的连接字符串定义。将Settings.settings
添加到 IRefactor.Common Properties 文件夹,并复制连接字符串。图 3 - 编译解决方案并执行单元测试。
- 更改
FrmMain
(View
)以使用新创建的IRefactor.Common.CoursesDS DataSet
。打开
FrmMain
的设计器。移除coursesDS
(领域对象的实例:IRefactor.CoursesView.CoursesDS DataSet
– 见红色箭头 1)。图 4 - 在“工具箱”窗口中,在最近添加的
IRefactor.Common
组件下,您将看到IRefactor.Common.CoursesDS DataSet
。将此
CoursesDS
拖放到FrmMain
窗体上。图 5 - 将
IRefactor.Common.CoursesDS DataSet
拖放到FrmMain
界面上会生成该类的实例。将其重命名为coursesDS
(以匹配之前的名称)。 - 对
IRefactor.CoursesView.CoursesTableAdapter
重复最后三个步骤(参见图 4,红色箭头 2)。 - 选择
BindingSource
(参见图 4,bsCourses
),并将其DataSource
更改为coursesDS
实例(coursesDS
现在来自IRefactor.Common
项目)。还要将其DataMember
更改为指向Courses
表。图 6 - 在
FrmMain
的代码隐藏文件中,添加对IRefactor.Common namespace
的引用,并将CoursesDS
更改为完全限定名称。// add reference using IRefactor.Common; // ... private void coursesBindingNavigatorSaveItem_Click { (object sender, EventArgs e) // ... // change the coursesDS to IRefactor.Common.CoursesDS IRefactor.Common.CoursesDS.CoursesDataTable changes = this.coursesDS.Courses.GetChanges() as IRefactor.Common.CoursesDS.CoursesDataTable; // ... }
- 编译解决方案并执行单元测试。
- 遍历
IRefactor.CoursesView
,移除所有先前领域对象IRefactor.CoursesView.CoursesDS DataSet
的实例。此外,从项目中删除 CoursesDS.xsd 文件。 - 编译解决方案并执行单元测试。
我们已成功完成第一步。我们已将名为 IRefactor.CoursesView
的 UI 项目与名为 IRefactor.Common
的领域项目分离开来。现在是时候认真对待,继续向 MVP 模式重构了。