技术为解决问题而生。

上面这个命题并非本文重点,我将来有空再谈这个。本文也并非什么了不起的技术创新,只是分享一下我对.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,很明显,这是一个相互依赖,这是不可行的:

解决方法大致有以下三种:

  1. 将Office的一些“公共功能”抽出成为新的模块,供HR使用,且抽出的新模块不依赖于HR
  2. 直接在HR中实现一部分Office的功能,使之不依赖Office
  3. 对各个业务逻辑模块进行“接口抽象”,详情后面会说

很明显,方案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模块依赖关系及程序结构的更多相关文章

  1. 【spring】jar包详解与模块依赖关系

    以spring3.X为例 jar包详解 1. spring-core.jar:包含Spring框架基本的核心工具类,Spring其它组件要都要使用到这个包里的类,是其它组件的基本核心: 2. spri ...

  2. seajs第二节,seajs各模块依赖关系

    index.html <!DOCTYPE html> <html> <head> <meta charset="utf-8"> &l ...

  3. Java 9 揭秘(4. 模块依赖)

    文 by / 林本托 Tips 做一个终身学习的人. 在此章节中,主要学习以下内容: 如何声明模块依赖 模块的隐式可读性意味着什么以及如何声明它 限定导出(exports)与非限定导出之间的差异 声明 ...

  4. C/C++源代码的Include依赖关系图

    前一篇博文中我曾仔细介绍过如何查看C/C++代码的依赖项关系图,在这篇文章中我将会介绍如何使用Visualization and Modeling Feature Pack 工具包,查看C/C++源代 ...

  5. spring framework体系结构及内部各模块jar之间的maven依赖关系

    很多人都在用spring开发java项目,但是配置maven依赖的时候并不能明确要配置哪些spring的jar,经常是胡乱添加一堆,编译或运行报错就继续配置jar依赖,导致spring依赖混乱,甚至下 ...

  6. [转] spring framework体系结构及内部各模块jar之间的maven依赖关系

    很多人都在用spring开发java项目,但是配置maven依赖的时候并不能明确要配置哪些spring的jar,经常是胡乱添加一堆,编译或运行报错就继续配置jar依赖,导致spring依赖混乱,甚至下 ...

  7. spring framework体系结构及模块jar依赖关系

    本文对于Spring的JAR包使用和配置,结合网友材料以spring 4.3.6.RELEASE版本为例,介绍spring框架结构和各模块对应JAR包以及模块间JAR依赖关系. 注:不同版本JAR包依 ...

  8. 【转】spring framework 5以前体系结构及内部各模块jar之间的maven依赖关系

    作者:凌承一  出处:http://www.cnblogs.com/ywlaker/  很多人都在用spring开发java项目,但是配置maven依赖的时候并不能明确要配置哪些spring的jar, ...

  9. 玩转IDEA项目结构Project Structure,打Jar包、模块/依赖管理全搞定

    前言 你好,我是A哥(YourBatman). 如何给Module模块单独增加依赖? 如何知道哪些Module模块用了Spring框架,哪些是web工程? IDEA如何打Jar包?打War包? 熟练的 ...

随机推荐

  1. 沉浸式状态栏_boolean hasTopLine = a.getBoolean(1, false);//AS会在"1"下显示错误红线

    TypedArray a = mContext.obtainStyledAttributes(attrs); boolean hasBottomLine = a.getBoolean(0, false ...

  2. Spark简介

    Spark是UC Berkeley AMP lab所开源的类Hadoop MapReduce的通用并行框架,Spark,拥有Hadoop MapReduce所具有的优点:但不同于MapReduce的是 ...

  3. Windows系统下部署安装一个/多个Tomcat8

    首先从http://tomcat.apache.org/上下载Tomcat8.0压缩版的,解压到指定路径后即可.  第一:在Windows系统中安装部署单个Tomcat         对于这种情况, ...

  4. 误打误撞写了段能让电脑奔溃的JS代码,但是自己不知道为什么,高手看到可以解答下吗?

    代码如下: <script> for(i=1;j=3*i;i++){ for(;j<=50;){ document.write(j+"<br>") } ...

  5. k-sum问题

    给定一个数组,里面的是任意整数,可能有重复,再给定一个目标T,从数组中找出所有和为T的K个数,要求结果中没有重复. Note: Elements in a quadruplet (a,b,c,d) m ...

  6. JavaScript中对象的含义与this的指向

    JavaScript中的对象:无序属性的集合 -其属性可以包含基本值.对象或函数.对象就是一组没有顺序的值.我们可以吧JavaScript中的对象想象成键值对,其中值可以是数据和函数.对象的行为和特征 ...

  7. 2008ISBN号码

    题目描述 Description 每一本正式出版的图书都有一个ISBN号码与之对应,ISBN码包括9位数字.1位识别码和3位分隔符,其规定格式如“x-xxx-xxxxx-x”,其中符号“-”是分隔符( ...

  8. Joseph(JAVA版)

    package Joseph;//约瑟夫环,m个人围成一圈.从第K个人开始报数,报道m数时,那个人出列,以此得到出列序列//例如1,2,3,4.从2开始报数,报到3剔除,顺序为4,3,1,2publi ...

  9. CentOS 7下MySQL安装配置

    安装: #centos 7 yum install -y mariadb mariadb-server mariadb-devel #debian 8 apt-get install nginx ma ...

  10. Unity4升级Unity5后Image Effects问题

    Assets\Editor\Image Effects\CameraMotionBlurEditor.js 会出现Ambiguous reference 'preview'错误提示,解决方法 查找pr ...