• Microsoft NLayerApp案例理论与实践 领域模型层


    本文将重点介绍Microsoft NLayerApp的领域模型层,这涉及到Domain.CoreDomain.Core.EntitiesDomain.MainModule以及Domain.MainModule.Entities四个项目。Domain.Core项目包含了基本接口的定义以及规约模式(Specification Pattern)的实现;Domain.Core.Entities则包含了支持Entity Framework的STE(Self-Tracking Entity)的实现代码,在上文《Microsoft NLayerApp案例理论与实践 - 基础结构层(数据访问部分)》我对STE做了一些介绍,但它的实现与Entity Framework(EF)结合的比较紧密,EF超出了本系列文章的讨论范围,因此,我们也不会针对STE的具体实现方式做太多讨论;Domain.MainModule根据项目需求,针对不同的实体定义了仓储接口,同时实现了项目所需的规约类型。领域服务也是该项目的重要部分;Domain.MainModule.Entities项目中包含了NLayerApp领域模型的核心代码。本文将从仓储接口、规约、领域服务、领域模型这四个方面对NLayerApp的Domain Model层做一个简单的介绍。

    仓储接口

    根据我们在《Microsoft NLayerApp案例理论与实践–DDD、分布式DDD及其分层》一文中的讨论,仓储的具体实现是放在基础结构层的,而仓储的接口则是放在领域模型层的。Domain.Core项目的IRepository接口就是仓储接口,所有的仓储类都需要实现该接口中定义的属性与方法。在Domain.Core项目下还有一个继承IRepository接口的IExtendedRepository接口,它包含了一些额外的方法来扩展IRepository的功能。事实上在整个NLayerApp中并没有真正用到IExtendedRepository接口,因此我们也不在此做过多讨论。下图是NLayerApp中与仓储的接口和实现相关的类关系图,为了方便浏览和描述,该图中仅包含了Customer仓储的定义与实现部分:

    image

    首先,ICustomerRepository接口继承于IRepository接口,以扩展IRepository来定义特定于Customer实体的仓储。因此,所有实现ICustomerRepository接口的类,不仅具备仓储的基本功能,而且还具有特定于Customer实体的仓储操作。其次,Repository类实现了IRepository接口,并作为所有仓储实现的基类,实现了IRepository接口中定义的方法,它在仓储部分的角色就是一个层超类型(Layer Supertype)。最后,CustomerRepository类继承于Repository类,同时实现了ICustomerRepository接口,由于Repository类中已经实现了IRepository中定义的所有方法,因此CustomerRepository类就无需去实现这些方法,只需要把关注点放在ICustomerRepository的实现上即可。以下是位于基础结构层的CustomerRepository代码,供读者朋友参考:

    public class CustomerRepository
        :Repository<Customer>,ICustomerRepository
    {
        #region Constructor
        /// <summary>
        /// Default constructor
        /// </summary>
        /// <param name="traceManager">Trace manager dependency</param>
        /// <param name="unitOfWork">Specific unitOfWork for this repository</param>
        public CustomerRepository(IMainModuleUnitOfWork unitOfWork, ITraceManager traceManager) 
            : base(unitOfWork, traceManager) { }
        #endregion
    
        #region ICustomerRepository implementation
        /// <summary>
        /// <see cref="Microsoft.Samples.NLayerApp.Domain.MainModule.Customers.ICustomerRepository"/>
        /// </summary>
        /// <param name="specification">
        /// <see cref="Microsoft.Samples.NLayerApp.Domain.MainModule.Customers.ICustomerRepository"/>
        /// </param>
        /// <returns>Customer that match <paramref name="specification"/></returns>
        public Customer FindCustomer(ISpecification<Customer> specification)
        {
            //validate specification
            if (specification == (ISpecification<Customer>)null)
                throw new ArgumentNullException("specification");
    
            IMainModuleUnitOfWork activeContext = this.UnitOfWork as IMainModuleUnitOfWork;
            if (activeContext != null)
            {
                //perform operation in this repository
                return activeContext.Customers
                                    .Include(c => c.CustomerPicture)
                                    .Where(specification.SatisfiedBy())
                                    .SingleOrDefault();
            }
            else
                throw new InvalidOperationException(string.Format(
                    CultureInfo.InvariantCulture,
                    Messages.exception_InvalidStoreContext,
                    this.GetType().Name));
        }
        #endregion
    }

    正如上图所述,ICustomerRepository接口扩展了IRepository接口以提供与Customer有关的仓储操作。对于应用程序开发框架来说,这样的设计有助于提高系统的扩展性。比如之前有网友针对Apworks框架提问,觉得Apworks的仓储接口只提供了一些很基本的操作,但他希望能够在仓储上增加一些诸如分页查询对象的操作,之前他的设计是,另外定义一个接口(IFooRepository),其中添加一些分页查询操作,然后让仓储实例同时实现IRepository和IFooRepository。如下:

    image

    这样做看上去FooRepository是一个完整的仓储实现,但IFooRepository与IRepository之间没有任何联系,IFooRepository本身并没有体现“仓储”的语义,但它原本就是一种仓储。从实践上看,我们需要在IoC容器中分别为IRepository和IFooRepository注册相同的类型:FooRepository,以便在程序中能够正确地解析IRepository和IFooRepository的具体实现,从而通过IRepository或者IFooRepository分别获得不同的仓储操作。当然,对于我们目前的情形,FooRepository同时实现IRepository和IFooRepository接口,那么C#是可以通过as关键字将该实例在IRepository和IFooRepository的实例间进行转换的,比如:

    IContainer container = IoCFactory.Instance.CurrentContainer;
    using (IRepositoryContext ctx = container.Resolve<IRepositoryContext>())
    {
        IRepository<Foo> repository = ctx.GetRepository<Foo>();
        // do sth. with repository ...
        IFooRepository<Foo> fooRepository = repository as IFooRepository<Foo>();
        if (fooRepository != null) // this is required...
        {
            // do sth. with fooRepository
        }
    }

    但是在应用程序开发的过程中,我们无法去约束开发人员一定要让FooRepository去实现IFooRepository接口,这就造成了上面的类型转换不成功,因此,判断fooRepository实例是否为空就显得非常重要。

    这样的设计还有另外一个缺陷,就是由于IFooRepository没有体现“仓储”的语义,这就导致它无法应用到基于仓储的类型约束上。例如,假设根据需求我们需要用到一个接口IMyInterface,它的定义如下:

    interface IMyInterface<T, S>
        where T : IRepository<S>
        where S : class
    {    }
    

    那么很明显我们就无法去定义一个类,在这个类中通过泛型参数T来使用IFooRepository接口:

    // error:
    class MyClass : IMyInterface<IFooRepository<MyEntity>, MyEntity>
    {    }
    

    相比之下,NLayerApp用了一个从语义上来讲更为合理的设计(如下图),它充分体现了“IFooRepository是一种仓储”的概念,总之,两种不同的设计的主要区别在各自所表达的面向对象语义上。

    image

    规约(Specification)

    Domain.Core项目下,NLayerApp定义了应用程序领域模型层所需要用到的规约框架,主要是通过LINQ Expression来实现的。在ISpecification接口中定义了SatisfiedBy方法,该方法返回一个LINQ Expression,用来执行判断领域对象是否能够满足当前规约条件的逻辑。NLayerApp的规约结构如下图所示:

    Specification

    有关规约模式,请参见:《Specifications》、《Specification Pattern》;有关规约模式、应用场景以及支持LINQ Expression的.NET规约实现,请参见:《EntityFramework之领域驱动设计实践(十):规约(Specification)模式》。本文就不再重复这些内容了。

    值得一提的是,NLayerApp的规约实现,在Specification抽象类中重载了一些逻辑运算符,这使得在实际应用中使用规约变得非常方便。

    领域服务(Domain Services)

    在DDD中,“服务”的概念得到了扩展,它表示在任何层中,包含了这样一种操作的类型,这种操作从逻辑上无法归结到任何对象上。因此“服务”并不仅仅是应用层或者基础结构层的专利,领域模型中也存在服务。在我的《EntityFramework之领域驱动设计实践【扩展阅读】:服务(Services)》一文中,对领域服务做了简单的介绍,供读者朋友参考。就NLayerApp而言,它实现了一个Bank Transfer的服务,首先定义了IBankTransferDomainService的接口,然后由BankTransferDomainService实现该接口。服务执行的参与者就是两个BankAccount实体,参数就是需要转账的金额。在Application层,BankingManagementService的PerformTransfer方法就使用了该服务来实现银行账户转账。

    领域模型(Domain Model)

    之前我也提到过,NLayerApp的领域模型是根据Entity Framework的Data Model,通过T4自动生成的,代码中除了包含了Data Model本身所定义的对象属性及对象间的关系外,还包含了基于Entity Framework实现STE的代码。从严格上讲,这并不是一个纯净的领域模型,其中STE的实现牵涉到了很多技术(而非领域)实现细节;此外,所有的领域对象都被DataContract修饰,也就意味着它们将同时以DTO的身份穿梭在网络中。NLayerApp的官方资料中对这种实现有过说明,解释过这种做法并不是很好的DDD实践,但它能够适用于NLayerApp。另外,NLayerApp采用C#的partial关键字向领域对象中添加了业务方法,Domain.MainModule.Entities项目下Partial子目录中包含了这些代码,比如在Order实体上实现了GetNumberOfItems操作。这一点与我以前在《EntityFramework之领域驱动设计实践 (一):从DataTable到EntityObject》一文中讨论的思路是相同的。在此,我们也不对NLayerApp的领域逻辑实现过程做太多介绍,有问题的朋友可以通过留言进行讨论。

    总结

    本文对NLayerApp的领域模型层做了简单的介绍,尤其对仓储接口的设计做了详细讨论。下篇文章我将介绍NLayerApp的应用层。

     

  • 相关阅读:
    初级工程师该如何去学习,如何去研发开关电源?
    CentOS-pam认证机制简介
    Linux-密码复杂度限制
    如何写一个简单的webserver(一):最简实现
    MySQL5.6绿色版安装(mysql-5.6.24-winx64.zip)
    Linux忘记roo密码的解决办法
    Linux中的netstat命令详解
    WireShark抓包分析(二)
    Wireshark抓取TCP包分析
    Wireshark 基本介绍和学习 TCP 三次握手
  • 原文地址:https://www.cnblogs.com/daxnet/p/2074451.html
Copyright © 2020-2023  润新知