谈谈.net模块依赖关系及程序结构
技术为解决问题而生。
上面这个命题并非本文重点,我将来有空再谈这个。本文也并非什么了不起的技术创新,只是分享一下我对.net模块依赖关系及程序结构方面的一些看法。先看一个最最简单的hello world网站的模块结构如何:
就一个Website,没有任何层次划分,因为简单嘛。但很快,你就发现,还是把网站和业务逻辑处理层分开比较好,于是变成:
箭头从BLL指向Website,表明Website依赖于BLL。随着BLL的内容的不断增多,你发现需要再细分一下,于是把BLL划分为处理人事业务的HR和办公自动化的Office(假如你在做一个企业应用):
随着HR和Office的内容的增多,你发现HR和Office中有些公共的东西可以提取出来,比如部门信息及客户信息等,于是你创建了一个公共资料模块PubMaterial,HR及Office都会用到它:
接着你很快又发现还是有些公共代码得提取出来,否则会导致大量重复代码,比如统一的日志记录、异常类型、数据库连接、加解密……等,于是你又弄了一个Common模块,其它模块都要用到它:
你很满意,是的,到目前为止,都没有碰到太大的问题,但接下来,问题来了,Office要新加一个功能,这个功能需要获取员工的具体信息,而员工具体信息HR中有实现,让Office直接调用HR模块吗?这样的话本来HR和Office之间的并列关系就被破坏了,但程序还是可以运行,技术上来说还是没问题的:
上面只是破坏了原本企图保持的业务逻辑模块之间的平行关系,技术上问题不大,但接下来真的是问题了,有一天老板需要在HR中新增一个功能,需要能直接看到员工的工作简报,工作简报本来应该是Office的功能,现在需要在HR中进行一些额外的处理,这样HR需要依赖于Office,很明显,这是一个相互依赖,这是不可行的:
解决方法大致有以下三种:
- 将Office的一些“公共功能”抽出成为新的模块,供HR使用,且抽出的新模块不依赖于HR
- 直接在HR中实现一部分Office的功能,使之不依赖Office
- 对各个业务逻辑模块进行“接口抽象”,详情后面会说
很明显,方案2会导致重复代码,肯定不可取;而方案1也存在很大的问题,因为接下来老板要求新增计算和查看统计数据的Report模块和处理公司审批流程的Flow模块,它们与HR存在相互依赖的关系,这次看看你如何招架:
然后,再要加呢?“Oh my god!”你喊道:“快给我讲方案3吧。”OK,现在我来摆出方案3,这不一定是最好的办法,但却是我所要使用的办法:
图看起来有些眼花缭乱,但再仔细一看便发觉其实只是给原先的每个模块增加了一个“接口层”而已,比如Office模块,增加了IOffice,现在就变成Office是对IOffice的实现,而原先需要依赖于Office的模块,现在转去依赖IOffice,而“接口层”仅仅变成了一个描述,它只包括数据及方法的声明,不包括任何实现,所以它可以不依赖于任何其它模块,它只被别的模块所依赖,包括实现它的模块。相信到此大家也都理解了。那么上图那个虚框和“Config”模块是什么意思呢?
试着这样想:假如现在Website要调用Office模块,通过IOffice,但是实现IOffice的却是Office模块,那是不是得先实例化一个Office?谁来负责实例化?如果是Website负责这个实例化工作的话,那势必Website会直接依赖与Office模块,这样就是走老路了,会出问题,而且Office本身又依赖于IHR、IPubMaterial和ICommon,那对应的HR、PubMaterial和Common又谁来实例化?看来如果照着老思路下去的话,还是会掉进“依赖关系地狱”中去的。不行,我们得借助一些工具,当然,重复造轮子也是不可取的,所以我们这次要使用一个叫StructureMap的工具。
使用过Microsoft Enterprise Library的朋友也许都不会对Unity陌生,这是一个IoC(Inversion of Control)工具,后来IoC又有了一个更贴切的名称,叫DI(Dependency Injection),IoC和DI其实都是为了解决程序模块之间的依赖关系而提出来的设计模式,它们本身并不是一种具体的技术,只是一些设计套路,旨在给程序模块松耦(Loose Coupling)。StructureMap也是一个和Unity类似的DI工具,个人感觉很不错。
我目前从NuGet上获取到的StructureMap的版本是2.6.4.1,需要说明的是StructureMap官方网站上提供的文档有些落后,跟不上形势了。所幸的是我们并不需要用它的所有功能,大多数软件不都这样么?80%的功能是留给20%的人用的。
有了这个StructureMap,我们要使用ICommon,直接告诉StructureMap就是,具体用什么去实例化,怎么实例化,不用我们操心,StructureMap自动帮我们做,这就是解决的思路。现在剩下的问题就只是告诉StrutureMap “ICommon”和“Common”的关系即可,可以直接用代码来指明关系,也可以用配置文件,配置文件看起来更具“扩展性”,但这样也就没有了编译期的出错检查,反正都是得写,我这次就用代码直接指明它们之间的对应关系吧。上图中的Config,就是这么一个用来指明接口与实现之间的对应关系的配置。(除了硬编码和写配置文件之外,StructureMap还可以使用反射来自动建立接口和实现体之间的对应关系,具体可以参考它的文档,这里略过不表)
在开始一个真正的例子之前,我还是得说明一些东西,那就是前面所提到的“模块”其实在不同的场合就有着不同的意义,这是一个抽象的概念,有时候是一个代码文件,有时候是一个类,有时候后是一个类库,有时候是一个程序集,大家不要纠结于它究竟是什么,而是要重点看看在自己的使用当中如何给它们松耦。
OK,都了解完之后,我们就开始一个小小的例子。简单起见,我删掉了许多模块,程序结构图变成了:
IHR的内容有:
public class Employee
{
public string EmpNo { get; set; }
public string ChineseName { get; set; }
} public class EmployeeDetail:Employee
{
public string Descriptions { get; set; }
public int TaskAmount { get; set; }
} public interface IHr
{
IEnumerable<Employee> GetAllEmployees();
EmployeeDetail GetEmployeeByNo(string strEmpNo);
int GetEmployeeAmount();
void AddEmployee(Employee emp);
string GetEmpNameByNo(string strEmpNo);
}
IOffice的功能有:
public class Task
{
public int Id { get; set; }
public string EmpNo { get; set; }
public string Descriptions { get; set; }
} public class TaskDetail : Task
{
public string EmpName { get; set; }
} public interface IOffice
{
IEnumerable<Task> GetAllTasks();
TaskDetail GetTask(int id);
void AddTask(string strEmpNo, string strDesc);
void DeleteTask(int id);
int GetTaskAmountOfEmployee(string strEmpNo);
}
ICommon的功能有:
public enum LogType
{
Information,
Warning,
Error
} public interface ICommon
{
void Log(LogType logType, string moduleName, string content, params object[] values);
}
很明显,这些都是“接口”,不包含任何的实现,它们只会被别的模块依赖,而不会依赖别的模块。
功能相信一看名字就知道怎么回事,不需要太多解释。值得说一下的是HR模块的“GetEmployeeByNo”方法返回的“EmployeeDetail”中包括了一个“TaskAmount”,也就是这个员工的“任务数”,这是Office模块的功能,这意味着HR模块需要调用Office模块的方法;接着看Office模块中的“GetTask”方法,会返回一个“TaskDetail”,“TaskDetail”中含有“EmpName”,即员工姓名,这个需要从HR模块中获取。这是一个“相互依赖”!但这次我向你保证没有问题,因为我们采用了新的设计模式工具——StructureMap,现在,我们来告诉StructureMap如何来帮助我们生成这些接口的实例,看代码:
public static class ContainerBootstrapper
{
public static void BootstrapStructureMap()
{
ObjectFactory.Initialize(x =>
{
x.For<IHr>().Singleton().Use<HrManager>();
x.For<IOffice>().Singleton().Use<OfficeManager>();
x.For<ICommon>().Singleton().Use<CommonManager>().Ctor<string>("logPath").Is(AppDomain.CurrentDomain.BaseDirectory+"log");
});
}
}
一个静态配置类,一个静态方法,只需要在程序入口函数处调用一下即可。上面的“Singleton”方法是告诉StructureMap我们的HR、Office和Common模块都是“单实例”的,自始自终只有一个实例,如果把“.Singleton()”去掉,那么每次请求实例的时候都会创建一个新的实例出来,另外还有“HttpContextScoped”和“HybridHttpOrThreadLocalScoped”模式,前者表示每个HttpContext使用一个实例,这得在Web程序中才有效,否则按默认处理,而后者则会判断当前程序类型,如果是Web程序,那么和前者一样,如果不是Web程序,那么就每个线程使用一个实例, 究竟怎么选择,这个要看你自身的需要,在我的这个小小的demo中,Singleton即可。另外,对ICommon的实现的CommonManager的构造函数是带参数的,需要指定,否则就会出现运行时错误,上面的代码的意思是logPath这个参数是个string,把“AppDomain.CurrentDomain.BaseDirectory+"log"”作为它的值初始化。
接下来看看Main函数代码片段,简单起见,我写了个控制台程序来“冒充”Website:
static void Main(string[] args)
{
//初始化StructureMap
ContainerBootstrapper.BootstrapStructureMap(); //获取HR实例
IHr hr = ObjectFactory.GetInstance<IHr>(); //用之
//... //获取Office实例
IOffice office = ObjectFactory.GetInstance<IOffice>(); //用之
//...
}
非常好!这样一来,模块项目依赖的关系就解决了。大家还可以看看“log”目录,你会发现一次运行中,尽管多次请求获取HR和Office的实例(HR中会请求Office,而Office也会请求HR),但它们都只会被初始化了一次,这是由于配置中指明了它们是Singleton。现在,我们来看看这个Solution的结构:
在这个Solution中,我是把IHr、IOffice和ICommon合并到了Interface这个Project中去,这样的话项目引用会少一点,但如果你要把它们分开的话也是完全没有问题的。
花了这么大的力气来处理模块相互依赖的问题其实还有一个目的,那就是可以做单元测试,如果时间允许,我将会另写一篇文章来讲述如何测试。
At Last,当然少不了完整代码(Visual Studio 2010):structuremap_demo.7z
谈谈.net模块依赖关系及程序结构的更多相关文章
- 【spring】jar包详解与模块依赖关系
以spring3.X为例 jar包详解 1. spring-core.jar:包含Spring框架基本的核心工具类,Spring其它组件要都要使用到这个包里的类,是其它组件的基本核心: 2. spri ...
- seajs第二节,seajs各模块依赖关系
index.html <!DOCTYPE html> <html> <head> <meta charset="utf-8"> &l ...
- Java 9 揭秘(4. 模块依赖)
文 by / 林本托 Tips 做一个终身学习的人. 在此章节中,主要学习以下内容: 如何声明模块依赖 模块的隐式可读性意味着什么以及如何声明它 限定导出(exports)与非限定导出之间的差异 声明 ...
- C/C++源代码的Include依赖关系图
前一篇博文中我曾仔细介绍过如何查看C/C++代码的依赖项关系图,在这篇文章中我将会介绍如何使用Visualization and Modeling Feature Pack 工具包,查看C/C++源代 ...
- spring framework体系结构及内部各模块jar之间的maven依赖关系
很多人都在用spring开发java项目,但是配置maven依赖的时候并不能明确要配置哪些spring的jar,经常是胡乱添加一堆,编译或运行报错就继续配置jar依赖,导致spring依赖混乱,甚至下 ...
- [转] spring framework体系结构及内部各模块jar之间的maven依赖关系
很多人都在用spring开发java项目,但是配置maven依赖的时候并不能明确要配置哪些spring的jar,经常是胡乱添加一堆,编译或运行报错就继续配置jar依赖,导致spring依赖混乱,甚至下 ...
- spring framework体系结构及模块jar依赖关系
本文对于Spring的JAR包使用和配置,结合网友材料以spring 4.3.6.RELEASE版本为例,介绍spring框架结构和各模块对应JAR包以及模块间JAR依赖关系. 注:不同版本JAR包依 ...
- 【转】spring framework 5以前体系结构及内部各模块jar之间的maven依赖关系
作者:凌承一 出处:http://www.cnblogs.com/ywlaker/ 很多人都在用spring开发java项目,但是配置maven依赖的时候并不能明确要配置哪些spring的jar, ...
- 玩转IDEA项目结构Project Structure,打Jar包、模块/依赖管理全搞定
前言 你好,我是A哥(YourBatman). 如何给Module模块单独增加依赖? 如何知道哪些Module模块用了Spring框架,哪些是web工程? IDEA如何打Jar包?打War包? 熟练的 ...
随机推荐
- RTMP协议中文翻译(首发)(转)
Adobe公司的实时消息传输协议 摘要 此备忘录描述了 Adobe公司的实时消息传输协议(RTMP),此协议从属于应用层,被设计用来在适合的传输协议(如TCP)上复用和打包多媒体传输流(如音频.视频和 ...
- row_number和partition by分组取top数据
分组取TOP数据是T-SQL中的常用查询, 如学生信息管理系统中取出每个学科前3名的学生.这种查询在SQL Server 2005之前,写起来很繁琐,需要用到临时表关联查询才能取到.SQL Serve ...
- android6.0锁屏界面接收新通知处理流程
灭屏状态下,接收新信息,屏幕会半亮显示通知流程: 1,应用构造notification后,传给NotificationManager,而后进入NotificationManagerService处理. ...
- css3的transition过渡
从*开始样式*,经过指定*时间*后,缓慢过渡到*结束样式* 语法:transition:要变化的属性名 持续时间 速度变化类型 延迟 强调:写在开始样式中 如何实现多个属性同时过渡:2种办法: 1. ...
- Google高级搜索语法
Google高级搜索语法 Google搜索果真是一个强悍的不得了的搜索引擎,今天转了一些 google的高级搜索语法 希望能帮助到大家. 一.allinanchor: anchor是一处说明性的文 ...
- 如何更改OS系统下截图生成图片格式 jpg pdf
Mac系统下快速截屏的快捷键: 1.截全屏: shift + command + 3 2.选取截屏 shift + command + 4 生成的图片,系统默认格式忘了,反正不好用,用下面命令更改生成 ...
- android json解析详细介绍之gson
废话不多说,什么json是轻量级数据交换标准:自己百度去深入了解:这里有三种json解析工具.本人只用过其中两种: 1.Google Json利器之Gson 评价:简单,方便. 2.阿里巴巴 ...
- VS与ultraedit 正则表达式替换
ASP中把request("{param}")调用替换为requestX("{param}") VS 表达式替换(?<a>request\(&quo ...
- Codeigniter 在Active Record中限制批量更新数目
今天手头电商项目有个需求是:将订单中的优惠券自动发放给买家,所以要只更新优惠券表中的某几行数据,查了手册和网络都没有解决办法. 一开始用循环和遍历来做都是错的,因为update语句一下就更新掉所有符合 ...
- python ImportError: DLL load failed: %1 不是有效的 Win32 应用程序
导入的时候报出了 ImportError 在windows上安装python 的模块后,导入模块时报 python ImportError: DLL load failed: %1 不是有效的 Win ...