再次调整项目架构是因为和群友dezhou的一次聊天,我原来的想法是项目尽量做简单点别搞太复杂了,仅使用了DbContext的注入,其他的也没有写接口耦合度很高。和dezhou聊过之后我仔细考虑了一下,还是解耦吧,本来按照软件设计模式就应该是高内聚低耦合的,低耦合使项目的模块独立于其他模块,增加了可维护性和移植性!
注:前面写的博客详细记录没项目操作的每一步,其实写起博客来很费时间,而且整片博文里很多无用的信息。对MVC来说会添加控制器,添加视图,添加类这些都最基本的要求了,并且前面博文里都写了,后面也就不再详细写这些东西了,主要写一些思路和关键代码,具体内容以源代码的形式放在博客后面提供下载。
一、默认项目结构
我们看一下,vs2015默认生成的项目结构。
项目中模型、数据访问、业务逻辑和视图相关的内容都在一个项目中,视图、业务逻辑和显示紧紧耦合,前期看着还没什么,到了内容多了项目变大以后,尤其是隔一段时间再更新项目,在看的话一片混乱,有时候一个小的改动造成整个项目导出报错,头痛之极。
二、三层架构
我们再看看三层架构:
- 用户界面表示层(USL)
- 业务逻辑层(BLL)
- 数据访问层(DAL)
三层架构主要是使项目结构更清楚,分工更明确,有利于后期的维护和升级。它未必会提升性能,因为当子程序模块未执行结束时,主程序模块只能处于等待状态。这说明将应用程序划分层次,会带来其执行速度上的一些损失。但从团队开发效率角和维护性上来说易于进行任务分配,可维护性高。
按照三层的思想,MVC中的控制器(C)和视图(V)都是处理界面显示相关的内容属于用户界面表示层(USL) ,模型(M)是控制器和视图间交换的数据,所以MVC框架应该都属于三层中的用户界面表示层。
数据访问层(DAL)和业务逻辑层(BLL) 、业务逻辑层和用户界面表示层(USL) 也要交换数据,干脆把模型(M)独立出来,作为控制器和视图,及三个层次之间交换的数据。
三、高耦合
我们看向Ninesky现在的项目结构,如下图:
包含四个项目:
Ninesky.DataLibrary是数据访问层,提供数据库访问的支持。
Ninesky.Base 是业务逻辑层,负责业务逻辑的处理。
Ninesky.Web 用户界面表示层(USL),负责显示页面和显示项目的逻辑处理。
Ninesky.Models 就是各层之间交换的数据实体。
从以上可以看到项目按照三层的思想进行了分层。PS:有群友问为什么项目名称叫DataLibrary、Base,不叫DAL,BLL?这可能是强迫症的原因,我反正看着DAL,BLL的项目名称特别不舒服,改了个自己喜欢的名字,其实功能都一样的。
再看一下项目的调用
看一下Ninesky.Base的CategoryService类。
代码中位置1声明了类CategoryRepository,这个类是 Ninesky.DataLibrary中的一个类。位置2将这个项目实例化了,在位置3处我们直接调用了这个类的Find方法。从上面可以看出CategoryService类是依赖CategoryRepository类的;Ninesky.Base项目是依赖于Ninesky.DataLibrary项目的。一个项目的类精确的调用了另一个项目类的方法那么他们之间就是高耦合。发生高耦合就是软件设计有问题,就要解耦,把依赖实现代码转换成依赖逻辑,这时候就要引入抽象层(通常是接口)。
四、依赖接口
我们添加一个dll项目Ninesky.InterfaceDataLibrary,给Ninesky.DataLibrary添加对Ninesky.InterfaceDataLibrary项目的引用。
在Ninesky.InterfaceDataLibrary项目添加InterfaceBaseRepository接口
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 using System; 2 using System.Collections.Generic; 3 using System.Linq.Expressions; 4 using System.Threading.Tasks; 5 6 namespace Ninesky.InterfaceDataLibrary 7 { 8 /// <summary> 9 /// 仓储基类接口 10 /// </summary> 11 /// <typeparam name="T"></typeparam> 12 public interface InterfaceBaseRepository<T> where T : class 13 { 14 /// <summary> 15 /// 查询[不含导航属性] 16 /// </summary> 17 /// <param name="predicate">查询表达式</param> 18 /// <returns>实体</returns> 19 T Find(Expression<Func<T, bool>> predicate); 20 } 21 }
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Linq.Expressions; 5 using Microsoft.EntityFrameworkCore; 6 using Ninesky.InterfaceDataLibrary; 7 8 namespace Ninesky.DataLibrary 9 { 10 /// <summary> 11 /// 仓储基类 12 /// </summary> 13 public class BaseRepository<T> :InterfaceBaseRepository<T> where T : class
14 { 15 protected DbContext _dbContext; 16 public BaseRepository(DbContext dbContext) 17 { 18 _dbContext = dbContext; 19 } 20 21 /// <summary> 22 /// 查询[不含导航属性] 23 /// </summary> 24 /// <param name="predicate">查询表达式</param> 25 /// <returns>实体</returns> 26 public virtual T Find(Expression<Func<T, bool>> predicate) 27 { 28 return _dbContext.Set<T>().SingleOrDefault(predicate); 29 } 30 } 31 } 32
在Ninesky.Base项目中引用Ninesky.InterfaceDataLibrary,我们在修改CategoryService代码
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 public class CategoryService 2 { 3 private InterfaceBaseRepository<Category> _categoryRepository; 4 public CategoryService(DbContext dbContext) 5 { 6 _categoryRepository = new BaseRepository<Category>(dbContext); 7 } 8 9 /// <summary> 10 /// 查找 11 /// </summary> 12 /// <param name="Id">栏目Id</param> 13 /// <returns></returns> 14 public Category Find(int Id) 15 { 16 return _categoryRepository.Find(c => c.CategoryId == Id); 17 } 18 }
在代码开始处声明了变量类型为InterfaceBaseRepository的变量,在构造函数中将InterfaceBaseRepository实例化为BaseRepository类型。
现在Ninesky.Base项目依然Ninesky.DataLibrary项目进行了依赖,并没有进行解耦,如果要想解除多Ninesky.DataLibrary的依赖就要想办法把接口的实例化转移到项目之外去。
五、控制反转
控制反转就是把依赖的创建移到类的外部。那么我们修改CategoryService类的构造函数。
构造函数传递了一个接口类型的参数,现在类中完全和Ninesky.DataLibrary没有了关系,可以删除对Ninesky.DataLibrary项目的引用了。
那现在又有了一个新的问题:控制反转如何实现,怎么进行接口的实例化?
常用的解决方法有服务定位器和依赖注入。
六、服务定位器
服务定位器就是在类中集中进行实例化。
单独创建一个项目,添加对项目的引用,然后再工厂类中集中进行实例化。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 public class Factory 2 { 3 public InterfaceBaseRepository<Category> GetBaseRepository() 4 { 5 return new BaseRepository<Category>(); 6 } 7 }
服务定位器的好处是实现比较简单,可以创建一个全局的服务定位器,缺点就是组件需求不透明。Ninesky采用另一种控制反转的实现:依赖注入。
七、依赖注入。
以前.Net MVC中注入挺麻烦的,幸好.Net Core MVC中内建了依赖注入的支持。
修改CategoryController代码,使用构造函数注入。这里为了例子的简单在控制器中直接使用数据存储层的类进行注入,而没有使用业务逻辑层的类。
控制器中采用构造函数注入,构造函数中传递CategoryService参数。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 public class CategoryController : Controller 2 { 3 /// <summary> 4 /// 数据上下文 5 /// </summary> 6 private NineskyDbContext _dbContext; 7 8 /// <summary> 9 /// 栏目服务 10 /// </summary> 11 private CategoryService _categoryService; 12 13 public CategoryController(CategoryService categoryService) 14 { 15 _categoryService = categoryService; 16 } 17 18 /// <summary> 19 /// 查看栏目 20 /// </summary> 21 /// <param name="id">栏目Id</param> 22 /// <returns></returns> 23 [Route("/Category/{id:int}")] 24 public IActionResult Index(int id) 25 { 26 var category = _categoryService.Find(id); 27 if (category == null) return View("Error", new Models.Error { Title = "错误消息", Name="栏目不存在", Description="访问ID为【"+id+"】的栏目时发生错误,该栏目不存在。" }); 28 switch (category.Type) 29 { 30 case CategoryType.General: 31 if (category.General == null) return View("Error",new Models.Error { Title="错误消息", Name="栏目数据不完整",Description="找不到栏目【"+category.Name+"】的详细数据。" }); 32 return View(category.General.View, category); 33 case CategoryType.Page: 34 if (category.Page == null) return View("Error", new Models.Error { Title = "错误消息", Name = "栏目数据不完整", Description = "找不到栏目【" + category.Name + "】的详细数据。" }); 35 return View(category.Page.View, category); 36 case CategoryType.Link: 37 if (category.Link == null) return View("Error", new Models.Error { Title = "错误消息", Name = "栏目数据不完整", Description = "找不到栏目【" + category.Name + "】的详细数据。" }); 38 return Redirect(category.Link.Url); 39 default: 40 return View("Error", new Models.Error { Title = "错误消息", Name = "栏目数据错误", Description = "栏目【" + category.Name + "】的类型错误。" }); 41 42 } 43 } 44 }
然后我们进入,Web的启动类Startup进行注入。如下图:
第一个红框内是在《2.1、栏目的前台显示》中注入的上下文;
第二个红框到第四个红框内是今天添加的内容。
第三个红框内注入InterfaceBaseRepository接口,使用BaseRepository进行实例化。
有第二个红框的内容是因为BaseRepository实例化时有一个DbContext类型的参数。在注入的时候要求用到的参数必须要在前面注入,并且系统并不会自动吧NineskyDbContext转换为DbContext。所以必须注入一个DbContext类型的参数。
第四个红框是注入CategoryService。这里CategoryService同样可以使用接口,时间原因没写。
至此可以看到,CategoryService解除了对BaseRepository的依赖,在Ninesky.Base项目中没有对Ninesky.DataLibrary进行任何的依赖,类的实例化是在Web项目中进行注入的,Web项目对Ninesky.DataLibrary进行了依赖。同样的方法也可以实现Web项目对Ninesky.Base项目的解耦。
如果要完全解除Ninesky.Web项目对Ninesky.DataLibrary和Ninesky.Base项目的依赖,可以使用配置文件加载,这次先不写了。
F5浏览器中查看一下,可以看到取出了数据,只是因为数据存储层的代码没有包含导航属性所以数据不完整。
八、其他
代码托管地址:https://git.oschina.net/ninesky/Ninesky
文章发布地址:http://www.ninesky.cn