总结一下做管理软件,有哪些项是经过检验的条款,必须遵守的。
1 要保存用户的偏号(profile/favourite)。 ASP.NET 2.0引入此功能,当用户修改默认的控件的属性时,框架应当保存用户的修改。显而易懂的例子是grid控件中的列顺序。用户修改之后,关闭窗体时,要可以保存起来,当用户再次打开时,应当加载用户上次的修改。
2 界面中的数据要有通一的导出方案。导出类型一般是Mircrosoft Excel, Microsoft Access, Text,CSV。Excel为第一需要考虑的导出格式。
3 半角全角输入转化。对于全角输入,应当转化为半角。全角半角长度不一样,但在界面中它的字符是一样的,应当默认的将用户全角的输入转化为半角。或是直接提示用户切换输入法,不允许全角输入。
4 使用标准的流行的报表开发技术和设计工具。自定义报表设计器很难控制稳定性,fyireport就是这样,即使有新功能也很难加入。再次,它的学习成本也比较高。推荐直接选择Crystal Report或是Reporting Services。
Crystal Report一直都没有大的改动,Reporting Services因为SQL Server昂贵的授权费用,因而产生了一些优秀的报表工具。但我仍然推荐用成熟的Crystal Report技术。
5 做好系统的三个门面窗体: Login, Splash,About。
登陆窗口的背景色要与主窗体一致,给用户的第一感觉的地方,要以稳重为主。我选择以微软网站的蓝色基调为主色。
Splash显示当前的程序的版本,授权用户信息。About对话框中显示license信息,以及客户服务联系方式。
6 界面中调用一项系统服务时,应该先检查它是否存在。比如即将显示报表界面,应当先检测报表服务是否存在,进行环境检测。运行事务时,应该先检测MSDTC服务是否已经启动。以.NET 4.5 为Target生成程序版本时,需要先检测.NET 4.5是否已经安装。
7 保持与服务器的连接,断线后要禁用用户输入。这需要引入心跳机制。
1 给每个日记帐表添加额外的五个字段。用来保存这笔记录的创建人,创建时间,最后修改时间,修改日期:
Created_By, Created_Date, Revised_By, Revised_Date
另一个字段是为维护数据(data fix)时,方便使用:添加一个自增列,identity(1,1) ,设置为每一个表的第一列。名字可以是RECNUM(record number), LineNO(line number) 等等。
2 统一的数据位数方案。比如
行号类: LineNo/EntryNo/RecordNo/Recnum 不显示小数点,
数量类:Qty/Quantity 6位小数
金额类:Amt/Amount 4位小数
3 对于通用的数据表,比如物料主档,客户主档,供应商主档,应该保守的留20个备用字段,以供客户填写自定义的信息。再完善的数据库字段,都有可能考虑不周到的地方。20个备用字段,应该可以满足大部分需求。
4 修改数据库排序规则,一般默认为USA标准的SQL_LATING_CP1_CI_AS。这样可以减少存储过程出错的机率。
另外,在安装SQL Server时,也要选取这个排序规则。
1 用户登陆表中的密码,不能以明文保存。可以用盐或是MD5加密,或是可逆的加密,或是字符串混淆(比如给每个字符都加一定规则的字符串,到检测密码时,再反过来还原密码)。
2 提供几种方式的用户验证机制。认证类型:PASSWORD, DOMAIN。
传统的密码验证需要输入密码,如果改成域DOMAIN验证,不用输密码,根据当前用户直接登陆系统。
3 完善的license许可授权机制。虚拟机检测,硬件检测,过期时间检测,功能限制检测,试用版过期检测。
4 混淆.NET程序集,增加反编译难度。
1 提供通用的跟踪机制解决方案。比如应有Debug.WriteLine输出跟踪信息,再截获这个输出,显示到log viewer程序中。或是应用UDP端口发送跟踪信息,再捕获显示到界面中。下面的代码可做参考
Trace.Listeners.Add(new TextWriterTraceListener("TextWriterOutput.log", "myListener")); Trace.TraceInformation("Test message."); // You must close or flush the trace to empty the output buffer. Trace.Flush();
2 提供通用的附件管理功能。附件可以上传到数据库中,也可以直接保存一个路径引用。同时,需要写一个附件浏览器,可查看所有的带附件的功能的内容。
3 界面中Tab键的顺序要合理,遵守从上到下,从左到右的顺序。还可以做到Enter转成Tab,回车间转成Tab键。
4 源代码中,数字类型的值的格式要统一。0x开头的16进制,默认的是10进制。这两个格式应当统一。可以使用Windows 7的计算器功能实现快速修改一个16时制数为10时制数。
5 提供标准的数据操作功能。
数据存档(Archieve):可以把数据导出为EXCEL或是其它的格式
数据清理(Cleanup):可以清除数据表
数据再开始(Restart):只清除日记帐数据,而保留系统设置和主档数据
数据导入(Load): 从备份文件中加载数据,相当于导入数据。
6 界面上长时间的操作,要转成后台线程。界面中可以有BackgroundWorker,代码逻辑中,应该调用System.Threading.Thread的后台线程来计算。与此同时,界面中的光标也需要改变
this.Cursor=Cursors.WaitCursor; ...... long operation this.Cursor=Cursors.Default;
7 经常留意代码效率改善方法,并把它应用到系统中。举例如下
1) 不要用string ax=”” 判断ax是否为空, ax.Length==0的效率高于ax==string.Emptyu 一般用as.IsNullorEmpty
2) 对于不改变的变量,用常量代替
3) 用Linq代替大量的foreach查询
4) MyType t=(MyType) t和t as MyType的效率比较
5) List<T>代替Array,Haset<T> ,Dictionary<K,V> 代替Hashtable
今天是个开心的日子,又是周末,可以安心轻松的写写文章了。经过了大概3年的DDD理论积累,以及去年年初的第一个版本的event sourcing框架的开发以及项目实践经验,再通过今年上半年利用业余时间的设计与开发,我的enode框架终于可以和大家见面了。
自从Eric Evan提出DDD领域驱动设计以来已经过了很多年了,现在已经有很多人在学习或实践DDD。但是我发现目前能够支持DDD开发的框架还不多,至少在国内还不多。据我所知道的java和.net平台,国外比较有名的有:基于java平台的是axon framework,该框架很活跃,作者也很勤奋,该框架已经在一些实际商业项目中使用了,算比较成功;基于.net平台的是ncqrs,该框架早起比较活跃,但现在没有发展了,因为几乎没人在维护,让人很失望;国内有:banq的jdon framework可以支持DDD+CQRS+EventSourcing的开发,但是它是基于java平台的,所以对于.net平台的人,没什么实际用处;.net平台,开源的主要就是园子里的晴阳兄开发的apworks框架。晴阳兄在DDD方面,在国内的贡献很大,写了很多DDD系列的文章,框架和案例并行,很不错。当然,我所关注的紧紧是c#和java语言的框架,基于scala等其他语言实现的框架也有很多,这里就不一一例举了。
上面这么多框架都有各自的特点和优势,这里就不多做评价了,大家有兴趣的自己去看看吧。我重点想介绍的是我的enode框架,框架的特色,以及使用的前提条件。
上面的架构图是enode框架的内部实现架构。当然,上面这个架构图并不是完整的CQRS架构图,而是CQRS架构图中command端的实现架构。完整的CQRS架构图一般如下:
从上图我们可以看到,传统的CQRS架构图,一般画的都比大范围,command端具体如何实现,实现方案有很多种。而enode框架,只是其中一种实现。
基于整个enode框架的架构图以及上面的文字描述说明,我们在看一下上面最开始框架简介中提到的框架所使用的关键技术。
public void Initialize() { var connectionString = "mongodb://localhost/EventDB"; var eventCollection = "Event"; var eventPublishInfoCollection = "EventPublishInfo"; var eventHandleInfoCollection = "EventHandleInfo"; var assemblies = new Assembly[] { Assembly.GetExecutingAssembly() }; Configuration .Create() .UseTinyObjectContainer() .UseLog4Net("log4net.config") .UseDefaultCommandHandlerProvider(assemblies) .UseDefaultAggregateRootTypeProvider(assemblies) .UseDefaultAggregateRootInternalHandlerProvider(assemblies) .UseDefaultEventHandlerProvider(assemblies) //使用MongoDB来支持持久化 .UseDefaultEventCollectionNameProvider(eventCollection) .UseDefaultQueueCollectionNameProvider() .UseMongoMessageStore(connectionString) .UseMongoEventStore(connectionString) .UseMongoEventPublishInfoStore(connectionString, eventPublishInfoCollection) .UseMongoEventHandleInfoStore(connectionString, eventHandleInfoCollection) .UseAllDefaultProcessors( new string[] { "CommandQueue" }, "RetryCommandQueue", new string[] { "EventQueue" }) .Start(); }
[Serializable] public class ChangeNoteTitle : Command { public Guid NoteId { get; set; } public string Title { get; set; } }
var commandService = ObjectContainer.Resolve<ICommandService>(); commandService.Send(new ChangeNoteTitle { NoteId = noteId, Title = "Modified Note" });
public class ChangeNoteTitleCommandHandler : ICommandHandler<ChangeNoteTitle> { public void Handle(ICommandContext context, ChangeNoteTitle command) { context.Get<Note>(command.NoteId).ChangeTitle(command.Title); } }
[Serializable] public class Note : AggregateRoot<Guid>, IEventHandler<NoteCreated>, IEventHandler<NoteTitleChanged> { public string Title { get; private set; } public DateTime CreatedTime { get; private set; } public DateTime UpdatedTime { get; private set; } public Note() : base() { } public Note(Guid id, string title) : base(id) { var currentTime = DateTime.Now; RaiseEvent(new NoteCreated(Id, title, currentTime, currentTime)); } public void ChangeTitle(string title) { RaiseEvent(new NoteTitleChanged(Id, title, DateTime.Now)); } void IEventHandler<NoteCreated>.Handle(NoteCreated evnt) { Title = evnt.Title; CreatedTime = evnt.CreatedTime; UpdatedTime = evnt.UpdatedTime; } void IEventHandler<NoteTitleChanged>.Handle(NoteTitleChanged evnt) { Title = evnt.Title; UpdatedTime = evnt.UpdatedTime; } }
[Serializable] public class NoteTitleChanged : Event { public Guid NoteId { get; private set; } public string Title { get; private set; } public DateTime UpdatedTime { get; private set; } public NoteTitleChanged(Guid noteId, string title, DateTime updatedTime) { NoteId = noteId; Title = title; UpdatedTime = updatedTime; } }
public class NoteEventHandler : IEventHandler<NoteCreated>, IEventHandler<NoteTitleChanged> { public void Handle(NoteCreated evnt) { Console.WriteLine(string.Format("Note created, title:{0}", evnt.Title)); } public void Handle(NoteTitleChanged evnt) { Console.WriteLine(string.Format("Note title changed, title:{0}", evnt.Title)); } }
目前暂时想到以上8个我觉得比较重要的问题,我会在接下来的文章中,一一讨论这些问题的解决思路。我觉得写这种介绍框架的文章,一方面要介绍框架本身,更重要的是要告诉别人你设计以及实现框架时遇到的问题以及解决思路。要把这个分析和解决的思路写出来,这才是对读者意义最大的;