一个中型OA系统的架构过程(.net)

来源:互联网 发布:飞扬软件图片 编辑:程序博客网 时间:2024/04/28 16:00
最近在作一图(GIS)文(MIS)一体化办公系统的架构,在这里将其架构设计写出来,供大家板砖板砖。系统采用DotNet作为其开发语言,C/S结构,分多层开发模式。以下部分为系统在进行详细分析前所作的架构。

 一、大框架

   三层结构,界面层,逻辑层,数据层。其中界面层命名为:UI,逻辑层又分为几层,业务外观(BusinessFacadeProjects)、业务规则(BusinessRuleProjects)、数据访问(DataAccessProjects)三层,其实算起来,系统已经不是严格的三层模式了,已经算作是多层系统了。

  设计思想说明:界面层与业务外观层进入数据交互,业务外观允许对业务规则或者数据访问层进行数据交互,业务规则进允许对数据访问层进行交互。也就是说用户所对界面进行的所有操作,都先通过业务外观层进入,不管逻辑层如何处理,最后都是通过数据访问层与数据库进行交互,同理,不管要从数据库取任何数据,先通过数据访问层再经过逻辑层的处理,再从业务外观层“丢”给界面层。为何如此设计?多层架构思想在这里就不多说了,本人在这里考虑的是C/S与B/S的统一,各种数据库的统一,例如,如果我的系统是C/S,我想改为B/S,那么由于有业务外观层统一为数据“入口”,那么我要改的东西仅仅是添加B/S界面层的代码;又,如果你把你的数据库从Sql Server改为Oracle,那么你只要导入数据库,然后改一下数据访问层的代码,就可以了,当然了,这里的前提是你的所有Sql语句都得写在数据访问层上。

 二、层架设

  1.界面层:这个没有什么好说的,UI就是一个项目(工程?包?)管它怎么叫,反正编译出来就一Exe文件。

  2.业务外观:为了方便起见,在引用的时候只要引用一个则可,所以就一个工程BusinessFacade,当然里面你可以包含多个类,然后在UI里面using或者Import。

  3.业务规则:这里分多个工程,一个为数据处理,一个为工作流引擎,一个为字符处理,另一个为项目规则定义。
  数据处理我们命名为DataOption,这里的意思是只要是与数据有关的,且要进行相应处理的,我们就先经过这里处理一下。
  工作流引擎命名为WorkFlow,由于太大块,也太复杂,有时间再分出来讨论。
  字符处理在这里命名为StringFormat,这一个工程是用来干什么的呢?其实很简单的,比如将一些Sql语句拆散了再组合,又或者将一些字符串经过一些特定的处理再返回,这些东西在大系统里面经常用到,所以在这里把它分出来,另外编译成一Dll,也方便以后重用。当然在后面的开发中,要求程序员要有正则表达式的相关知识。
  项目规则命名为ProjectRule,又是什么东东呢?这里只要是对一些数据进行标准化的处理规则,比如,我们要生成一个列表页,一般是返回一个DataTable,然后绑定到DataGrid中,当然,如果你仅仅有DataTable,在UI上你就分不清每列代表什么东西,除非你的DataTable命名非常规范。所以在这里你还得返回一个Title(列头组合),为了美观,你当然还得返回Width(列宽)的集合,然后在UI里地这些东西进行处理,让用户看起来是一个可读的且美观的列表。这些东西既然都是通用的,你为什么不把他们再定义呢?也就是说把Title,Width,DataTable定义为一个类,比如定义为DataList,里面包括一DataTable,两个字符串数组,我们以这个类为标准进行数据交互,那么UI层的开发人员,你就不用管别的层是如何开发的,总之你的处理就是按这个标准进行开发,同时,数据处理层的人员也是,不管你UI层如何搞,我只按这个标准生成数据集合,如此一来整个系统的数据列表的开发就统一起来了。同时由于有了这个标准,每一层的开发人员都最大限度地考虑了在这个标准的基础上可能出现的错误处理,可以使整个系统容错性更好更健壮。当然在这里不仅仅是一个DataList,还要分很多的“Rule”,只是在这里暂时就不一一列出了。

  4.数据访问:分两个工程,一个工程只对数据库进行直接的操作,提供各种各样的接口,我把它命名为DataBaseOption。另一个不进行对数据的直接操作,只是所有的与Sql语句有关的都放在这里,然后通过DataBaseOption进行操作数据库,我在这里命名为DataAccess。把这一层分离出来的好处是,如果客户突然间想把系统改为另一种数据库进行数据的存储,那么你只要改DataAccess的代码及DataBaseOption的少量代码则可全盘搞定。

 三、层详细架设

   1.UI界面层:界面层大概主要由表单,菜单,按钮等组成,当然这些东西还有统一的样式。然后用户对数据的操作能做的大体就是添加删除修改等功能,由于是在详细分析前,所以当然别的不通用的功能在这里暂时先不讨论。由于以上部分是通用的,所以在这里我们先做一个基类,在这里,我们命名基类时前面带Base字样,一般的Form我们前面带Frm,这个基类的命名则为BaseForm.vb/BaseForm.cs,它主要的功能是定义通用的数据操作及界面样式生成,例如此系统要实现类似于Office的样式功能,且从2000-2003是可选择的,那么这些处理的方法就定义在这个Form里面,当用户改变样式时,它的任务是对每一个MDI子窗口的样式改为原先定义好的样式。所以,在这里我们得定义一个void或者sub类型的方法:

  public void SystemStyle(enum office)
  {
    这里包括窗体框架的样式和窗体内输入表单的样式。
  }

  office枚举为2000/XP/2003/default,至于方法中如何处理,我们在这里暂时不管它,留给编码者思考。   为了在MDI子窗体中能实现统一的删除/修改/添加功能,在这里我们在基类里面定义添加/编辑/删除三个方法,且这三个方法在继承后是可以override的,也就是说,为了防止在父窗体中点击添加/修改/删除按钮都不会出问题,我们必须所有的子窗口都应该从这里继承,为了通用起见,在这里的BaseForm中必须是没有操作的,也就是说仅仅是一个空的方法体。 

    /// <summary>
    /// 保存,可重写
    /// </summary>
    public virtual void Save()
    {
    }

    /// <summary>
    /// 删除,可重写
    /// </summary>
    public virtual void Delete()
    {
    }

    /// <summary>
    /// 添加新的东东
    /// </summary>
    public virtual void AddNew()
    {
    }

  在这个系统中,我所定义的BaseForm.cs原则是所有的窗体都应该继承的基类,但是所有的窗体中又可以再分出类来,所以在这个基础上,我们得再写基类。比如,在系统中,输入表单的Form占了系统的很大一部分,所以,在这里我得给它们抽象出一个比较通用的操作方法,所以在这里定义了一个BaseInputForm.cs,这个类用来干什么呢?我在这里的设想是只要开发人员在开发输入窗体时,主要继承了这个类,那么他的工作就是设置一个TableName属性为相对应的数据表名,把须要的控件拖进窗体,把控件的Name属性设置为相应的字段名,则可实现数据的添加/更新/删除。为了能达到这个功能,在这里先继承了BaseForm.cs,然后定义一个Hashtable,key用来保存输入表单的Name属性值,Value则保存表单的Text/Value值。
  一个Hashtable,我们如何才能把当前窗体的输入表单值全部保存进去呢?这个就得写一个递归的循环当前窗体的所有控件的方法了。因为窗体的控件不仅仅是可以输入的,所以在这里还得写方法筛选,代码如何写,在这里就不给出了。   在这里,TableName是一个属性,这个属性我们用来保存当前被操作的数据表的表名,同时我们还得到了一个带有字段名及对应该输入值的Hashtable,所以在这里我们得在业务规则(BusinessRuleProjects)层写一个规则,这个规则包括一string字段(表名),一Hashtable字段(字段及对应值),一enum字段(添加/修改/删除),然后我们的各层就通过这个规则作数据交互。例如,点击保存按钮->Save()->构造规则->调用相应该的数据访问层的方法,实现数据更新。
  这个Form除了定义表单外,我们还得定义按钮(Menu,Bar等)的自动生成,在这里如果想实现与权限的对应,就得把按钮的信息写在数据库里,然后根据不同的登录名不同的权限,生成不同的对应该操作菜单,在这里由于时间限制,不作详细讨论了。
  有了以上的东东之后,那么我们的程序员在设计输入表单的时候,主要实现排版及设置好Name属性后就一切OK,则于BaseInputForm.cs继承了BaseForm.cs,所以在这里程序员根本就不用考虑样式,会自动处理(当然你的控件要支持多样式功能,VS自带的是不支持的,所以你得找一套支持的,如果你的水平够强,自己写一套也可,我在这里的C/S系统就是用的一套国外开发的,而B/S的则是本人自己开发)。也不用考虑如何添加进数据库的问题,因为这些问题不是属于界面层的问题,至于此部分的设计,以后有时间再作分析。
  除了输入表单的基类外,我们还考虑了一个列表页的基础,这个基类在这里不作详细说明了,其规则为上面ProjectRule的例子说明。

(明天要出差去了,以下部分不知何时才有时间补上。由于“你的砖头就是我的动力!”,所以,如果砖头来得比较猛烈的话,估计小梦在出差期间只好再“砌砖”了)。

接上文,我们讲解了UI界面层的在设计初期的架构部分,由于是在系统分析还没有全面展开的情况下先做的初期架构,所以中间业务逻辑部分我们暂时放下,先来考虑数据访问层的一些问题。

    2.数据访问层:分两个工程,一工程主要封装数据库访问及操作的对像,我将其命名为DataBaseOption,另一工程主要功能为负责业务逻辑对操作数据库部分的功能进行构造一些操作数据库的规则然后调用DataBaseOption的对像进行对数据库的操作,在这里我把它命名为DataAccess。其中DataAccess与业务逻辑有关,在这里暂时先放一边,而DataBaseOption我们可以看作仅仅与数据库有关,所以在这里我们先作这一部分的讨论。

    对于数据库的操作,我们最常用的就是查询/插入/更新/删除,当然对于一个OA系统来说,还应该多加一个,那就是事务。

    对于查询,我们在这里应该考虑的有直接传入SQL语句查询或者存储过程的查询,当然视图在这里你已经可以把它当一个表看待了,暂时不考虑。查询返回的值,对于DotNet来说,最常用的可能就是返回DataSet,DataReader了,而DataTable被包含于DataSet之中。

    于对插入和更新,其实我们可以放在一起考虑的,因为插入本身就是更新了数据库,而插入或者更新的方法当然可以用SQL语句实现,这些通用的方法在这里我们还是得考虑的。再而就是用数据集更新数据库,也就是用一个适配器更新一个记录集(DataSet)。当然在更新记录集当中我们也可以对记录集里面的一些记录进行删除,然后用记录集更新数据库,达到删除数据的目的。

    至于删除,就没有什么好说的了,删除比较简单。
    对于事务,能够创建/结束与回滚。

    好了,有了上面的分析,现在我们开始思考如何架设这个类。首先当然得有个数据库连接,为了严格地不让这个连接在别的地方使用,我们定义为:

    private OracleConnection conn = new OracleConnection;
    private string connString;

    为了能实现DataSet更新数据库,我们还得定义一个OracleDataAdapter:
    private OracleDataAdapter upDateAdaper;

    同时我们还得定义一个连接字符串,这个串,为了方便我们的开发,把它定义为一个属性,便于我们把它存放于任何地方,比如存放在Web.config配置文件里。
  public string ConnString
  {
   get
   {
    return connString;
   }
   set
   {
    connString = value;
   }
  }
 
   好了,到了这里,我们得考虑性能的问题,如果一个连接长期连接的话,会占用很多的资源,所以在这里我们要保证在对数据进行操作的时候连接是打开着的,同时,当我们的操作结束的时候,连接马上关闭。如何实现呢?在这里可以定义一个数据库连接的方法,判断conn的State返回或者从新打开一个连接。当我们用完这个连接后,记得Close()一下就可以了。

    查询返回值问题,我们定义两类,一类是DataSet,一类是OracelDataReader。
    public DataSet GetDataSet(string sql)
    {
    }
    public DataSet GetDataSet(string tableName,string fields,string condition)
    {
    }
    public static DataSet ReturnDataSet(……)
    ……

    public OracleDataReader GetDataReader(string sql)
    {
    }
    public OracleDataReader GetDataReader(……)
    {
    }
    public static OracleDataReader ReturnDataReader(……)
    {
    }
    ……

    由上面给出的部分代码,我们可以看到有部分是static类型的,定义为static类型主要是为了方便引用,而仅仅为public类型DataSet方法,则要考虑到更新数据库的问题,也就是说生成一个DataSet的同时要生成一个Adapter,在后面部分的更新程序里面要保证这个在逻辑层已经被更改过的DataSet能够Update到数据库里,当然这些说明应该在方法体上面的XML注释里说明。

    在这里我们应该注意一点,也就是仅有一个sql参数及有三个参数的GetDataSet的区别,这两个东东的作用几乎是一样的,为什么写两个呢?这个是考虑到数据库的问题,也就是说,如果你的系统在开发阶段控制得很好,所有SQL语句全由DataBaseOption构造,那么,如果你把你的数据库从Oracle改为Sql Server的时候,可能你只要改变这个数据库操作类就可以实现了;但是如果你的sql语句由DataAccess构造,然后传给只有一个参数的GetDataSet,由于每种数据尽管都支持标准的SQL,但是也有些差别,所以当你进行数据库移植时,有可能得同时修改DataAccess,DataBaseOption的代码。当然了,如果你的系统根本就不会出现这种改变数据库容器的问题,你根本就不用考虑这么多。

    设计了数据的查询后,我们来思考一下数据的更新问题,前面说到了,有用SQL语句更新和DataSet更新,在这里特定于这个系统来说,我们在第一部分还说到了用Hashtable保存数据然后更新的问题,所以在这里我们还得考虑如果用Hashtable更新DataSet,然后再更新数据库。废话少说。

    public enum UpdateOption{insert,update};
    public int UpdateData(string sql)
    {
    }
    public int UpdateData(DataSet myDataSet)
    {
    }
    public int UpdateData(string tableName,Hashtable hsTable,UpdateOption option)
    {
    }
    public int UpdateData(……)
    {
    }

    同样,在这里也应该有static类型的。
    ……

    在这里,传入一个DataSet如果更新数据库呢?首先在生成DataSet的时候我们要new一个DataAdapter,然后把这个Adapter暂时寄存于upDateAdaper中,在UpdateData里面再调用upDateAdaper来update这个DataSet,至于代码如何编写,有什么技巧性的问题,这个已经属于编码者的问题了。同样像OracleCommand这些东西,由于仅仅与编码者有关,在这里不作分析。

    数据的删除问题,一部分已经在数据更新里面实现,另一部分就是用Sql语句实现,这些不用多说。比如写一些通用的ExecuteNonQuery方法则可。

    事务问题,其实在大型的数据库里都已经有事务处理功能了,我们的任务就是写通用的方法创建/结束/回滚事务,方便在DataAccess里面调用。

    由于小梦在出差,时间比较紧,在这里就不能作很详细的介绍了,其实对于数据库操作类,仅仅是那些方法体的参数,都得考虑半天,真正在作架构的时候,你必须得考虑得全面,因为这个关系到你架构逻辑层引用的问题。同时,你必须要很详细的注明每一个方法体的功能及其参数的说明,否则编码人员不知道你的意图如何,到时候就有可能出现偏差。如果你懂Rose(Rose不支持DotNet,要用Rational XDE Developer for .net)用它来架构然后生成代码,会节省你很多的时间。

    有了界面层和数据操作层,我们举个例子说明它的工作过程:
    首先,我们的程序员对照着数据库设计文档设计一个Form(拖动输入控件设置Name属性)-->生成一输入窗体-->点击保存-->传表名及带有表信息的哈希表给业务外观层-->调用DataAccess相应的类及方法-->调用DataBaseOption的UpdateData(string tableName,Hashtable hsTable,UpdateOption option)方法,实现数据的插入/更数。