开发ERP软件应该遵守的22条规则

开发ERP软件应该遵守的22条规则

总结一下做管理软件,有哪些项是经过检验的条款,必须遵守的。

界面篇

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

 



 

 
 
分类:  .NET Solution

分享一个基于DDD以及事件驱动架构(EDA)的应用开发框架enode

前言

今天是个开心的日子,又是周末,可以安心轻松的写写文章了。经过了大概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框架简介

  1. 框架名称:enode
  2. 框架特色:提供一个基于DDD设计思想,实现了CQRS + EDA + Event Sourcing + In Memory这些架构模式的,支持负载均衡的,轻量级应用开发框架。
  3. 开源地址:https://github.com/tangxuehua/enode
  4. nuget包Id:enode

使用该框架前需要了解或遵守以下几个约定:

  1. 一个command只允许导致一个聚合根的修改或一个聚合根的创建,如果违反这个规则,则框架不允许;
  2. 如果一个用户操作会涉及多个聚合根的修改,则需要通过saga (process manager)来实现;拥抱最终一致性,简单的说就是通过将command+domain event不断的串联来最终实现最终一致性;如果想彻底的知道enode哪里与众不同,可以看一下源代码中的BankTransferSagaExample,相信这个会让你明白什么是我所说的事件驱动设计;
  3. 框架的核心编程思想是异步消息处理加最终一致性,所以,如果你想实现强一致性需求,那这个框架不太适合,至少目前没有提供这样的支持;
  4. 框架的设计目标不是针对企业应用开发,传统企业应用一般访问量不大且要求强一致性事务;enode框架更多的是针对互联网应用,特别是为一些需要支持访问量大、高性能、可伸缩且允许最终一致性的互联网站点提供支持;看过:可伸缩性最佳实践:来自eBay的经验的人应该知道要实现一个可伸缩的互联网应用,异步编程和最终一致性是必须的;另外,因为如果数据量一大,那我们一般会把数据分开存放,这就意味着,如果你还想实现强一致性,那就要靠分布式事务。但是很不幸,分布式事务的成本代价太高。伸缩、性能和响应延迟都受到分布式事务协调成本的反面影响,随着依赖的资源数量和用户访问数量的上升,这些指标都会以几何级数恶化。可用性亦受到限制,因为所有依赖的资源都必须就位。
  5. 框架定位:目前定位于单台机器上运行的单个应用内的CQRS架构前提下的command端的实现;如果要实现多台机器多个应用之间的分布式集成,则大家需要再进一步借助ESB来与更高层的SOA架构集成;

enode框架架构图:

开发ERP软件应该遵守的22条规则_第1张图片

CQRS架构图

上面的架构图是enode框架的内部实现架构。当然,上面这个架构图并不是完整的CQRS架构图,而是CQRS架构图中command端的实现架构。完整的CQRS架构图一般如下:

开发ERP软件应该遵守的22条规则_第2张图片

从上图我们可以看到,传统的CQRS架构图,一般画的都比大范围,command端具体如何实现,实现方案有很多种。而enode框架,只是其中一种实现。

enode框架的内部实现说明

  1. 首先,client会发送command给command service,command service接受到command后,会通过一个command queue router来路由该command应该放到哪个command queue,每个command queue就是一个消息队列,队列里存放command。该消息队列是本地队列,但是支持消息的持久化,也就是说command被放入队列后,就算机器挂了,下次机器重启后,消息也不会丢失。另外,command queue我们可以根据需要配置多个,上图为了示意,只画了两个;
  2. command queue的出口端,有一个command processor,command processor的职责是处理command。但是command processor本身不直接处理command,直接处理command的是command processor内部的一些worker线程,每个worker线程会不断的从command queue中取出command,然后按照图中标出的5个步骤对command进行处理。可以看出,由于command processor中的worker线程都是在并行工作的,所以我们可以发现,同一时刻,会有多个command在被同时处理。为什么要这样做?因为client发送command到command queue的速度很快,比如每秒发送1W个command过来,也就是并发是1W,但是command processor如果内部只有单线程在处理command,那速度跟不上这个并发量,所以我们需要设计支持多个worker同时处理command,这样延迟就会降低;我们从架构图可以看到,command processor获取聚合根是从内存缓存(如支持分布式缓存的redis)获取,性能比较高;持久化事件,用的是MongoDB,由于mongoDB性能也很高;如果觉得事件持久化到单台MongoDB server还是有瓶颈问题,那我们可以对MongoDB server做集群,然后对事件进行sharding,将不同的event存储到不同的MongoBD Server,这样,事件的持久化也不会成为瓶颈;这样,整个command processor的处理性能理论上可以很高,当然我还没测试过集群情况下性能可以达到多少;单个mongodb server,持久化事件的性能,5K不成问题;这里有一点借此在说明下,被持久化的其实不是单个事件,而是一个事件流,即EventStream。为什么是事件流是因为单个聚合根一次可能产生不止一个领域事件,但是这些事件比如一起被持久化,所以设计思路是把这些事件设计为一个事件流,然后将这个事件流作为一条mongodb的记录插入到mongodb;事件流在mongodb中的主键是聚合根ID+事件流的版本号,通过这两个联合字段作为主键,用来实现乐观锁;假如有两个事件流都是针对同一个聚合根的,且他们的版本号相同,那插入到mongodb时,会报主键索引冲突,这就是并发冲突了。需要对command进行自动重试(enode框架会帮你自动做掉这个自动重试)来解决这个问题;
  3. command processor中的worker处理完一个command后,会把产生的事件发布给一个合适的event queue。同样,内部也会有一个event queue router来路由到底该放到哪个event queue。那么event queue中的事件接下来要被如何处理呢?也就是event processor会做身事情呢?很简单,就是分发事件给所有的事件订阅者,即dispatch event to subscribers。那这些event subscribers都会做什么事情呢?一般是做两种处理:1)因为是采用CQRS架构,所以我们不能仅仅持久化领域事件,还要通过领域事件来更新CQRS的查询端数据库(这种为了更新查询库的事件订阅者老外一般叫做denormalizer);由于更新查询库没有必要同步,所以设计event queue;2)上面提到过,有些操作会影响多个聚合根,比如银行转账,订单处理,等。这些操作本质上是一个流程,所以我们的方案是通过在领域事件的event handler中发送command来异步的实现串联整个处理流程;当然,如何实现这个流程,还是有很多问题需要讨论。我个人觉得比较靠谱的方案是通过process manager,类似BPM的思想,国外也有很多人把它叫做saga。对saga或process manager感兴趣的看官,可以看看微软的这个例子:http://msdn.microsoft.com/en-us/library/jj591569.aspx,对于如何用enode来实现一个process manager,由于信息太多,所以我接下来会写一篇文章专门系统的介绍。

回顾enode框架所使用的关键技术

基于整个enode框架的架构图以及上面的文字描述说明,我们在看一下上面最开始框架简介中提到的框架所使用的关键技术。

  1. DDD:指架构图中的domain model,采用DDD的思想去分析设计实现,enode框架会提供实现DDD所必要的基类聚合根以及触发领域事件的支持;
  2. CQRS:指整个enode架构实现的是CQRS架构中的command端,CQRS架构的查询端,enode框架没做任何限制,我们可以随意设计;
  3. EDA:指整个编程模型的思路,都要基于事件驱动的思想,也就是领域模型的状态更改是基于响应事件的,聚合根之间的交互,也不是基于事务,而是基于事件驱动和响应;
  4. Event Sourcing:中文意思是事件溯源,关于什么是事件溯源,可以看一下这篇文章。通过事件溯源,我们可以不用ORM来持久化聚合根,而是只要持久化领域事件即可,当我们要还原聚合根时只要对该聚合根进行一次事件溯源即可;
  5. In Memory:是指整个domain model的所有数据都存储在内存缓存中,比如分布式缓存redis中,且缓存永远不会被释放。这样当我们要获取聚合根时,只要从内存缓存拿即可,所以叫in memory;
  6. NoSQL:是指enode用到了redis,mongodb这样的nosql产品;
  7. 负载均衡支持:是指,基于enode框架的应用程序,可以方便的支持负载均衡;因为应用程序本身是无状态的,in memory是存储在全局的redis分布式缓存中,独立于应用本身;而event store则是用MongoDB,同样也是全局的,且也支持集群。所以,我们可以将基于enode框架开发的应用程序部署任意多份在不同的机器,然后做负载均衡,从而让我们的应用程序支撑更高的并发访问。

框架API使用简介

框架初始化

复制代码
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();
}
复制代码

command定义

[Serializable]
public class ChangeNoteTitle : Command
{
    public Guid NoteId { get; set; }
    public string Title { get; set; }
}

发送command到ICommandService

var commandService = ObjectContainer.Resolve<ICommandService>();
commandService.Send(new ChangeNoteTitle { NoteId = noteId, Title = "Modified Note" });

Command Handler

复制代码
public class ChangeNoteTitleCommandHandler : ICommandHandler<ChangeNoteTitle>
{
    public void Handle(ICommandContext context, ChangeNoteTitle command)
    {
        context.Get<Note>(command.NoteId).ChangeTitle(command.Title);
    }
}
复制代码

Domain Model

复制代码
[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;
    }
}
复制代码

 Domain Event

复制代码
[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;
    }
}
复制代码

Event Handler

复制代码
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));
    }
}
复制代码

后续需要讨论的关键问题

  1. 既然是消息驱动,那如何保证消息不会丢失;
  2. 如何保证消息至少被执行一次,且不能被重复执行;
  3. 如何确保消息没执行成功就不能丢,也就是要求消息队列支持事务;
  4. 因为是多线程并行持久化事件并且是多台机器集群负载均衡部署的,那如何保证领域事件被持久化的顺序与发布到事件订阅者的顺序完全一致;
  5. 整个架构中,基于redis实现的memory cache以及基于mongodb实现的eventstore,是两个关键的存储点,如何确保高吞吐量和可用性;
  6. 因为事件是并行持久化的,那如果遇到并发冲突如何解决?
  7. 命令的重试如何实现?消息队列中的消息的重试机制如何实现?
  8. 既然抛弃了强一致性的事务概念,而用process manager来实现聚合根交互,那如何具体实现一个process manager?

目前暂时想到以上8个我觉得比较重要的问题,我会在接下来的文章中,一一讨论这些问题的解决思路。我觉得写这种介绍框架的文章,一方面要介绍框架本身,更重要的是要告诉别人你设计以及实现框架时遇到的问题以及解决思路。要把这个分析和解决的思路写出来,这才是对读者意义最大的;

 
 
标签:  DDDEDAcqrsEventSourcingInMemory

你可能感兴趣的:(DDD,EDA,CQRS,EventSourcing,InMemory)