我对大型系统的理解,从数量上面来讲,源代码超过百万行以上,系统有超过300个以上的功能,从质量上来讲系统应该具备良好的可扩展性和可维护性,系统中的功能紧密关联。除去业务上的复杂性,如何设计这样的一个协作良好的系统,搭建开发人员基础平台,一直是我研究的方向。
SouceCounter(版本3.3.91.79)对源代码的统计信息如下:
下面来详细解析一下这个系统的设计架构,纯.NET技术架构方案,C/S WinForms系统。
系统分为Framework和Application两个部分,前者是框架(Framework),包含核心的基础功能,如公共类库,许可授权,数据字典,公共控件,公共窗体,多线程组件,通信基础,会话管理等基础部分,后者是应用部分,根据业务逻辑的不同,对于ERP系统而言,可分为进销存(Distribution),工程(Engineering),生产(Production),生产计划(Production Planning),财务(Account Receivable,Account Payable,General Ledger),客户关系(CRM)等模块。
public const string Major = "3"; public const string Minor = "4"; public const string Build = "0"; public const string Revision = "0";
public enum LicenseType { Internal, Standard, Enterprise }
设计精良的系统应该先预定义好一系列的基础界面,用于管理框架功能中的元数据,这一节分两个部分讲解,一是框架要定义什么,二是如何去实现这些功能。先来看一下框架数据库Framework有哪些基础的元数据表定义:
先写一个基础查询语句,用于查询框架系统数据库的所有表。
SELECT * FROM sys.tables ORDER BY name
数据表 | 定义 |
Attachment | 系统中所有功能的附件,可直接存储文件或是存放一个FTP文件路径 |
Branch / BranchDetail | 实现多库存组织 |
Company | 实现多公司帐套 |
Configuration | 系统参数配置 |
ContextFunction ContextFunctionDetail | 系统上下文菜单项定义 |
DictionaryDictionaryDetail | 可变的数据字典定义,用于可修改的数据字典 |
FormLayoutFormLayoutDetail | 自窗义界面(Form Designer设计之后保存的Xml文件) |
FormProfile | 用户偏号(网格排序,控件位置或大小拖动) |
LanguageTranslation | 多国语言翻译 |
Component | 系统启动时读取的程序集 |
Lookup | 查找数据对话框 |
MessageMessageDetail | 消息盒子 |
Report | 报表参数定义 |
ScheduledTask | 系统预置的计划任务 |
SystemModuleSystemFunction | 系统包含的模块与功能 |
User | 系统用户 |
UserDefinedQuery | 预定义查询 |
UserGroup | 用户组别 |
UserGroupAuthorization | 用户组别授权 |
UserGroupMenu | 用户组别菜单 |
UserGroupMenuBitmap | 用户组别导航图 |
UserLog | 用户日志 |
Workflow | 工作流定义 |
通过上面框架数据库表的定义即可看到框架的基础功能,也就是对以上数据进行读写。我按照窗体的类别简单介绍。
工作流实现的四大基础功能:通知提醒,批核,计划任务,调用自定义代码。
通知提醒:ERP系统中包含大量的提醒功能,每加一个业务功能就写一遍提醒功能的代码调用显得有些繁琐,在此只做一个设置即可实现通知设置,包括要通知的人员,通知的内容。
批核:框架应该抽象出所有单据的批核需求,建立一个独立的批核系统,任何功能只需要简单设置一下即可调用工作流代码,实现批核流程。
计划任务: 系统中有一些定期执行的任务,比如员工生日提醒,库存余额报警,待收货记录提醒,老板报表定时发送。
调用自定义代码:如果系统中的功能存在缺陷defect,软件公司不愿意修改的情况下,可以考虑增加自定义的.NET代码解决问题。只需要在合理的事件点上插入合适代码,完成重复的数据修复工作。
基于微软工作流的解决方案,先看一下包含的基础组件:
Activities 活动库,活动是工作流定义中一个基本的代码执行单元,可固化执行的代码均可封装到一个活动中。包含文档批核活动,发送报表活动,查询活动,调用.NET代码活动等。调用.NET代码活动的设定方法参考如下:
assembly=Microsoft.Applications.MyReportDll; class=Microsoft.Applications.MyReportDll.EmployeelistingDAL; method=GetEmployeeListing();
熟悉.NET框架反射调用方法的朋友一看就明白上面定义的含义,这个活动的源代码可以简化如下所示:
protected override ActivityExecutionStatus Execute(ActivityExecutionContext executionContext) { assembly = Assembly.Load(segments[0]); type = assembly.GetType(segments[1]); method = segments[2]; type.GetMethod(method, BindingFlags.Public | BindingFlags.Static).Invoke(null, null); }
Contracts 接口与实现 在第一步活动定义中,大量的调用了WF中的CallExternalMethod活动,这个活动用于调用外部自定义方法,实现接口与实现的分离。在设计活动时只需要指定要调用的接口和方法,具体的实现可根据需要变化。
.NET WF要求在启动WF Runtime时,需要先注册要执行的服务,代码参考如下:
SqlWorkflowPersistenceService persistenceService=new SqlWorkflowPersistenceService (ConnectionString); runtime.AddService(persistenceService);
在活动的代码中,可通过以下的方式引用经过注册的服务:
SqlWorkflowPersistenceService persistenceService=context.GetService<SqlWorkflowPersistenceService>(); //调用 persistenceService服务的方法
这样,服务中可调用ERP代码的接口,实现了ERP逻辑与.NET WF工作流的整合。
Workflows 给每种常见的流程定义一个工作流类型,方便做持久化和验证工作。
Workflow Designer Control re-host工作流定义组件,直接参考借用MSDN中的例子。
Monitor 工作流监控,查看流程的执行情况,当前执行结点,执行路径。
从功能上来讲,系统应该具备以下四个基础服务器,实现数据读写分离。
Application Server 业务逻辑服务器,.NET Remoting服务器端。
Report Server 报表服务器,用于报表呈现,减低Application Server的压力。
Workflow Server 工作流服务器 执行工作流。
Scheduling Server 计划任务服务器,减低Application Server的压力。
每种服务器都配置Console版和Service版,代码完全一样,Console以控制台程序呈现,Service以Windows 服务应用形式实现,前者方便开发,后者用于部署和实际使用。
根据ERP项目的功能分类,分为进销存(Distribution),工程(Engineering),生产(Production),生产计划(Production Planning),财务(Account Receivable,Account Payable,General Ledger),客户关系(CRM)等模块。每个模块独立为一个Visual Studio Project,编译成一个程序集。通过插件式结构,实现使用时只需要在Component表中插入一行记录,即可让系统识别到此程序集,运用反射方法调用程序集中的功能。
如果是用LLBL Gen Pro开发系统,则业务实体层具备以下文件夹层次结构。
以销售单表头为例子,数据库表SalesOrder,生成实体为SalesOrderEntity,设计读写接口文件为ISalesOrderManager, 接口的实现类为SalesOrderManager,用简单的图表示如下:
SalesOrder –> SalesOrderEntity –> ISalesOrderManager –> SalesOrderManager
系统强制执行以上约定,并且设计了Code Smith模板代码生成来减少出错的可能。既提供强制性约束,又提供工具辅助开发人员遵守约定,系统开发效率成倍提升。
业务实体层还实现了数据审计(Audit)功能,记录表的每个记录的修改值。刚毕业参加工作时,常常混淆Audit和Approval的区别,现在一些系统还存在用Audit作为批核的意义。
定义系统中不可变的数据字典,虽然用代码写死字典的方法值得商议,但它的好处也是非常明显的。
定义一个劳动合同的枚举,分固定期限和无固定期限的合同,参考下面的代码。
public enum ContractType { [StringValue("F")] [DisplayText("Fixed Time")] FixedTime, [StringValue("U")] [DisplayText("Unlimited Time")] UnlimitedTime }
获取它的值用如下方法,值用于存储到数据库中或程序代码使用:
StringEnum<ContractType>.GetStringValue(ContractType.FixedTime)
获取它的描述用如下方法,描述用于界面中呈现:
StringEnum<ContractType>.GetDisplayText(ContractType.FixedTime)
一部分逻辑在Business Logic中实现,比如类型初始化值,自动带值,值验证等逻辑。复杂的逻辑比如进出仓,涉及到的关联表会多一些,可能要扣减库存,修改物料库存余额,批号生成,生成出仓平均单价等,复杂的逻辑要单独放在一个项目中设计源代,这样方便维护。
从形式上来分,分为图形报表和数据报表。图形报表用微软.NET 自带的图形控件完成,MSDN上包含Samples Environments for Microsoft Chart Controls的大量例子。数据报表用水晶报表完成,600多个水晶报表文件,涵盖单据的打印,列表查询打印,主档数据打印等类别。丰富的水晶报表功能为系统增色不少。
集成一些常用的功能,不需要进入系统即可完成系统维护。比如数据库升级,数据库备份,数据库还原,新帐套创建,系统参数设定,数据库性能优化(主要是索引重建),这些实用工具程序减轻了系统管理员的负担。
Distribution 进销存,包含销售,采购,仓库模块。
模块 | 功能 |
Sales 销售 | 销售合同,销售订单,送货,退货,销售包装,销售发票 |
Purchasing 采购 | 采购申请,采购订单,采购收货,采购退货,供应商发票 |
Inventory 仓库 | 进仓(Receipt),出仓(Issue),转仓(Transfer),盘点(Cycle-count) |
Engineering 工程 物料清单,标准成本设定。
Production 生产 工作单,工作单发料,倒冲与组装,物料退回,发散料。
Production Planning 生产计划 主生产计划MPS,物料需求计划MRP, 能力需求计划CRP
Finance 财务 Account Receivable应收,Account Payable应付,General Ledger总帐。
CRM 客户关系 销售线索,销售日报,销售月报,出差申请与费用报销。
在我的从业经历以来,我认为搭建一套开发框架对企业开发是很有用处的。前期所花费的精力和时间在后期都会得到充分的回报。然而搭建框架所花费的时间和精力,值得商榷。公司一直都是对股东负责,能用最少的时间做完项目,收回合同款即可,大费周折的去做产品基础功能,对于小公司而言生存都是问题,精心设计的框架需要大量的精力去维护和改善,抛开公司因素,学习一个大型系统对个人的职业发展和成长也是相当重要的。