X-gen设计模式综合实战2-配置管理

发表于:2018-12-21 22:26

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:王侦    来源:简书

  1.配置管理模块的详细功能
  获取用户配置的数据
  配置的方式很多,要求除了框架自身提供的配置方式外,还要能支持用户自定义的配置方式。
  比如:框架本身提供默认的xml配置,如果用户想使用数据库来配置,那么框架必须能够支持用户自定义一种数据库配置的方式,并能够很容易的加入到系统中进行使用。
  缓存用户配置的数据
  同一份配置数据,在运行期间会多次使用,但是获取用户配置数据的动作就只需要一次就可以了,获取过后,就缓存下来,重复使用。
  对外提供接口
  让其他模块通过这些接口来获取他们需要的数据。
  2.配置管理模块的功能边界
  (单一职责)配置管理模块不关心被访问的数据是怎么来的,它只是按照访问方式去获取这些数据,当然不同的数据格式有不同的访问方式。
  虽然需要支持自定义配置方式,但是需要配置的数据是一定的,只是配置的格式不同,访问这些配置数据的方式不同,但最后殊途同归,都向配置管理模块提供相应的数据。因此,这个需要配置的数据项是要统一起来的。
  (单一职责)配置管理模块不关心获取到的数据是什么,也不关心这些数据的含义,更不关心这些数据怎么用,它只是负责获取这些数据并保存起来。
  (单一职责)配置管理模块不关心谁来使用这些数据,也不关心外部获取这些数据后如何使用,它只是负责提供这些数据而已。
  3.配置管理模块对外接口
  对外数据接口设计
  X-gen框架并不局限于使用xml作为配置的方式,可以选用properties文件、数据库等其他的配置方式。
  虽然配置方式不同,需要配置的数据却是一样的,所以,你可以把这里xml描述的配置数据改成任何你希望使用的配置方式相对应的数据结构。
  3.1 第一类是核心框架运行需要的数据
  以xml配置为例,默认取名为GenConf.xml,在每次使用的时候根据需要修改或配置其内容。为了示例简单,没有做dtd或者schema。
  <?xml version="1.0" encoding="UTF-8"?>
  <GenConf>
  <NeedGens>
  <NeedGen id="UserGenConf" provider="XmlModuleGenConf" themeId="simple">
  <Params>
  <Param id="fileName">UserGenConf.xml</Param>
  </Params>
  </NeedGen>
  </NeedGens>
  <Themes>
  <Theme id="simple">cn/javass/themes/simple</Theme>
  </Themes>
  <Constants>
  <Constant id="prePackage">cn.javass</Constant>
  <Constant id="projectName">test</Constant>
  </Constants>
  </GenConf>
  <NeedGens>用来注册需要generate的模块的配置,可以注册很多个需要generate的模块。
  <NeedGen>表示其中的一个模块:
  id:模块的标识号,必须唯一;
  provider:用来获取该模块配置数据的标识,对应的类在该模块使用的theme里面配置;
  themeId:该模块使用的theme。
  <Params>参数,通常是根据不同的provider,提供不同的参数配置,每个参数配置都包括参数标识和具体的参数值。
  <Param>一个参数的配置。(key, value)
  <Themes>注册外部themes
  <Theme>一个theme,配置一个相对路径(theme的根文件目录)。
  <Constants>公共常量
  3.2 第二类是用户需要生成的模块的配置数据
  比如:用户想要生成一个模块内的增删改查的源代码,里面有每个具体功能的配置,而每个具体功能就是一个源代码文件。
  需要在每次使用的时候根据需要来配置,并注册到核心框架运行配置里面去,每次生成主要配置的就是这类数据。
  假设配置文件名为UserGenConf.xml:
 <?xml version="1.0" encoding="UTF-8"?>
  <ModuleGenConf id="UserConf">
  <NeedGenTypes>
  <NeedGenType id="GenBusinessEbi">
  <NeedGenOutType id="Console"></NeedGenOutType>
  <NeedGenOutType id="File"></NeedGenOutType>
  </NeedGenType>
  </NeedGenTypes>
  <ExtendConfs>
  <ExtendConf id="codeOutPath" isSingle="true">build</ExtendConf>
  <ExtendConf id="moduleName" isSingle="true">test</ExtendConf>
  </ExtendConfs>
  </ModuleGenConf>
  <NeedGenTypes>用来配置需要生成的功能,可以配置多个
  <NeedGenType>配置一个本模块需要生成的功能
  <NeedGenOutType>配置模块的某个功能生成完成后,输出的类型,一个功能可以有多种输出。
  <ExtendConfs>本模块需要配置的,自行扩展的数据。
  <ExtendConf>一条自行扩展的数据。
  isSingle:数组是单值还是数组。
  3.3 第三类是外部主题的配置数据
  在制作主题的时候就配置好,里面有这套主题需要的外部数据,和预定义好的功能配置,在每次使用的时候一般不需要配置或修改。
  外部主题目前采用默认xml方式,文件名固定是ThemeConf.xml。没必要支持自定义。
 <?xml version="1.0" encoding="UTF-8"?>
  <Theme id="simple">
  <GenTypes>
  <GenType id="GenBusinessEbi" type="cn.javass.themes.simple.actions.GenBusinessEbiAction">
  <Params>
  <Param id="relativePath">business.ebi</Param>
  <Param id="mbPathFile">business/Ebi.txt</Param>
  <Param id="preGenFileName"></Param>
  <Param id="afterGenFileName">Ebi.java</Param>
  </Params>
  </GenType>
  </GenTypes>
  <GenOutTypes>
  <GenOutType id="Console" type="cn.javass.xgen.output.types.OutputToConsole"></GenOutType>
  <GenOutType id="File" type="cn.javass.xgen.output.types.OutputToFile"></GenOutType>
  <GenOutType id="My" type="cn.javass.themes.simple.outputtypes.MyOutput"></GenOutType>
  </GenOutTypes>
  <Providers>
  <Provider id="XmlGenConf" type="cn.javass.xgen.genconf.implementors.xmlimpl.GenConfXmlImpl"></Provider>
  <Provider id="XmlModuleGenConf" type="cn.javass.xgen.genconf.implementors.xmlimpl.ModuleGenConfXmlImpl"></Provider>
  <Provider id="XmlThemeConf" type="cn.javass.xgen.genconf.implementors.xmlimpl.ThemeXmlImpl"></Provider>
  </Providers>
  </Theme>
  <GenTypes>主题提供的可输出的功能
  <GenType>一个可输出的功能。在ModuleGenConf里面配置每个模块的NeedGenType的id就是该值。
  type:真正实现输出功能的类。
  <Params>配置每个输出类需要的参数
  <Param>每个参数的配置
  <GenOutTypes>主题提供的输出类型
  <GenOutType>一个具体的输出类型。在ModuleGenConf里面配置每个模块的NeedGenOutType的id就是该值。
  type:真正实现输出类型的类。
  Providers:主题提供的读取配置文件的类型
  Provider:一个具体的读取配置文件的类型。在GenConf里面配置的NeedGen的provider就是该值。
  type:真正实现具体的读取配置文件类
  3.4 对外程序接口设计
  为了让外部获取配置管理模块内的数据,提供相应的API——GenConfEbi。
  4.配置管理模块的内部实现
  实现的起点
  为了让大家更好的理解配置管理模块的内部实现架构,因此先以一个最简单的实现结构为起点,采用重构的方式,逐步把相关的设计模式应用进来,从简单到复杂,从而让大家更好的看到如何选择要使用的设计模式、如何实际应用设计模式以及如何让多种设计模式协同工作。
  1.先就来看看实现配置管理的起点,首先根据对外提供的数据结构定义,制作出相应的数据model来。
  2.针对前面定义的API,提供一个最基本的实现,只需要满足最基本的功能就可以了,需要实现读取配置文件的功能,然后要有缓存配置数据的功能,最后就是实现API中要求的功能。
  此时配置管理模块的结构示意如图
  4.1 简单工厂
  4.1.1 面临的问题
  观察上面的实现,向模块外部提供了接口,可是外部根本不知道模块内部的具体实现,那么模块外部如何来获取一个实现接口的实现对象呢?
  4.1.2 用简单工厂来解决
  简单工厂是解决上述问题的一个合理方案。那么先一起来回顾一下简单工厂的一些基础知识,然后再来看如何应用它来解决上面的问题。
  提供一个创建对象实例的功能,而无须关心其具体实现。被创建实例的类型可以是接口、抽象类,也可以是具体的类。
  4.1.3 使用简单工厂来解决问题的思路
  简单工厂解决这个问题的思路就是,在配置管理模块里面添加一个类,在这个类里面实现一个方法,让这个方法来创建一个接口对象并返回然后把这个类提供给客户端,让客户端通过调用这个类的方法来获取接口对象。
  4.1.4 此时配置管理模块的结构示意如图
  4.2 单例模式
  4.2.1 面临的问题
  面临的问题看看上面的基本实现,会发现一些问题:
  1)如果GenConfEbo被创建多次的话,那么就会重复获取配置数据,浪费程序运行时间;
  2)并且每个GenConfEbo的实例都会缓存这些数据,浪费内存空间。
  3)同一个类里面,既有实现GenConfEbi要求的对外功能,又有内部实现需要的获取配置数据和缓存数据的功能,从类的设计上来说,这个类的职责太不单一了,应该分离一部分职责出去。
  因此这种实现肯定是不好的,那么怎么解决呢?
  4.2.2 用单例模式来解决
  保证一个类仅有一个实例,并提供一个访问它的全局访问点。
  此时配置管理模块的结构示意如图
  4.3 桥接模式
  4.3.1 面临的问题
  面临的问题按照功能要求,配置数据的来源是多方面,比如:xml、properties、 txt、DB等等,这也就意味着需要有不同的获取数据的实现来对应这些不同的数据来源。
  另外一个方面,对于模块外部的应用而言,他们不关心配置数据是如何来的,他们只关心需要使用的数据,而且在某些需要的情况下,他们可能会要求更多的、不同的数据,他们会认为“只要他们要数据,配置管理模块就应该提供这些数据”。换句话说,这些数据的多少、数据的组合结构是可能发生变化的。
  这就出现了两个纬度的变化,一个是获取配置数据这边需要不断扩展,另一个是配置数据所构成的数据模型这边也需要变化和扩展,怎么办呢?
  4.3.2 用桥接模式来解决
  将抽象部分与它的实现部分分离,使它们都可以独立地变化。
  4.3.3 使用桥接模式来解决问题的思路
  可以把获取配置数据这边,设计成为桥接模式的实现部分,而数据模型这边设计成为桥接模式的抽象部分,而且数据模型这边确实需要使用具体实现部分来获取数据,简直就是标准的桥接模式的应用。
  而ConfManager就相当于桥接的抽象部分的顶层实现,而GenConfEbo就相当于使用基本的抽象部分的扩展部分,只不过这里采用的是对象组合的方式,而不是标准桥接模式中的对象继承的方式。
  具体实现那边,需要先定义出实现的接口来,然后不同的配置方式对应不同的实现。
  4.3.4 此时配置管理模块的结构示意如图
  4.4 解释器模式
  4.4.1 面临的问题
  面临的问题自己解析xml,本来也没有什么特别困难的,但是问题就在于,如果xml文件的格式发生了变化,那么读取配置文件的程序就需要做出相应的变更,严重的时候,几乎相当于完全重写程序。
  那么怎么解决当xml的结构发生改变过后,能够很方便的获取相应元素、或者是属性的值,而不用再去修改解析xml的程序呢?
  4.4.2 用解释器模式来解决
  给定一个语言,定义它的文法的一种表示,并定义一个解释器,这个解释器使用该表示来解释语言中的句子。
  4.4.3 使用解释器模式来解决问题的思路
  要解决通用解析xml的问题:
  第一步:需要先设计一个简单的表达式语言,在客户端调用解析程序的时候,传入用这个表达式语言描述的一个表达式,然后把这个表达式通过解析器的解析,形成一个抽象的语法树。
  第二步:解析完成后,自动调用解释器来解释抽象语法树,并执行每个节点所对应的功能,从而完成通用的xml解析。
  这样一来,每次当xml结构发生了更改,也就是在客户端调用的时候,传入不同的表达式即可,整个解析xml过程的代码都不需要再修改了。
  4.4.4 约定简单的语法规则
  如下,为了通用,用root表示根元素,a、b、c、d等来代表元素,一个简单的xml如下:
   <?xml version="1.0" encoding="UTF-8"?>
  <root id="rootId">
  <a>
  <b>
  <c name="testC">12345</c>
  <d id="1">d1</d>
  <d id="2">d2</d>
  <d id="3">d3</d>
  <d id="4">d4</d>
  </b>
  <e id="e1">
  <f>f1</f>
  </e>
  <e id="e2">
  <f>f2</f>
  </e>
  </a>
  </root>
  约定简单的语法规则如下:
  1)获取单个元素的值:从根元素开始,一直到想要获取值的元素,元素中间用“/”分隔,根元素前不加“/”。比如表达式“root/a/b/c”就表示获取根元素下、a元素下、b元素下的c元素的值
  2)获取单个元素的属性的值:要获取值的属性一定是表达式的最后一个元素的属性,在最后一个元素后面添加“.”然后再加上属性的名称。比如表达式“root/a/b/c.name”就表示获取根元素下、a元素下、b元素下、c元素的name属性的值
  3)获取相同元素名称的值,当然是多个:要获取值的元素一定是表达式的最后一个元素,在最后一个元素后面添加“$”。比如表达式“root/a/b/d$”就表示获取根元素下、a元素下、b元素下的多个d元素的值的集合
  4)获取相同元素名称的属性的值,当然也是多个:要获取属性值的元素一定是表达式的最后一个元素,在最后一个元素后面添加“$”,然后在后面添加“.”然后再加上属性的名称,在属性名称后面也添加“$”。比如表达式“root/a/b/d$.id$”就表示获取根元素下、a元素下、b元素下的多个d元素的id属性的值的集合
  5)如果要获取某个需要区分的元素下面的值,那么对于判断使用这样的语法:[属性名 =值],属性名和值都不需要加引号,比如:要想获取 id=”e1”的e元素下面的f元素的值,就是用这样的表达式:root/a/e$[id=e1]/f。
  4.4.5 此时配置管理模块中读取XML部分的结构示意如图
  4.5 组合模式
  4.5.1 面临的问题
  面临的问题分析前面解释器模式的实现,会发现对于客户端而言,并不想要去区分到底是非终结符对象还是终结符对象,只是想要以一个统一的方式来请求解析。该怎么解决这个问题呢?
  4.5.2 用组合模式来解决
  将对象组合成树形结构以表示“部分-整体”的层次结构。组合模式使得用户对单个对象和组合对象的使用具有一致性。
  4.5.3 使用组合模式来解决问题的思路
  事实上,解释器模式本身就已经应用了组合模式来设计了,比如上面的类实现中,ReadXmlExpression就相当于组合模式的Component,而 ElementExpression、ElementsExpression就相当于树枝对象,而 ElementsTerminalExpression、ElementTerminalExpression、 PropertyTerminalExpression、PropertysTerminalExpression就相当于树叶对象。
  对于解释器模式而言,只是负责按照抽象语法树进行解析,可是抽象语法树怎么来呢?就是谁来负责组合这棵树呢?
  那就需要我们自己来写一个解析器对象了,这个解析器对象就相当于解释器模式的客户端,在这个对象里面去组合抽象语法树。
  4.5.4 此时配置管理模块中读取XML部分的结构示意如图
  4.6 备忘录模式
  4.6.1 面临的问题
  在根据字符串来构建对应的抽象语法树的时候,有很多字符串前面都是一
  样的,这样一来,每次重复创建太浪费时间了,最好是相同字符串对应的树形对象就不用创建了,直接分析后面不同的,然后把新的部分添加到已有的这棵树里面就可以了。
  那么该怎么样来处理呢?
  4.6.2 用备忘录模式来解决
  在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可将该对象恢复到原先保存的状态。
  4.6.3 使用备忘录模式来解决问题的思路
  每次在拿到一个字符串过后,先跟已有的记录进行比较,找出最大的已经解析过的字符串,那么这个长度的字符串是不需要再解析的,只需要把后面没有解析的字符串拿出来进行解析,然后把解析的对象添加到已经解析好的这个对象后头就可以了。
  这就需要每次在解析的时候,每次解析完成,就向备忘录里面添加一条记录,而每次进入的时候,根据最长能匹配的字符串,从备忘录里面获取到相应的对象,这就不用解析了。
  当然,备忘录对象还是需要实现一个窄接口,也需要实现一个备忘录管理者。这里跟标准的备忘录模式有一个小小的区别,这里是在解析器对象里面直接去备忘录管理者里面获取备忘录对象,而不是从客户端来传递,这是为了让客户端操作更方便,其实由客户端传递也是可以的。
  4.6.4 此时配置管理模块中读取XML部分的结构示意如图
  4.7 原型模式
  4.7.1 面临的问题
  当我们把备忘录模式加入过后,就需要从备忘录里面获取字符串对应的对象,也需要在解析过后,把当前解析好的对象设置到备忘录里面,这里就出现了一个新的问题?那就是这些对象如果直接设置到备忘录里面,那么大家都是指向同一个内存空间,那么当解析中操作这些对象的时候,就会影响到备忘录中的对象。
  那么该怎么解决这个问题呢?
  4.7.2 用原型模式来解决
  用原型实例指定创建对象的种类,并通过拷贝这些原型创建新的对象。
  4.7.3 使用原型模式来解决问题的思路
  分析上面的问题,发现是需要把这些对象的数据设置到备忘录里面,因此一个简单的方法就是使用原型模式,通过克隆这些对象,这样既得到一个新的对象,而值又完全是一样的,这样一来,把这个新创建的对象设置到备忘录里面,大家就互不干扰了。
  同样的道理,要从备忘录里面取出这些对象的时候,也不是直接返回备忘录里面的对象,而是重新克隆一个对象,然后返回这个新的对象,从而保证备忘数据和解析操作的数据是相互独立的。
  4.8 生成器模式
  4.8.1 面临的问题
  面临的问题现在已经实现了通过一个字符串,就能够自动创建抽象语法树,然后通过解释器去解释执行,最终获取相应的数据的功能了。
  而且这个功能是通用的,语法也是比较简洁的,基本可以满足我们的功能要求了。但是新的问题又出现了,那就是要解析一个xml,需要拼接很多很多的字符串,而这些字符串的拼接过程是十分类似的,只是最终拼接后的结果不一样,那么有没有什么好方法,来让这些字符串的拼接过程变得简单、统一呢?
  4.8.2 用生成器模式来解决
  将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
  4.8.3 使用生成器模式来解决问题的思路
  每个字符串都是从根节点开始,依次拼接到最后需要取值的地方,因此可以把这个拼接的过程统一起来,做成生成器,而调用这些生成器来构建最终产品的客户端,这个时候就充当了生成器模式中的指导者。
  4.8.4 此时配置管理模块中读取XML部分的结构示意如图
  4.9 策略模式
  4.9.1 面临的问题
  现在字符串也可以通过Builder模式来拼接了,看起来一切很棒。又有一个新的问题出现了,那就是配置的数据里面有动态的内容,需要动态解析。有些朋友会说,哪里有动态的内容呢?
  由于在配置ExtendConf的值时,会发现经常需要在某一个配置里面,需要使用已经配过的一个值,这个值可能直接来之本模块其它的ExtendConf的值,也可能来自GenConf中配置的数据。因而设计了表达动态引用的语法,大致如下:
  1)直接引用本模块配置的其它ExtendConf的数据,有两种方式,如果只是简单引用某一个配置的值,那么就直接写成${引用的ExtendConf的id},示例如下:
   <ExtendConfs>
  <ExtendConf id="moduleName" isSingle="true">user</ExtendConf>
  <ExtendConf id="modulePackge" isSingle="true">cn.javass.${moduleName}</ExtendConf>
  </ExtendConfs>
  2)直接引用本模块配置的其它ExtendConf的数据,还有两种方式,x-gen支持Beanshell的脚本,并自动传入gm代表GenConfModel,mapEcms代表当前模块配置的 mapExtends,那么就可以写成${mapEcms.get(“引用的ExtendConf的id”);},示例如下:
   <ExtendConfs>
  <ExtendConf id="moduleName" isSingle="true">user</ExtendConf>
  <ExtendConf id="modulePackge" isSingle="true">cn.javass.${ mapEcms.get(“moduleName”);} </ExtendConf>
  </ExtendConfs>
  当然,Beanshell脚本更多的知识,请参见Beanshell随机文档。总之现在要解析包含动态内容的配置,而且有两种基本的解析方式,一种是直接替换当前模块内的配置数据,另外一种是用 Beanshell来运行配置的表达式,以获得最后的结果。那么在具体运行时,我们该到底使用哪一种方式来实现动态解析呢?
  4.9.2 用策略模式来解决
  定义一系列的算法,把它们一个个封装起来,并且使它们可相互替换。本模式使得算法可独立于使用它的客户而变化。
  4.9.3 使用策略模式来解决问题的思路
  很明显,上面描述的场景是策略模式适用的场景,两种解析动态内容的方
  式就是两种算法,而要判断到底该使用哪一种方式来实现动态解析,就可以在策略模式的上下文中进行就可以了。
  4.9.4 此时配置管理模块读取XML部分的结构示意如图
  
     上文内容不用于商业目的,如涉及知识产权问题,请权利人联系博为峰小编(021-64471599-8017),我们将立即处理。
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号