工作多年后才明白的.NET底层开发技术

参加工作已经有五年了。由于是非计算机科系,一开始的工作是做网页,维护ASP脚本程序,有时候也要做图片,总而言之,一项任务交到手里,没有借口说不会做,想办法弥补然后把任务做完。后来才得到机会,参与编程开发工作。而且,当时一心想用C#.NET做开发,不希望参与DELPHI的开发,偏偏工作经过层层的转发,交到手里的就是DELPHI开发。经历努力,再后来就真的用.NET进行开发工作,一直持续到现在。消极的观念会说,学的很杂,做的也杂,积极一点的想法,把老板交给你的任务完成,就是称职的员工。现在正值毕业生找工作的时间,一时找不到开发的工作,也不要急,先听从公司的安排做个软件测试,或是文档管理之类的职位,等有空缺的开发人员的机会,你再毛遂自荐,同样可以进入程序员这个领域。机会是留给有准备的人的,难道不是吗?

一直用.NET做ERP/MIS类型的软件,学业不精,可以完成工作任务,业余时间也制作一些集成化的工具包,下面分享一下我认为的.NET领域里的底层技术。

1 通信技术(Remoting,WCF, ASMX)

通常的三层架构,数据库,数据访问层,界面层。通讯技术定义了三层组件之间的调用约定,以及方法。这样说起来有些抽象,举例为证。现在要制作一个进销存项目,基本的功能是进仓,出仓,转仓,查询功能要能查询到仓库进出日记帐,
库存余额。对于一笔物料为FLEX0901的进仓业务,用ORM的方式完成,伪代码如下
InventoryMovmentEntity  receipt=new InventoryMovmentEntity(“REF1108080001”,"RECEIPT”);
InventoryMovmentDetailEntity  detail=receipt.InventoryMovmentDetails.AddNew(); 
detail.ItemNo=”FLEX0901”;
detail.MoveDate=DateTime.Now;
InventoryMovementDAL.Instance.Save(receipt);

这里有几个问题处理的不错,比如
1) ORM的数据读写,关注面从拼凑SQL读写到构建实体,这是进步。如果用DAL+SQL Script的方式完成,伪码如下
string mySql=”INSERT ICMOVH(RefNo,Direction)   VALUES(‘REF1108080001’, 'RECEIPT’) ";
InventoryMovementDAL.Instance.ExecuteNonQuery(mySql);
mySql=”INSERT ICMOVD(ItemNo,MoveDate) VALUES(‘FLEX0901’,'2011/8/8’) ";
InventoryMovementDAL.Instance.ExecuteNonQuery(mySql);
这里省略了拼凑参数值的句段,直接把值放到SQL语句中去。
后一种方式,明显的缺陷时,当添加新的字段,对系统扩展时,修改和维护起来的成本,明显大于前者。

2) 使用了单件模式,InventoryMovementDAL.Instance来统一操作数据读写,而不是这样
InventoryMovementDAL  inventoryDAL=new InventoryMovementDAL();
inventoryDAL.Save(receipt);

从这个单件模式,引出了通讯技术的必要性。当有很多个用户,同时操作进仓功能,意味着同时有很多个InventoryMovementDAL的数据写入操作,这时产生的问题
1)不好控制前后两张单据的进仓单参考编号重复。为保证不重复,在保存之前,我们需要到数据库检查一次是否已经存在该参考编号的进仓单,也就是InventoryMovementDAL的Save方法的开头,
要包含这样一段代码,以DAL+SQL Script的方式表示
string mySql=” SELECT COUNT(1) FROM ICMOVH WHERE RefNo=’REF1108080001’  ";
bool existing=InventoryMovementDAL.Instance.ExecuteScalar(mySql)>0;
可以想像,当并发用户为100时,每一笔进仓业务,需要预先一次数据检查,来回于数据库,这样的程序性能肯定不好。
2 )对于库存报表,多个并发的InventoryMovementDAL会导致数据前后不统一。库存余额报表现在可以读到物料FLEX0901还有100个pc,一会当有进仓单入库200pc的FLEX0901时,如果不手动刷新数据,此时的报表仍然显示的物料FLEX0901的库存余额是100,而不是300. 这有时候是不可接受的结果。
可以做一个timer,定期刷新库存余额。这里的问题是,InventoryMovementDAL只有一个实例,无法自己告诉自己,已经有新的库存了,重新读取数据。这里需要一种并发机制,告诉InventoryMovementDAL实例,有新的数据加入到库存余额中,需要刷新报表。

3)有一些数据项,是全局的,对于整个系统都是唯一的,需要特殊处理。
比如ERP系统允许的并发用户,同时允许有10个用户连接进入系统,超过则无法处理;
ERP系统的一个用户把当前系统的默认货币从HK$改进了US$,其它的用户,要能知道这个改变,此时,不太可能让当前正在操作业务的用户退出,重新进入系统;
当发生网络故障时,ERP系统要能知道网络故障,并suspend当前正在进行的操作;这一点可以理解为一条有效的改善,当无法连接到SQL Server时,要阻止当前正在录入数据的用户,否则他很辛苦的录入了数据,而系统又无法保存,给出一个提示A network-related or instance-specific error occurred while establishing a connection to SQL Server. The server was not found or was not accessible. Verify that the instance name is correct and that SQL Server is configured to allow remote connections  用户会抱怨你的软件没有做好,尽管这不是你的错。
如下图,当前用户正在编辑客户资料,发生网络故障,界面被Disable了。借助于通讯技术中的并发,可以实现。
image 
通篇都没有给Remoting,WCF打广告,说Remoting,WCF的好处。当你的程序遇到这几个问题时,是否可以考虑下通讯技术,来改善性能和客户体验。

 

2  反射,动态编译

反射是动态获取程序集的元数据的一种技术,这句话是做.NET程序员面试题目的一个的答案,你可选择记住它,就好比高中生物学里面讲到的细胞的结构的课程时,细胞由细胞膜,细胞质和细胞核组成。根据做程序的经验,Never ask why不是好习惯,即使是微软的API,有时候违反了调用约定,也会很抓狂。

请看下面这一段代码
Assembly assembly = Assembly.GetExecutingAssembly();

object entryForm = Activator.CreateInstance(formBaseType) as Form;

entryForm .MdiParent = this;
entryForm .Show();
entryForm .Activate();
代码的含义比较简单,从当前程序集中创建formBaseType类型,并调用它的方法。从方法名上来看,大概可以看出,这是一段MDI创建child子窗体,并显示子窗体的代码。
这段小代码,也是插件式框架的基本思路,请参考《Management Console 工具管理类软件通用开发框架(开放源码)》中的例子来体会它的用处。
在ERP/MIS系统中,应用反射的例子,实在是太多了。数据访问接口InventoryMovementDAL,借助于反射,来查找并调用它的实现类; 窗体也它的子窗体借助于反射来获取属性,传递值;ERP系统的整个框架,也是借助于反射搭建起来.

看下面的图,ERP的三个模块Paradox.ERP.SystemAdministration, Paradox.ERP.Engineering,Paradox.ERP.Inventory被Paradox.Framework.Kernal反射调用,如果再写一个Paradox.ERP.Sales的销售模块,几乎不需要改动,就可以让它被框架调用。

image

动态编译的例子,是应用到一个工资系统中. 请看图
image

对于如何解析工资的formular公式,有若干种办法,这里使有的是动态编译的方法。把每一个工资项看成是一个类型class的属性,Formular的内容则放到一个方法中去,当成表达式计算求职,最后应用反射,返回各属性的值即可。
public class FormularCalculation
    {
        public static object Build(string[] items, string formular)
        {
            string nameSpace = "A";
            string className = "FormularCalculation";
            string methodName = "Run";
            CSharpCodeProvider compiler = new CSharpCodeProvider();
            CompilerParameters paras = new CompilerParameters();
            paras.GenerateExecutable = false;
            paras.GenerateInMemory = true;
           
           StringBuilder classSrc = new StringBuilder();
            classSrc.Append(" using  System;"+Environment.NewLine);
            classSrc.Append(" namespace "+nameSpace+" { " + Environment.NewLine);
            classSrc.Append("public  class  " + className + "{ " + Environment.NewLine);
            foreach (string item in items)
            {
                classSrc.Append("public decimal " + item + ";" + Environment.NewLine);
            }
            classSrc.Append("public  void Run() { 基本工资=5000; " + Environment.NewLine);
             string [] format= Regex.Split(formular,Environment.NewLine);
             foreach (string prop in format)
             {
                 classSrc.Append(prop +" ;"+ Environment.NewLine);
             }
            classSrc.Append("}"+Environment.NewLine);
            classSrc.Append("}" + Environment.NewLine);
            classSrc.Append("}" + Environment.NewLine);
            string source = classSrc.ToString();

            CompilerResults result = compiler.CompileAssemblyFromSource(paras, source);
            CompilerErrorCollection error= result.Errors;
            Assembly assembly = result.CompiledAssembly;
            object eval = assembly.CreateInstance(nameSpace+"."+className);
            MethodInfo method = eval.GetType().GetMethod(methodName);
            object reobj = method.Invoke(eval, null);
            return eval;
        }
    }
调用方法如下
string[] items = { "应发合计","基本工资","奖金","福利费", "扣款合计","社保","税","实发合计","应税所得额"};
string formular=@ ”    应发合计=基本工资+ 奖金 + 福利费 - 扣款合计;
扣款合计=社保 + 税+ 应税所得额;
实发合计=应发合计-  扣款合计; ";
object obj = FormularCalculation.Build(items, formular);
            Type type = obj.GetType();
            foreach (PropertyInfo fi in type.GetProperties(BindingFlags.NonPublic | BindingFlags.Public
                                               | BindingFlags.Instance | BindingFlags.DeclaredOnly))
            {
                string value = fi.Name;  //取到计算后的各个属性的值
            }
动态编译还应用于软件的加密,在内存中产生加密程序的源代码,动态编译并运行,检测是否符合license授权。

3  ORM(NHibernate,LLBL Gen) 对象关系映射

虽然可以找出很多理由来拒绝ORM,比如performance不好,接口不好用,没有NHibernate设计器,是的,这都是理由。但是,一旦接触过ORM之后,我发现做系统再也离不开这个工具。相对于ERP/MIS类的系统,大部分时间都是和SQL的读写在搏斗,ORM带给你的几个好处,是不可忽视的。
1) 增加或删除数据库字段,界面和程序几乎不需要改动。这一点我非常认同,即使是很稳定的系统,也避免不了要做customization,也要加些字段,如果用SQL拼凑,你几乎要改动所有的相关内容,而且还无法获取编译器的语法检查的好处,ORM在编译期间就可以检测出一些类型不匹配的问题。
2 )界面和逻辑的真正分离 改动计算逻辑,不需要改动界面,也就是实现MVC,MVP的模式,其实我们可以不用管这两个模式,我们只是在用ORM来读写数据库。
3 )代码更幽雅,调试起来更容易,维护方便。

Linq技术之后,微软大力发展Entity Framework,不推荐在项目中使用。MS的优点是,它发现一项技术很有用,或为开发一个很得力的工具,最终它会做的很好,比如Visual Studio,Office,但是这是需要时间的,它要不停的学习,观察,改善,特别像API这类的东西,如果更新太快,会对项目产生较大的风险,实际项目中最需要的是稳定的API。MS打算要放弃的东西,它会慢慢减少资源,慢慢减少关注的次数,时间一长,最后就淡出了开发人员的视线。
NHibernate经过多年的发展,稳定,好用,有庞大的Java社区(Hibernate)的支持,不愁遇到问题没有答案。

 

4  工作流 Workflow

目前微软推出了两个版本的工作流,.NET 3.5和.NET 4.0的,应该把它看成两个产品,并不是简单的版本升级。

ERP/MIS领域常见的需求有
1)采购单审批(如果条件)  要求:

当金额大于等于500且采购员为A时,需要通过May的审批

当金额大于等于500且采购员为B时,需要通过Jack的审批

当金额小于500时,不需要通过审批,可直接过帐
2 ) 当发生工程更改ECN时,要通知生产部重排计划,通知货仓安排发料。
像这种类型的需求,每个企业的要求都不一样,要达到定制的目的,又不想为每个不同的客户分别写代码,非用到工作流不可。你可以不选择用工作流,那就为不同的客户定制代码,也行。
MS的工作流也做成了中间件,你需要尽可能的override
image
请通过查看《信息化基础建设 工作流开发》,了解更多关于工作流的心得体会。

posted @ 2011-08-09 09:24  信息化建设  阅读(27049)  评论(63编辑  收藏  举报