设计框架的目标就是简化程序员的开发难度,标准化开发规则和制定业务规则。所以,这里要注意一个问题:框架并不是万能的!这一点很重要,否则,也不需要我们这些程序员了。对于架构师来说,完成之前的要求之后,还要充分考虑“扩展性”。
个人认为,扩展性分两种:一种是技术性扩展,一种是业务性的扩展。从这两点出发思考,就是框架从要“做什么”到“怎么做”的转变。
技术性扩展,也分两种,一就是能够接入新的技术,融入框架中,这个可能比较容易,毕竟一般是由架构师来完成的,自然不会容易出现跳出规则的情况,也就是开发这部分的人,水平应该比较高;二就是能够允许二次扩展开发。框架就如建房子的地基,要允许设计师装修,增加些点缀也好,增加房间也罢,要有足够的空间让程序员自己去发挥,但是这种发挥也是要遵守框架原有的规则。那么这里就需要充分理解开发模式,或者说,简单一点,知道工厂模式就好了,例如,我设计的框架,有一个程序启动时候的加载器,在.net461中放在Global中加载。有一些程序就是加载一次就可以的,例如在框架中定义一个接口IStarter
public interface IStarter
{
void Start();
void Stop();
}
然后建立一个启动器的加载器,这里把要加载的启动器放到bin目录中的starter目录中
public static class StarterFactory
{
public static IStarter CreateStarter(string filename)
{
if (cache.ContainsKey(filename))
return cache[filename];
try
{
var asmName = AssemblyName.GetAssemblyName(filename);
var asm = Assembly.Load(asmName);
var type = asm.ExportedTypes.FirstOrDefault(t => typeof(IStarter).IsAssignableFrom(t));
if (type == null)
{
throw new Exception($"文件中没有找到实现IStart的实体类");
}
var s = (IStarter)Activator.CreateInstance(type);
if(s != null)
{
s.OnStop += S_OnStop;
cache[filename] = s;
}
return s;
}
catch (Exception ex)
{
OnError?.Invoke($"启动类加载文件出错:{filename}", ex);
return null;
}
}
public void LoadStarter()
{
var starterDir = "这里是starter目录的具体位置";
var di = new DirectoryInfo(starterDir);
var fiArr = di.GetFiles("*.dll");
foreach(var fi in fiArr)
{
try
{
var starter = CreateStarter(fi.FullName);
starter.Start();
}
catch(Exception ex)
{
cyb.Utility.Tool.LogHelper.Error("加载启动器失败"+fi.Name,ex);
}
}
}
}
上述只是一个简单的例子,通过接口实现的工厂模式,可以完全脱离的强引用,程序员要扩展这样的应用,只要实现IStarter就可以了。
另外,这样的技术,我在设计框架的时候,IOC也是使用了这个技术,代码就不写了,对关键处作一些说明。
往往设计框架的时候,需要考虑不同的数据库的接入,在sqlSugar这个ORM内置了几种数据库的驱动,其实我不太喜欢这样做,至于具体用哪种ORM,我也不一一说,自己喜欢就好了,都差不多。
我们定义数据访问层的接口为IBaseDAL
这样开发一个员工数据访问层 IEmployeeDAL:IBaseDAL
如果用过JAVA的成员都知道[Autoware]这个注入,我感觉挺好用的,就在我的框架也实现了这么个功能。
例如:
public class BLLEmployee
{
[Autoware] IDALEmployee dal;
}
在构造BLLEmployee的时候,通过寻找DAL目录下的库文件,然后寻找实现IDALEmployee的实例类就直接创建,当然也是用Assembly.Load()方法加载DLL,然后把导出的类一一对比就可以了。
通过上述两个例子,用工厂模式来实现扩展还是比较简单的。简单无所谓,关键是实用就好了。
为什么要独立封装出不同的DAL来访问不同的数据类型?例如SqlSugar支持MsSQL,PG,MySql等等关系型的数据库,如果换成Mongo呢?好像目前可选的orm还没有哪个很好同时支持关系型数据库和文档型数据库的,其实也不要偷懒,自己写一下也不难,再如果换成ElasticSearch呢?呵呵,谁知道呢,所以,建议不同的数据库类型,采用一组对应的DAL基类实现,但是都要实现接口IBaseDAL,可能不能都能兼容,那么设计和开发过程中,尽量保持一致的操作。
我在开发过程中,能用Dictionary解决的就不使用Switch或者if,记得学习编程的时候,老师也是这么说的,这么多年,也是这么做的,目的只有一个,减少程序的复杂度,增强代码的可阅读性,降低日后代码的维护难度。
业务扩展就有点复杂了,也不能说得太清楚,这里就提到一个概念:过度封装。在做ERP的时候,把常用的业务分为三类,单表,主从表,关联表,关联表其实并不需要再界面上体现,这里简单说说另外两种:
单表:新增(POST)、编辑(PUT)、删除(DELETE)、搜索(Get、GetList、GetPageList)
主从表:新增和编辑(SaveBill)、删除(Delete)、搜索(GetHdr,GetHdrPageList,GetDtl,GetDtlList)
框架建立了一个 public virtual class TokenApiControllerBase,这是一个API的基类,在用户具体开发的时候,肯呢个上述接口不一定够用或者和实际业务需求有一定出入,那么,可以用过继承和重载接口的方法,来定义一个新的API基类,满足业务扩展的需要,这些业务扩展是经过抽象的,不是指特定某一个业务,否则就失去框架的意义了,还是那句话,不能过度封装。
同理,业务层也需要这么样封装和扩展,数据层和业务并没有关联,也不应该有,所以,这部分的扩展,无非就是适应不同的数据库类型。建议业务层也设计业务层接口,例如:IBaseBLL
总之,不管是技术性的扩展还是业务性的扩展,都需要熟练掌握面向对象的知识和技巧,简单的方法就是多使用接口,尽量少使用强引用。
这里还未涉及到微服务,其实,文章未来也不会太过具体描述如何建立微服务,现在网上搜搜,有很多微服务的框架可以使用,从学习的角度来讲也足够了,这里已经把单个微服务的系统架构做了一个简单的说明
page-Controller-BLL-DAL-DB这样的层级,同级之间不允许创建调用,上级可以创建多个下级
例如 BLLEmployee 中可以定义IDALEmployee,IDALDepartment等不同的数据访问层。那么业务层之间通讯怎么办?其实也简单,可以定义一个MessageCenter,或者说InternalMessage
在一个业务层中发布 IMPublish
在接收的方法中打上标注接收就好了
[IMConsume(topic)]
public RValue
这样就很简单了,只要是匹配到topic的方法,都会被执行,一般横向通讯只会出现在BLL,如果DAL需要横向通讯,通过委托到BLL中转就好了,别想着简单方法简单来,这样会很乱的。同样,这样也能很好落实了“扩展性”的问题,例如一个销售单流程完成一个步骤,要向其他内部业务通知一下,当日后又新增加了一个业务,就不需要去修改发布那端的代码了,一下子就把系统结构简单化,减少了强引用的问题,至于怎么实现内部消息通知,很多技术框架都有设计,抄一个就好了,不过我是自己写的,把这个制作了一个IMStarter,来缓存IMConsume标注过的方法,以备调用需要,当然就消耗一点内存罢了。
最后一句,我认为扩展性的本质就是:解耦。