三层架构,够不够---DDD眼中的三层(附C#源代码实现)

原创
2012/11/30 07:53
阅读数 4.9K

软件复杂度的根本,来源于思维的复杂度。

三层架构

从DDD看三层

DDD的三层实现详细架构

看代码

业务域 (Domain)
持久层 (数据层)
测试和使用的例子
完整代码下载

 

 

 得心应手武器库:

Fluent nHibernate
nUnit

Git (GitHub)

 本文所涉及使用的工具, 见前文: 我的.Net武器库 ------ 新.Net架构必备工具列表

三层架构

相对于目前日新月异的新概念,新名词,三层架构已经算得上元老了。虽仍有争议,但业界更多的是共识。

图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()删除考试。

完全代码下载

展开阅读全文
打赏
0
32 收藏
分享
加载中
予沁安博主

引用来自“jeffsui”的评论

ddt倒是有很多

DDT ?
2012/12/17 08:59
回复
举报
ddt倒是有很多
2012/12/17 07:36
回复
举报
予沁安博主

引用来自“KK_OP”的评论

引用来自“予沁安”的评论

引用来自“KK_OP”的评论

引用来自“予沁安”的评论

引用来自“KK_OP”的评论

dddlib是根据kan back那本《领域驱动设计》书的理念来实现的。
但,本人见博主所说的ddd和kan back说的有出入。

呵呵,我的理解,DDD书的所表述的东西和所谓通用框架天然就是矛盾。

没有哦。 我们使用dddlib来实现DDD范式的开发非常爽哦。

呵呵, 这是一个很大的话题, 慢慢聊. 不过,很高兴看到,这一块大家不仅仅是在谈理论了.

的确是一个很大的话题。呵呵。
对了,论坛里刚好有人写了一个小例子:http://www.oschina.net/code/snippet_107336_7479

顺便也看看我的代码分享 http://www.oschina.net/code/snippet_871522_15682
2012/12/05 09:59
回复
举报
予沁安博主

引用来自“KK_OP”的评论

引用来自“予沁安”的评论

引用来自“KK_OP”的评论

引用来自“予沁安”的评论

引用来自“KK_OP”的评论

dddlib是根据kan back那本《领域驱动设计》书的理念来实现的。
但,本人见博主所说的ddd和kan back说的有出入。

呵呵,我的理解,DDD书的所表述的东西和所谓通用框架天然就是矛盾。

没有哦。 我们使用dddlib来实现DDD范式的开发非常爽哦。

呵呵, 这是一个很大的话题, 慢慢聊. 不过,很高兴看到,这一块大家不仅仅是在谈理论了.

的确是一个很大的话题。呵呵。
对了,论坛里刚好有人写了一个小例子:http://www.oschina.net/code/snippet_107336_7479

不错,谢谢。
2012/12/05 09:54
回复
举报

引用来自“予沁安”的评论

引用来自“KK_OP”的评论

引用来自“予沁安”的评论

引用来自“KK_OP”的评论

dddlib是根据kan back那本《领域驱动设计》书的理念来实现的。
但,本人见博主所说的ddd和kan back说的有出入。

呵呵,我的理解,DDD书的所表述的东西和所谓通用框架天然就是矛盾。

没有哦。 我们使用dddlib来实现DDD范式的开发非常爽哦。

呵呵, 这是一个很大的话题, 慢慢聊. 不过,很高兴看到,这一块大家不仅仅是在谈理论了.

的确是一个很大的话题。呵呵。
对了,论坛里刚好有人写了一个小例子:http://www.oschina.net/code/snippet_107336_7479
2012/12/05 09:08
回复
举报
予沁安博主

引用来自“KK_OP”的评论

引用来自“予沁安”的评论

引用来自“KK_OP”的评论

dddlib是根据kan back那本《领域驱动设计》书的理念来实现的。
但,本人见博主所说的ddd和kan back说的有出入。

呵呵,我的理解,DDD书的所表述的东西和所谓通用框架天然就是矛盾。

没有哦。 我们使用dddlib来实现DDD范式的开发非常爽哦。

呵呵, 这是一个很大的话题, 慢慢聊. 不过,很高兴看到,这一块大家不仅仅是在谈理论了.
2012/12/05 00:14
回复
举报

引用来自“予沁安”的评论

引用来自“KK_OP”的评论

dddlib是根据kan back那本《领域驱动设计》书的理念来实现的。
但,本人见博主所说的ddd和kan back说的有出入。

呵呵,我的理解,DDD书的所表述的东西和所谓通用框架天然就是矛盾。

没有哦。 我们使用dddlib来实现DDD范式的开发非常爽哦。
2012/12/04 13:05
回复
举报
予沁安博主

引用来自“KK_OP”的评论

dddlib是根据kan back那本《领域驱动设计》书的理念来实现的。
但,本人见博主所说的ddd和kan back说的有出入。

呵呵,我的理解,DDD书的所表述的东西和所谓通用框架天然就是矛盾。
2012/12/04 11:42
回复
举报
予沁安博主

引用来自“KK_OP”的评论

引用来自“予沁安”的评论

引用来自“KK_OP”的评论

http://www.oschina.net/p/dddlib

说明文档几乎没有啊!其实所谓实现DDD的框架也已经有不少了。比如:http://sharparchitecture.net/ 和 http://lokad.github.com/lokad-cqrs/
问题是,涉及到架构,不是功能库,使用了这些框架,你的系统就真的是三层了,就真的是DDD了? 我怀疑。

因为作者没时间写文档。
dddlib只是实现了领域层(大概就是你说的数据层)所依赖的基础设施。增删改查都通过dddlib来实现,你不需要知道是什么数据库,你只需要关心业务。

是的,这也是我现在做的事件。不过基于。Net
2012/12/04 11:26
回复
举报
dddlib是根据kan back那本《领域驱动设计》书的理念来实现的。
但,本人见博主所说的ddd和kan back说的有出入。
2012/12/04 10:22
回复
举报
更多评论
打赏
18 评论
32 收藏
0
分享
返回顶部
顶部