架构分层及IoC

一、框架整体分层设计

该不该分层以及分层的好处

关于该不该分层以及分层的好处,网上已经有相当多的讨论了,这里就不再赘述。这里引述一篇比较有代表性的文章《分层式结构的优缺点》以供参考,全文如下:

分层式结构究竟其优势何在?Martin Fowler在《Patterns of Enterprise Application Architecture》一书中给出了答案:

开发人员可以只关注整个结构中的其中某一层; 可以很容易的用新的实现来替换原有层次的实现; 可以降低层与层之间的依赖; 有利于标准化; 利于各层逻辑的复用。

概括来说,分层式设计可以达至如下目的:分散关注、松散耦合、逻辑复用、标准定义。

一个好的分层式结构,可以使得开发人员的分工更加明确。一旦定义好各层次之间的接口,负责不同逻辑设计的开发人员就可以分散关注,齐头并进。例如UI人员只需考虑用户界面的体验与操作,领域的设计人员可以仅关注业务逻辑的设计,而数据库设计人员也不必为繁琐的用户交互而头疼了。每个开发人员的任务得到了确认,开发进度就可以迅速的提高。

松散耦合的好处是显而易见的。如果一个系统没有分层,那么各自的逻辑都紧紧纠缠在一起,彼此间相互依赖,谁都是不可替换的。一旦发生改变,则牵一发而动全身,对项目的影响极为严重。降低层与层间的依赖性,既可以良好地保证未来的可扩展,在复用性上也是优势明显。每个功能模块一旦定义好统一的接口,就可以被各个模块所调用,而不用为相同的功能进行重复地开发。

进行好的分层式结构设计,标准也是必不可少的。只有在一定程度的标准化基础上,这个系统才是可扩展的,可替换的。而层与层之间的通信也必然保证了接口的标准化。

分层式结构同样也具有一些缺陷:

降低了系统的性能。这是不言而喻的。如果不采用分层式结构,很多业务可以直接造访数据库,以此获取相应的数据,如今却必须通过中间层来完成。 有时会导致级联的修改。这种修改尤其体现在自上而下的方向。如果在表示层中需要增加一个功能,为保证其设计符合分层式结构,可能需要在相应的业务逻辑层和数据访问层中都增加相应的代码。

分层与面向接口编程

谈到分层之间的那些接口,大概马上就会有很多同学来吐槽了,不愿意用接口,原因无非如下:

  • 定义接口导致项目分层过细,增加一个模块,要增加很多的类文件及代码量,增加开发人员的工作量

  • 单一实现的接口,实际用处不大,但阅读代码时,接口导致“转到定义”只能转到接口上,实现类还得手动去找。

以上是通常拒绝使用接口的理由。

但是,从我个人的看法,从接口使用的利弊来说,利是远远大于弊的:

  • 使用接口的好处之一是解耦各个层次的依赖,让我们能轻松将“具体实现”替换掉(对于大多数项目而言,这个好处并不明显)

  • 接口是项目的骨架,是架构的整体体现,架构师在构建项目的时候,直接定义出各个层次的接口,就把整个项目的主体骨架设计出来了,随后开发人员只需要以“填空”的方式去逐个实现各种方法,能极大的提高开发效率,并保证项目的完整性与合理性

  • 在进行层次的单元测试的时候,由于各个层次是以接口隔离的,上层不依赖于下层的具体实现,测试上层的时候,只要使用Moq、Fakes等Mock框架对下层接口进行模拟,即可非常轻松的测试上层的业务实现。如果上层是与下层紧耦合的,那上层的代码几乎是不可测试的。(如果不了解单元测试,可以看看我写的两篇文章:基于VS2012 Fakes框架的TDD实战

补充:关于接口是否有必要的讨论,iteye.com上有一个帖子讨论得非常热烈,强烈推荐看一下:

主题:在项目架构中如何进行分层才是最合理的?

OSharp的分层设计

OSharp开发框架约定的分层方案,依然是传统的三层(数据层 - 业务层 - 展现层)分层方式,但也有自己的特点:

  • 使用了三层分层方式,但并不严格遵守传统三层的“各层职责极其分明”的约定

  • 参考了“领域驱动设计”的一些思想,但为了保证性能,并不使用 DDD 的聚合模型

数据层:

如上图所示,OSharp的数据层的作用是向业务层提供数据实体的数据库持久化操作,向业务层与展现层提供进行数据查询的查询数据集。对外API很简单,主要是 IUnitOfWork 与 IRepository<TEntity> 两个接口,同时定义了支持泛型主键类型的实体模型基类 EntityBase,数据传输对象接口 IAddDto 与 IEditDto。

业务层:

OSharp的业务层以模块为划分,一个模块是业务内聚的一个或多个实体组成的操作单元,由三个部分组成:

  1. 数据实体模型(Model)的定义,数据模型是业务层与数据层之间的数据交互对象,也是数据层进行持久化的对象,为业务数据的最终承载。

  2. 数据传输对象(Dto)的定义,数据传输对象是展现层向业务层的交互对象,Dto在需要的时候也可以作为视图模型展现到页面上。

  3. 业务处理模块的定义,业务处理模块包括业务契约(IXXXContract)的定义与业务逻辑(IXXXService)的实现。业务处理模块从展现层接受 Dto 形式的数据,经过业务处理后,转化为Model提交给数据层进行持久化操作。业务处理模块同时还向展现层提供相应实体的 IQueryable<TEntity> 类型的查询数据集,作为展现层进行数据查询的数据源。原则上业务处理模块只处理数据的插入、更新、删除操作,不对展现层提供数据查询服务。

业务层有如下特点:

  1. 业务层接受来自展现层的参数为 Dto 或简单类型参数

  2. 业务层向展现层返回业务操作结果为 OperationResult 类型

  3. 业务层只处理 插入、修改、删除等非查询业务,原则上不提供展现层数据的查询业务

  4. 业务层向展现层开放相应实体的 IQueryable<T> 类型的查询数据集,作为展现层的查询数据源

  5. Dto 与 Model 的转换工作由 Automapper 来完成

  6. Model 的构建工作由业务层来完成,展现层向业务层提供必要的 Dto,不参与 Model 的构建工作

展现层:

MVC 的展现层包含 控制器(Controller)与 视图(View)两个部分。控制器负责使用业务层开放的查询数据集来进行数据查询操作,再把查询结果提交给 视图 进行展示,还负责接收来自 View 提交的数据,转换为 Dto 提交给业务层进行处理,并接收并解析业务层反馈的业务处理结果(OperationResult),交给 视图 展现给用户。视图 接收并解析 控制器 传递过来的数据,解析成 HTML 页面发送到浏览器进行展示,并向 控制器 提交用户的请求数据。

在本篇,只是对分层做一个简要的讲解,从整体上认识 OSharp 的分层架构,在后面进入实际应用的时候,还要对各个层进行更详细的分析。

二、分层解耦与依赖注入

前面已经分析了解耦的必要性,那么,OSharp 开发框架中的解耦,是怎样实现的呢?OSharp 中的解耦工作,主要是通过 Autofac 这个 IoC 组件来完成的。Autofac 是一个轻量级的,对系统污染与侵入性非常小的 IoC 组件,并且对 Webform、MVC、WebApi、SignalR等主流 ASP.NET Web 技术都有非常好的支持,是个相当不错的 IoC 组件。

为了统一管理 IoC 相关的代码,并避免在 OSharp 底层类库中到处引用 Autofac 这个第三方组件,OSharp 中定义了一个专门用于管理需要依赖注入的接口与实现类的空接口 IDependency:

/// <summary>
/// 依赖注入接口,表示该接口的实现类将自动注册到IoC容器中
/// </summary>
public interface IDependency
{ }

这个接口没有任何方法,不会对系统的业务逻辑造成污染,所有需要进行依赖注入的接口,都要继承这个空接口,例如:

业务单元操作接口:

/// <summary>
/// 业务单元操作接口
/// </summary>
public interface IUnitOfWork : IDependency
{
    ...
}

实体仓储操作接口:

/// <summary>
/// 实体仓储模型的数据标准操作
/// </summary>
/// <typeparam name="TEntity">实体类型</typeparam>
/// <typeparam name="TKey">主键类型</typeparam>
public interface IRepository<TEntity, TKey> : IDependency where TEntity : EntityBase<TKey>
{
    ...
}

账户模块业务契约:

/// <summary>
/// 业务契约——账户模块
/// </summary>
public interface IIdentityContract : IDependency
{
    ...
}

在需要引用 注入对象 的地方,统一使用“构造函数注入”的方式来进行注入。

实体仓储实现类:

/// <summary>
/// EntityFramework的仓储实现
/// </summary>
/// <typeparam name="TEntity">实体类型</typeparam>
/// <typeparam name="TKey">主键类型</typeparam>
public class Repository<TEntity, TKey> : IRepository<TEntity, TKey> where TEntity : EntityBase<TKey>
{
    private readonly IUnitOfWork _unitOfWork;

    public Repository(IUnitOfWork unitOfWork)
    {
        _unitOfWork = unitOfWork;
    }

    ...

}

账户模块业务实现类:

/// <summary>
/// 业务实现——账户模块
/// </summary>
public partial class IdentityService : ServiceBase, IIdentityContract
{
    private readonly IRepository<User, int> _userRepository;
    private readonly IRepository<Role, int> _roleRepository;
    private readonly IRepository<Organization, int> _organizationRepository;

    /// <summary>
    /// 初始化一个<see cref="IdentityService"/>类型的新实例
    /// </summary>
    public IdentityService(IRepository<User, int> userRepository,
        IRepository<Role, int> roleRepository,
        IRepository<Organization, int> organizationRepository)
        : base(userRepository.UnitOfWork)
    {
        _userRepository = userRepository;
        _roleRepository = roleRepository;
        _organizationRepository = organizationRepository;
    }
}

Autofac 是支持批量子类注册的,有了 IDependency 这个基接口,我们只需要 Global 中很简单的几行代码,就可以完成整个系统的依赖注入匹配:

ContainerBuilder builder = new ContainerBuilder();
builder.RegisterGeneric(typeof(Repository<,>)).As(typeof(IRepository<,>));
Type baseType = typeof(IDependency);

// 获取所有相关类库的程序集
Assembly[] assemblies = ...

builder.RegisterAssemblyTypes(assemblies)
    .Where(type => baseType.IsAssignableFrom(type) && !type.IsAbstract)
    .AsImplementedInterfaces().InstancePerLifetimeScope();//InstancePerLifetimeScope 保证对象生命周期基于请求
IContainer container = builder.Build();
DependencyResolver.SetResolver(new AutofacDependencyResolver(container));

如此,只有站点主类库需要引用 Autofac,而不是到处都存在着注入的相关代码,大大降低了系统的复杂度。

三、开源说明

(一)github.com

OSharp项目已在github.com上开源,地址为:https://github.com/i66soft/osharp,欢迎阅读代码,欢迎 Fork,如果您认同 OSharp 项目的思想,欢迎参与 OSharp 项目的开发。

在Visual Studio 2013中,可直接获取 OSharp 的最新源代码,获取方式如下,地址为:https://github.com/i66soft/osharp.git

(二)nuget

OSharp的相关类库已经发布到nuget上,欢迎试用,直接在nuget上搜索 “osharp” 关键字即可找到