三层架构
相对于目前日新月异的新概念,新名词,三层架构已经算得上元老了。虽仍有争议,但业界更多的是共识。
图1 常用三层的描述图
足够简单、清晰,我仍要提醒的是,注意层之间连线的箭头,非常之重要,借用UML的定义,箭头表示依赖关系。也就是说,必须先有数据层,才有业务层,然后才有表现层。这又怎么样,小问题。不,这是一个大麻烦!
从DDD看三层
我们暂时靶这个话题放一放,挑个比较新一点的东西。业务域驱动开发(DDD) 近年也是风生水起,红红火火,但它是什么,是怎么回事,似乎就不如三层架构那么妇孺皆知了。
图2 从DDD的角度看三层架构
以业务域为系统的核心,所有其它与业务无关的内容对这个核心来谈,都是外部服务/功能。这里,出于本文说明的需要,独立出了两个较为特别的外部功能,持久层和用户接口。
两个看上去完全不同的架构设计,哪个更对哪个更好?每一个都有大量的拥护者,大量的讨论,互相三间似乎又泾渭分明,至少我们经常看到的文章给我们如此的印象。自然,我们的思考,为什么不能融合在一起呢?其实,它们并不像看起来区别那么大。从名词上,虽然我有意把名称错开,我们也仍能看到之间的对应关系、业务层=业务域,数据层=持久层,表现层=用户接口。当然,这些细节用词的不同仍有必要的,毕竟,它们不完全是一回事。
DDD的三层实现详细架构
好了,抽象的讨论已经足够了,我们也足够糊涂了。细节为王,我们如何实现?来看看这个实际系统的简化架构图。.
图3 实际架构设计
可以看到,在保留了清晰的三层外,重要的是把依赖关系改变了。而所谓依赖注入(DI),只是一种实际的技术实现,完成和实现这种架构设计需求。也可以清晰的看到,图中是以Domain为核心的。 当然,这是一个简化又简化的示意图,不想一开始就把事情弄的复杂.
看代码
最后,来看看具体的代码,才有更好的体验。
业务域 (Domain)
考试类:
namespace
Skight.Demo.Domain.Examination
{
public
class
Exam
{
public
virtual
int
Id
{
get
;
set
;
}
public
virtual
string
Code
{
get
;
set
;
}
public
virtual
string
Name
{
get
;
set
;
}
}
}
很简单的一个考试类,可以看到,域中的类定义几乎不受持久层(数据库)影响,除了两点:
1.属性ID是从数据表的主键而来;
2. 如果要用nHibernate的Lazy Load每个属性都必须是Virtual。
即使如此,这个类已经足够干净了。我也看到,一些系统实现,专门定义了一个基础类Entity,然后,把ID的定义放在这个类中. 我觉得很没必要, 画蛇添足。
作为示例,这个域类很简单, 但却是核心的核心。项目越往后,这一层膨胀的越厉害。后面几部分,现在看起来比较多,复杂。之后,不会有大的变化,反而显得会越来越简单。
仓储接口:
using
System
;
using
System.Collections.Generic
;
using
System.Linq.Expressions
;
namespace
Skight.Demo.Domain
{
public
interface
Repository
{
Item
get_by_id
<
Item
>(
int
id
);
void
save
<
Item
>(
Item
item
);
Item
get_single_item_matching
<
Item
>(
Query
<
Item
>
query
);
void
delete
<
Item
>(
Item
item
);
IEnumerable
<
Item
>
get_all_items_matching
<
Item
>(
Query
<
Item
>
query
);
IEnumerable
<
Item
>
get_all_items
<
Item
>();
}
}
注意到:
1. 接口命名,我没有加I,这是特意的。
2. 用到了Query<>接口, 这个是对查询的一个抽象。好处是,不需要像大多数的仓储实现,要为每个类建立一个仓储接口,膨胀的很厉害。
Quer接口很简单,没有任何方法和属性,只是为了使用强类型。它的实现类会根据需要, 越来越多。 因为,查询几乎就是数据层的主要功能。
查询接口的定义:
namespace Skight.Demo.Domain
{
public interface Query<Item>
{
}
}
持久层 (数据层)
考试映射类:
using
FluentNHibernate.Mapping
;
using
Skight.Demo.Domain.Examination
;
namespace
Skight.Demo.NHRepository
{
public
class
ExamMap
:
ClassMap
<
Exam
>
{
public
ExamMap
()
{
Id
(
x
=>
x
.
Id
);
Map
(
x
=>
x
.
Code
);
Map
(
x
=>
x
.
Name
);
}
}
}
Fluent nHibernate对仓储接口的实现:
using
System
;
using
System.Collections.Generic
;
using
System.Linq
;
using
NHibernate
;
using
NHibernate.Criterion
;
using
Skight.Demo.Domain
;
using
Skight.Demo.NHRepository.QueryImpls
;
namespace
Skight.Demo.NHRepository
{
public
class
RepositoryImpl
:
Repository
{
ISession
session
{
get
{
return
SessionProvider
.
Instance
.
CurrentSession
;
}
}
#region CRUD
public
Item
get_by_id
<
Item
>(
int
id
)
{
return
session
.
Get
<
Item
>(
id
);
}
public
void
save
<
Item
>(
Item
item
)
{
session
.
SaveOrUpdate
(
item
);
}
public
void
delete
<
Item
>(
Item
item
)
{
session
.
Delete
(
item
);
}
#endregion
#region Advanced Query
public
IEnumerable
<
Item
>
get_all_items_matching
<
Item
>(
Query
<
Item
>
query
)
{
if
(
query
==
null
)
throw
new
ArgumentNullException
();
if
(
query
is
QueryImplByQueryOver
<
Item
>)
{
QueryOver
<
Item
>
my_query
=
(
query
as
QueryImplByQueryOver
<
Item
>).
Query
;
return
my_query
.
GetExecutableQueryOver
(
session
).
List
();
}
throw
new
ArgumentException
(
string
.
Format
(
"Query {0} is not type supported."
,
query
.
GetType
()));
}
public
Item
get_single_item_matching
<
Item
>(
Query
<
Item
>
query
)
{
IEnumerable
<
Item
>
result
=
get_all_items_matching
(
query
);
if
(
result
.
Count
()
>
1
)
throw
new
TooManyRowsMatchingException
();
if
(
result
.
Count
()
<=
0
)
throw
new
NoRowsMatchingQueryException
();
return
result
.
Single
();
}
#endregion
//Shouldn't use every often
public
IEnumerable
<
Item
>
get_all_items
<
Item
>()
{
return
session
.
CreateCriteria
(
typeof
(
Item
)).
List
<
Item
>();
}
}
}
Fluent nHibernate的配置:
using
System
;
using
System.IO
;
using
System.Reflection
;
using
FluentNHibernate.Cfg
;
using
FluentNHibernate.Cfg.Db
;
using
NHibernate
;
using
NHibernate.Cfg
;
using
NHibernate.Tool.hbm2ddl
;
namespace
Skight.Demo.NHRepository
{
public
class
SessionProvider
{
#region Instance for use outside
private
static
SessionProvider
instance
;
public
static
SessionProvider
Instance
{
get
{
if
(
instance
==
null
)
{
instance
=
new
SessionProvider
();
}
return
instance
;
}
}
#endregion
#region Set up database
private
const
string
DBFile
=
"SkightDemo.db"
;
public
bool
IsBuildScheme
{
get
;
set
;
}
public
void
initilize
()
{
session_factory
=
Fluently
.
Configure
()
.
Database
(
SQLiteConfiguration
.
Standard
.
UsingFile
(
DBFile
).
ShowSql
())
.
Mappings
(
m
=>
m
.
FluentMappings
.
AddFromAssembly
(
Assembly
.
GetExecutingAssembly
()))
.
ExposeConfiguration
(
c
=>
c
.
SetProperty
(
"current_session_context_class"
,
"thread_static"
))
.
ExposeConfiguration
(
build_schema
)
.
BuildSessionFactory
();
}
private
void
build_schema
(
Configuration
configuration
)
{
if
(
IsBuildScheme
)
{
new
SchemaExport
(
configuration
)
.
Execute
(
true
,
true
,
false
);
}
}
#endregion
private
readonly
object
lock_flag
=
new
object
();
private
ISessionFactory
session_factory
;
public
ISessionFactory
SessionFactory
{
get
{
if
(
session_factory
==
null
)
{
lock
(
lock_flag
)
{
if
(
session_factory
==
null
)
{
initilize
();
}
}
}
return
session_factory
;
}
}
public
ISession
CreateSession
()
{
ISession
session
=
SessionFactory
.
OpenSession
();
return
session
;
}
public
ISession
CurrentSession
{
get
{
return
SessionFactory
.
GetCurrentSession
();
}
}
}
}
使用的SQLite文本数据库,作为示例。
测试和使用的例子
自动创建数据库:
using
NUnit.Framework
;
namespace
Skight.Demo.NHRepository.Tests
{
[TestFixture]
public
class
CreateDatabase
{
[Test]
public
void
Run
()
{
var
provider
=
SessionProvider
.
Instance
;
provider
.
IsBuildScheme
=
true
;
provider
.
initilize
();
}
}
}
这里,只是用测试的形式,实现功能。如果运行这个测试,将自动生成数据库。并且,可以输显示数据库生成脚本。在产品环境下,我就是用这个脚本来做数据库安装的。
操作数据(模拟UI):
using
NHibernate
;
using
NHibernate.Context
;
using
NUnit.Framework
;
using
Skight.Demo.Domain
;
using
Skight.Demo.Domain.Examination
;
namespace
Skight.Demo.NHRepository.Tests
{
[TestFixture]
public
class
DataOperation
{
private
Repository
repository
;
private
ISession
session
;
private
ITransaction
transaction
;
[SetUp]
public
void
SetUp
()
{
//Dependecy Inject
repository
=
new
RepositoryImpl
();
session
=
SessionProvider
.
Instance
.
CreateSession
();
transaction
=
session
.
BeginTransaction
();
CurrentSessionContext
.
Bind
(
session
);
}
[TearDown]
public
void
TearDown
()
{
transaction
.
Commit
();
transaction
.
Dispose
();
transaction
=
null
;
session
.
Close
();
session
.
Dispose
();
}
[Test]
public
void
create_a_exam
()
{
var
exam
=
new
Exam
();
exam
.
Code
=
"001"
;
exam
.
Name
=
"计算机考试"
;
repository
.
save
(
exam
);
}
[Test]
public
void
get_the_exam_by_id
()
{
var
exam
=
repository
.
get_by_id
<
Exam
>(
1
);
Assert
.
IsNotNull
(
exam
);
}
[Test]
public
void
delete_the_exam
()
{
var
exam
=
repository
.
get_by_id
<
Exam
>(
1
);
repository
.
delete
(
exam
);
}
}
}
同样,用测试的形式,模拟UI的数据的操作。
首先,运行Create_a_exam()插入一个考试对象。
然后,运行get_the_exam_by_id()获取刚插入的考试。
运行 delete_the_exam()删除考试。