本文也并非什么了不起的技术创新,只是分享一下我对.net模块依赖关系及程序结构方面的一些看法。重点讲述了模块相互依赖的问题,以及如何用StructureMap来解决这个问题。
技术为解决问题而生。
上面这个命题并非本文重点,我将来有空再谈这个。本文也并非什么了不起的技术创新,只是分享一下我对.net模块依赖关系及程序结构方面的一些看法。先看一个最最简单的hello world网站的模块结构如何:

- 将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