经过数个ERP/MIS项目的开发,列举常见的问题与错误,共勉励。
在项目的开始,一般都可以做到界面与逻辑的分离,界面只做数据绑定,业务逻辑则实现客户要求的计算。
WinForms中的BindingSource和Web中的ObjectDataSource,可以实现对object绑定到界面控件属性的功能。
这会带来极大的方便,降低程序的维护代价,做到低耦合。举例说明
采购单Sales Order的价格公式
Sales Order Price Amout=Item Price + Tax + Additional charge
采购单物料总金额=物料金额+ 税金+ 附加费用(运费,保险费…)
Tax 税金的计算,一般会根据物料的总金额,乘以百分比即可,是由系统自动运算的。
现在改需求了,客户不需要考虑税金,免税,这样公式即变化为
Sales Order Price Amout=Item Price + Additional charge
公式中少了一项内容。如果界面和逻辑严格分离,这里只需要改一下逻辑即可,不需要改动界面。当项目中发生这样的需求改动越来越多时,业务与界面分离的好处会大大降低维护的代价。
做ERP/MIS软件,需求不可能没有变化。即使是实施之后,修改起来比较麻烦,客户愿意出钱,也需要改动原来需求。
常见的需求更改:增加数据库字段,添加更多的明细表记录。增加数据库字段是经常遇到的,如果你的数据绑定代码是手写的,或是拼凑SQL语句,这回你可真要痛苦一下,加一个字段会令你重新检查所有的数据读写代码。另外,界面也要添加控件,绑定实体的属性。我的经验是,设计代码生成可靠,生成数据读写代码,当增加数据库字段后,只需要重新生成数据读写代码,非常轻松的维护代码。如果应用到ORM框架和数据绑定,增加字段只需要改一下界面即可,会更轻松。
用户的输入总是有各种问题的,要常常记得验证客户的输入。设计自定义的控件,NumbericInputBox,CurrencyBox专有来输入数值数据,货币数据,可以减少很多验证代码。其次,在保存数据时,要验证数据的主键是否重复,检查外键的字段是否已经存在,这些检查是必要,尽管SQL会帮忙你找出错误,但是,终究是你没有写验证的原因。
ERP/MIS开发,验证也是体现逻辑的一个地方。工作单过帐时要检查物料清单是否有效,工序是否已经确认,下销售单时,要判断客户的信用额是否超过预定的额度,大量的验证代码存在于ERP/MIS系统中,请添加足够的验证代码,而不是假设用户的输入或系统的判断是正确的。
在项目开发前中期,我们预定接口与实现一定要分离,像这样的
public interface IAccountManager { }
public interface AccountManager { }
调用方法
IAccountManager accountManager = ClientProxyFactory.CreateProxyInstance<IAccountManager >();
可是,当项目处于不停的维护和修改的状态时,我们常常会偷懒,写出这样的代码
IAccountManager accountManager =new AccountManager ();
这种偷懒也是要付出代价的,前一种标准的写法,可以实现WCF,.NET Remoting通信框架的互换,而不影响任何代码,CreateProxyInstance这个方法里面,可以做的事情太多了。前一种写法,可以实现cache缓存接口实现类,而后一种方法则无法做到,前一种方法,可以减少编译时的依赖,完全在运行时才解析出需要的类型。
我们开发的代码,没有对代码的执行环境做很全面的检测。比如,客户的电脑没有安装.NET 3.0/3.5,我们写的Linq代码就会出错,如果没有安装.NET 4.0,用到并行类的方法也会出错。如果客户安装的Crystal Report的版本与我们开发时引用的版本不一致,代码执行时,会发生FileNotFoundedException异常,Report Viewer控件也是如此。客户的机器上没有安装Reporting Services,而我们的代码也没有检查Reporting Services服务是否存在,就开始读取它的报表项来呈现报表。如果竞争对手想试用你的ERP/MIS系统,把你的软件装在虚拟机中,对于依靠检测系统日期来判断是否过期的验证方式,则在虚拟机中改日期后,你的对手可以有足够的时间来消化学习你的系统。
对此,要充分考虑软件的运行环境,做出判断以减少低级错误。
你的系统维护是否是这样的,客户说要改,你就马上开始改代码。老板说要改一个地方,你也毫不怨言的打开VS.NET,开始修改代码。这样是可以的,但不可取。客户要改,尽管他和老板的关系好,但你也要先记下来,当面沟通后,事后写个邮件给你的上司,说明有哪些任务要做,有哪些修改。这样做,一来是因为你的上司给你发工资,你的工作只对上司负责,不是对客户负责,二来是,记下来后,再书面确认一下,以防止客户抵赖或是沟通不充分。在不需要交修改维护费用的情况下,客户要改的东西实在是太多了,有时候可能客户因为心情不好,把你叫过去说半天,改这里改那里,等下班了回去之后,客户他自己都忘记了要改什么了。沟通不充分的问题,主要是怕你误解了客户的意思,或是说客户没有说明白,既然客户是上帝,上帝是没有错的,那就是你的理解错了。所以,事后用书面形式再确认一下,会是增加沟通效果有好办法。这个维护修改的书面表达形式,就是要说的Design Specifications。把客户要改的东西,放到标准的Design Specifications模板文件中去,打印出来,拿给客户签个名,他会很重视自己的说过的修改。我有曾见过厉害的客户,他明明知道这个修改有些不合理,但是工作完成后,等验收时,完全是按照他的意思做的,他也表示满意。这就叫做信誉,下次再找我改需求,我肯定会很乐意。
在我们的项目中,常常会引用一些第三方的控件,因为他们强大,好用。但是,我们还需要自定义一套控件,用来隔离这种第三方控件的变化。第三方的控件供应商,会常常更新它的API,属性和方法,如果项目中缺少这种隔离,则控件一变,我们的所有代码都需要改变。举例说明,第三方控件WebGrid,有一个属性DisplayLayout,用于显示界面元素,我们在ERP/MIS中大量应用到这个属性。如果有一天,WebGrid把这个属性DisplayLayout改名为Layout,这下惨了,我们要改的代码太多了。推荐一个好办法是,重新这个第三方控件,加入我们的自定义属性
public class Grid:WebGrid
{
pubic DisplayLayoutDisplayLayout;
}
当第三方的组件WebGrid把属性DisplayLayout改名为Layout,我们只需要这样做
public class Grid:WebGrid
{
pubic Layout DisplayLayout;
}
引用此控件的ERP/MIS代码则不需要任何变化,即可适应第三方软件的API的变化。
这是我们公司ERP维护的一个困境,有大量的客户会要求实施人员帮忙定制一些报表。随着时间的推移,ERP维护人员走了一批又一批,当初定制报表的存储过程,修改的目的,文档也丢失了,很难从几百上千行的代码中,找到合适的理由,为什么要这样修改。工作多年后,虽然习惯了看没有文档的代码,习惯看没有说明和注释的代码,只好硬着头皮帮忙客户找SQL,找报表的存储过程,一点点的修复bug。ERP/MIS中,与数据有关的bug会占有很大的比例,就因为数据的不同,程序无法作出适当的处理,出错。虽然现在已经用建立Sharepoint建立了以客户为目录的共享文件夹,但要维护好客户的需求记录,还真不容易做到。
我一直都很佩服Oracle的市场宣传做的很好,从开发人员到工程师,从老板到销售助理,一提到数据库,都会提到Oracle。有时候销售助理,会大肆宣扬Oracle的好处,甚至比开发人员还懂得多。也有的客户,指定只用Oracle,拒绝用其它的数据库,没有任何理由。所以,如果是做一个长期的产品,还是要考虑将来数据库移植的问题。数据访问,要用通用的DbConnection,DbCommand,DbDataAdapter,或是用ORM;少用存储过程,多用程序代码来实现逻辑。在系统开发时,开发人员常常只是理论上实现了多种数据库的支持,真要到真实的多种数据库平台中跑一下的时候,问题会接二连三的来了。以SQL语句,选择前十行数据为例子
MySQL 和PostgreSQL : SELECT * FROM emp limit 10
Oracle : SELECT * FROM emp WHERE rownum<=10
SQL Server : SELECT TOP 10 * FROM emp
我的建议是,如果要支持多种数据库,尽早做规划,尽早建立真实的数据库环境测试。
以举例为证,窗体类一般会在后面加个frm来表是是窗体类,如SalesOrderfrm,所有显示报表的窗体,后面加rpt,如SalesOrderrpt。 这里有个小技巧,为什么不是rptSalesOrder,而是SalesOrderrpt。这是为了在IDE的Solution Explorer视图中,以方便的看清同一组的窗体而以此命名的。如果我再加一个报表SalesOrderQuotationrpt,这样,在Solution Explorer视图中,这两个报表窗体是显示在一起的,方便查看。
对于控件,请看下面的命名
private EPN.WinUI.Editors.TextEditor txtRevisedBy;
private EPN.WinUI.Misc.Label lblLastRevised;
private EPN.WinUI.Editors.DateTimeEditor dteRevisedDate;
private EPN.WinUI.Misc.Button btnPrint;
txt表示TextEditor,btn表示Button,lbl表示Label.
日期格式,有的地方是用yyyy-MM-dd,但是有的地方是yyyy/MM/dd,没有统一。
价格Price字段的长度,是decimal(12,4), Amout合计字段的长度是decimal(16,4)
对于这些规范,要始终的坚持。如果可能,可以开发一个Code Analysis规则,来自动分析程序集。