浅谈企业软件架构(3)
第三章 层次优势
前一章我们谈到了分层有很多好处,那么到底有些什么好处呢,本章我们在上一章的例子的基础上增加两个不同类型的客户端来简要说明,分层后我们在某些需求发生变化是如View的要求变了,我们只是通过增加View来解决问题,而不是在单层例子的时候,需要几乎全部的重写来完成这一工作。
3.1 WinForm的客户端例子
假定一个场景由于客户某些需求发生了变化,或者通过Web开发的界面无法实现客户要求的客户端展现,我们需要把切换到WinForm的界面来,现在就上一章的例子来实现这样一个切换过程。
在解决方案中增加一个Windows Forms Application类型的DemoWinForm的项目,接着我们把Form.cs修改成FormCustomer.cs,添加我们需要的控件如下图:
给项目添加Application Configuration File如下图:
App.config文件代码如下:
<configuration>
<configSections>
<section name="hibernate-configuration" type="NHibernate.Cfg.ConfigurationSectionHandler, NHibernate"/>
</configSections>
<hibernate-configuration xmlns="urn:nhibernate-configuration-2.2">
<session-factory>
<property name="dialect">NHibernate.Dialect.MsSql2000Dialect</property>
<property name="connection.provider">NHibernate.Connection.DriverConnectionProvider</property>
<property name="connection.connection_string">Data Source=.;Initial Catalog=Sunwise;Persist Security Info=True;User ID=sa;Password=sa</property>
</session-factory>
</hibernate-configuration>
</configuration>
现在我们来看看FormCustomer.cs的代码:
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using Model;
using Biz;
namespace DemoWinForm
{
public partial class FormCustomer : Form
{
private Customer _customer;
private CustomerBiz _customerBiz;
public FormCustomer()
{
InitializeComponent();
_customerBiz = new CustomerBiz();
}
页面事件交互代码:
{
_customer = new Customer();
_customer.Firstname = this.textBox2.Text.Trim();
_customer.Lastname = this.textBox3.Text.Trim();
_customer.Gender = this.textBox4.Text.Trim();
_customer.Address = this.textBox5.Text.Trim();
_customer.Remark = this.textBox6.Text.Trim();
_customer.CustomerId = Convert.ToInt32(this.textBox1.Text.Trim());
_customerBiz.Add(_customer);
}
private void button_Get_Click(object sender, EventArgs e)
{
_customer = _customerBiz.Get(Convert.ToInt32(textBox1.Text.Trim()));
if (_customer != null)
{
this.textBox2.Text = _customer.Firstname;
this.textBox3.Text = _customer.Lastname;
this.textBox4.Text = _customer.Gender;
this.textBox5.Text = _customer.Address;
this.textBox6.Text = _customer.Remark;
}
}
private void button_Edit_Click(object sender, EventArgs e)
{
_customer = _customerBiz.Get(Convert.ToInt32(textBox1.Text.Trim()));
_customer.Firstname = this.textBox2.Text.Trim();
_customer.Lastname = this.textBox3.Text.Trim();
_customer.Gender = this.textBox4.Text.Trim();
_customer.Address = this.textBox5.Text.Trim();
_customer.Remark = this.textBox6.Text.Trim();
_customerBiz.Edit(_customer);
}
private void button_Del_Click(object sender, EventArgs e)
{
_customer = _customerBiz.Get(Convert.ToInt32(textBox1.Text.Trim()));
_customerBiz.Delete(_customer);
}
界面交互的代码与Web的例子比较接近,主要是实现界面事件的响应和界面数据处理。完成这样一个切换基本上没有太大的开发量。当然大家会说如果界面操作比较复杂的时候工作量也较大!是的,但是至少我们减少了不少工作量,尤其如果我们在Biz层开发的时候采用了测试驱动的开发模式,编写了大量的测试单元的话,还可以大大的减少后期测试工作量。如果没有分层处理,这样的迁移在项目上绝对就是一个巨大的灾难!
3.2 Extjs的客户端例子
这一节我们实现一个采用Extjs作为前端展示技术的界面层,分层给我们带来的好处之一就是,采用什么样的界面技术对业务来说不会有太多的影响,只是不同的展现形式而已。ExtJS可以用来开发RIA也即富客户端的AJAX应用,是一个用javascript写 的,主要用于创建前端用户界面,是一个与后台技术无关的前端ajax框架。因此,可以把ExtJS用在.Net、Java、Php等各种开发语言开发的应 用中。ExtJs最开始基于YUI技术,由开发人员Jack Slocum开发,通过参考Java Swing等机制来组织可视化组件,无论从UI界面上CSS样式的应用,到数据解析上的异常处理,都可算是一款不可多得的JavaScript客户端技术的精品。采用该技术来做界面层展现工具好处就是很多展现组件很好很强大,但是弊病就是对项目组来说开发技术门槛较高,较传统的界面开发方式跟踪调试难度大。
3.2.1. 添加类型为Asp.Net Web Application的DemoExtjs项目
把extjs2.2目录拷贝到项目中,系统会自己把该目录复制项目目录下。如下图:
3.2.2. 添加Customer.js文件
Customer.js文件包含Extjs的界面的控件及控件响应事件Javascript脚本,界面效果如下图:
Customer.js脚本代码如下:
Ext.onReady(function(){
var simpleForm = new Ext.form.FormPanel({
labelAlign: 'left',
title: '表单例子',
buttonAlign:'right',
bodyStyle:'padding:5px',
width: 600,
frame:true,
labelWidth:80,
//定义 Colunmn
items: [{
layout:'column',
border:false,
labelSeparator:':',
//add 控件
items://column1
[{
columnWidth:.5,
layout: 'form',//属于FormLayout布局,如果没有,textfield无法显示
border:false,
items: [{
xtype:'textfield',
fieldLabel: 'ID',
name: 'id',
anchor:'90%'
},
{
xtype:'textfield',
fieldLabel: '姓',
name: 'firstName',
anchor:'90%'
},
{
xtype:'textfield',
fieldLabel: '名',
name: 'lastName',
anchor:'90%'
},
{
xtype:'textfield',
fieldLabel: '性别',
name: 'gender',
anchor:'90%'
},
{
xtype:'textfield',
fieldLabel: '地址',
name: 'address',
anchor:'90%'
},
{
xtype:'textfield',
fieldLabel: '备注',
name: 'remark',
anchor:'90%'
}]
}]
}],
buttons: [{text:"增加",handler:function(){
// 提交表单
simpleForm.getForm().submit(
{
url:"CustomerData.aspx",
method:"POST",
params:{action:"add"},
success:function(form, result)
{
Ext.Msg.alert('提示', '数据保存成功!');
}
});
}
},{
text:"获得",
handler:function()
{
var id = Ext.get("id").dom.value;
simpleForm.getForm().load(
{
waitMsg: '正在加载数据',
waitTitle: '提示',
url: 'CustomerData.aspx?id=' + id,
method: 'GET',
params:{action:"get"},
success: function(form, action)
{
Ext.Msg.alert('提示', '数据加载成功!')
}
});
}
},{
text:"修改",
handler:function()
{
simpleForm.getForm().submit(
{
url:"CustomerData.aspx",
method:"POST",
params:{action:"edit"},
success:function(form, result)
{
Ext.Msg.alert('提示', '数据保存成功!');
}
});
}
},{
text:"删除",
handler:function()
{
simpleForm.getForm().submit(
{
url:"CustomerData.aspx",
method:"POST",
params:{action:"delete"},
success:function(form, result)
{
Ext.Msg.alert('提示', '数据保存成功!');
simpleForm.getForm().reset();
}
});
}
}]
});
simpleForm.render(document.body);
})
3.2.3. 添加CustomerExtjs.aspx文件
更名Default.aspx为CustomerExtjs.aspx添加对Javascript脚本的引用代码,代码如下:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
<title>Untitled Page</title>
<link href="ext-2.2/resources/css/ext-all.css" rel="stylesheet" type="text/css" />
<script src="ext-2.2/adapter/ext/ext-base.js" type="text/javascript"></script>
<script src="ext-2.2/ext-all.js" type="text/javascript"></script>
<script src="Customer.js" type="text/javascript"></script>
</head>
<body>
<form id="form1" runat="server">
<div>
</div>
</form>
</body>
</html>
关于更多的Extjs的使用请参考Extjs相关文档。
3.2.4. 添加CustomerData.aspx文件
// 提交表单
simpleForm.getForm().submit(
{
url:"CustomerData.aspx",
method:"POST",
params:{action:"add"},
success:function(form, result)
{
Ext.Msg.alert('提示', '数据保存成功!');
}
});
}
}
注意上面的Javascript脚本代码,我们在Customer.js中定义了数据处理的页面为url:"CustomerData.aspx",可以把CustomerData.aspx看成是在Web端响应界面事件的后台服务页面。Extjs开发需要两个页面来完成数据的交互,一个是Javascript脚本控件渲染的页面,一个是响应界面事件的服务端处理页面。
CustomerData.aspx代码如下:
using System.Configuration;
using System.Web;
using System.Text;
using Model;
using Biz;
namespace DemoExtjs
{
public partial class CustomerData : System.Web.UI.Page
{
private CustomerBiz _customerBiz;
private Customer _customer;
protected void Page_Load(object sender, EventArgs e)
{
_customerBiz = new CustomerBiz();
Response.Clear();
Request.ContentType = "text/json";
Response.ContentType = "text/json";
string action = Request["action"];
if (string.IsNullOrEmpty(action))
{
return;
}
switch (Request["action"]) //相应不同的业务操作
{
case "add":
if (Add() )
{
Response.Write("{success:true,errors:{}}");
}
else
{
Response.Write("{success:false,errors:{info:'错误'}}");
}
break;
case "edit":
if (this.Edit())
{
Response.Write("{ success:true}");
}
break;
case "delete":
if (this.Delete())
{
Response.Write("{success:true,errors:{}}");
}
else
{
Response.Write("{success:false,errors:{info:'错误'}}");
}
break;
case "get":
Response.Clear();
Response.Write(this.Get());
break;
default:
break;
}
}
3.2.4.1. 增加Customer对象函数
{
_customer = new Customer();
_customer.CustomerId = Convert.ToInt32( Request.Form["id"]);
_customer.Firstname = Request.Form["firstName"];
_customer.Lastname = Request.Form["lastName"];
_customer.Gender = Request.Form["gender"]; ;
_customer.Address = Request.Form["address"];
_customer.Remark = Request.Form["remark"];
return _customerBiz.Add(_customer);
}
3.2.4.2. 获取Customer对象函数
{
Int32 id = Int32.Parse(Request["id"]);
_customer = _customerBiz.Get(id);
StringBuilder sb = new StringBuilder(1000);
// _customer to json
// 字符串拼接,
sb.Append(@"{
success: true,
data: ");
sb.Append("{");
sb.Append(string.Format("id:'{0}', firstName:'{1}',lastName:'{2}',gender:'{3}',address:'{4}',remark:'{5}'",
_customer.CustomerId, _customer.Firstname, _customer.Lastname, _customer.Gender, _customer.Address, _customer.Remark));
sb.Append("}");
sb.Append("}");
return sb.ToString();
}
3.2.4.3. 修改Customer对象函数
{
Int32 id = Int32.Parse(Request["id"]);
_customer = _customerBiz.Get(id);
_customer.Firstname = Request.Form["firstName"];
_customer.Lastname = Request.Form["LastName"];
_customer.Gender = Request.Form["gender"];
_customer.Address = Request.Form["address"];
_customer.Remark = Request.Form["remark"];
return _customerBiz.Edit(_customer);
}
3.2.4.4. 删除Customer对象函数
{
Int32 id = Int32.Parse(Request["id"]);
_customer = _customerBiz.Get(id);
return _customerBiz.Delete(_customer);
}
3.3 结语
通过两个不同的界面层的实现来描述界面层与业务层的关系,在企业开发过程中,实际的业务逻辑层随着时间的推移和企业应用的不断提高,往往会越来越复杂,那么这样的层次结构就会给升级维护和开发带来一个全新的局面。同时,如果采用测试驱动的开发模式的话,大部分的业务通过对业务逻辑层的单元测试编写就可以获得测试验证。这样的当某个时间点增加业务逻辑时,原来的单元测试代码就可以覆盖测试本次修改可能影响的所有代码,从而大大提高维护的效率,减少因为紧急维护带进更多BUG的可能性。
下一章我们会简要的谈谈单元测试在三层开发中的使用,TDD开发模式会给初学者开发过程带来新的思路。