65.9K
CodeProject 正在变化。 阅读更多。
Home

模板消息框架

emptyStarIconemptyStarIconemptyStarIconemptyStarIconemptyStarIcon

0/5 (0投票)

2015年8月5日

CPOL

11分钟阅读

viewsIcon

18454

downloadIcon

112

框架有助于在运行时解析文本中的模板代码。

Content

引言

在开发领域,我们时常会遇到处理消息的业务需求,无论这些消息是短信、电子邮件、Word文档、PDF、报告等。大多数时候,设计出来的解决方案考虑不周、难以维护且硬编码。普通开发者通常会直接在代码中硬编码文本以满足业务需求。由于没有人审查代码,这就会成为生产代码的一部分,并一直存在,直到代码审查迭代发现它。

搜索标准解决方案时,建议采用模板代码风格的消息,这能提供最佳解决方案。模板代码确实可以发挥重要作用,因为它们易于维护且与代码完全隔离。它们有巨大的潜力作为构建大型生产规模消息应用程序的基础。本文最初解释了开发人员解决问题的方式,然后对同一代码进行优化迭代,直到最终发展成完整的生产规模库。

问题

大多数开发者通常会采用以下直接的方法来实现需求。

static void Attemp1()
{
    XmlDocument xmlDocument = new XmlDocument();
    StringBuilder sb = new StringBuilder();

    string xmlData = "<d>" +
                        "<Customers>" +
                           "<Customer Name='Irfan'/>" +
                               "<Orders Total='99999.99'>" +
                                   "<Order No='1' Type='ABC' Amount='1111'/>" +
                                   "<Order No='2' Type='DEC' Amount='222'/>" +
                               "</Orders>" +
                        "</Customers>" +
                      "</d>";
    xmlDocument.LoadXml(xmlData);

    sb.AppendLine(string.Format(@"Dear {0},", xmlDocument.SelectSingleNode(@"/d/Customers/Customer/@Name").Value));
    sb.AppendLine("");
    sb.AppendLine("This is to let you know that your following Orders have been confirmed.");
    sb.AppendLine("");
    sb.AppendLine("Order Details are ;");
    sb.AppendLine("Order Number                 Order Type              Order Amount");
    XmlNodeList orderList = xmlDocument.SelectNodes(@"/d/Customers/Orders/Order");
    foreach (XmlNode node in orderList)
    {
        sb.AppendLine(string.Format(@"{0}                            {1}                      {2}",
            node.Attributes["No"].Value, node.Attributes["Type"].Value, node.Attributes["Amount"].Value));
    }
    sb.AppendLine("");
    sb.AppendLine("Thanks for doing business with us.");
    sb.AppendLine("");
    sb.AppendLine("Regards,");
    sb.AppendLine("Team");
    string outString = sb.ToString();
}

这种方法确实能产生结果,可以轻松地将结果通过电子邮件发送给相应的用户。

如果你仔细观察上述方法,你会确信该方法存在一个严重的缺陷,它根本无法维护。该方法不给业务用户任何灵活性来添加/更改消息内容,因为消息是硬编码的,需要完整的开发周期。代码中的硬编码消息是严重的罪过,应该受到惩罚,让整个开发团队买单。

其次,如果业务用户要求在消息中包含一个新的列 **订单状态** 怎么办?这将意味着更改文本、更改数据以及更改代码来理解新字段,因此需要完整的开发周期。

理想的方法是构建一个机制,允许业务用户随时更改内容,而无需进入完整的开发周期。

解决方案

如果分析上述方法,我们可以找到构成这个“菜肴”的三种“食材”。

  • 静态内容,例如“感谢您与我们合作”,这些内容不会改变。
  • 可能改变的内容数据,例如客户姓名和订单详情。
  • 将内容与内容数据关联的机制。

迈向解决方案的第一步是将硬编码的消息与代码隔离开。消息应放置在别处,可能在数据库(通常是这种情况)或文件系统中。

第二步自然是识别会改变的内容与不会改变或本质上是静态的内容。根据标准实践,我们需要对消息进行模板化,以清楚地区分静态内容和动态内容。例如,在我们的例子中,**Irfan** 是客户的姓名,是动态的,因此我们可以在消息中使用 **<CUSTOMER_NAME>**。模板化的消息版本将是这样的:

现在可以看到使用基于模板的消息的好处,即消息可以轻松地与代码分离。让我们回顾一下之前的代码并修改它以使用上面的模板,看看我们改进了多少。

第一轮迭代

如下详细介绍,我们在上述代码中做了一些更改。消息内容现在移到了一个单独的文件 TextTemplate2.txt 中。同样,客户数据也已放置在一个单独的文件 Data2.xml 中。数据和模板现在都已隔离。

static void Attemp2()
{
    XmlDocument xmlDocument = new XmlDocument(), dom = new XmlDocument();

    string message = System.IO.File.ReadAllText(@"TextTemplate2.txt");
    string xmlData = System.IO.File.ReadAllText(@"Data2.xml");
    xmlDocument.LoadXml(xmlData);

    message = message.Replace(@"<CUSTOMER/>", xmlDocument.SelectSingleNode(@"/d/Customers/Customer/@Name").Value);
    Regex regEx = new Regex(string.Format(@"<{0}>(.*\s)*</{0}>", "ORDERS"), RegexOptions.IgnoreCase | RegexOptions.Multiline);
    Match match = regEx.Match(message);
    string templateString = match.Value;

    string holdString = string.Empty;

    XmlNodeList orderList = xmlDocument.SelectNodes(@"/d/Customers/Customer/Orders/Order");

    foreach (XmlNode node in orderList)
    {
        holdString += templateString;
        holdString = holdString.Replace(@"<ORDERS>", string.Empty);
        holdString = holdString.Replace(@"<ORDER_NUMBER/>", node.Attributes["No"].Value);
        holdString = holdString.Replace(@"<ORDER_TYPE/>", node.Attributes["Type"].Value);
        holdString = holdString.Replace(@"<ORDER_AMOUNT/>", node.Attributes["Amount"].Value);
        holdString = holdString.Replace(@"</ORDERS>", string.Empty);
    }

    message = regEx.Replace(message, holdString);
}

正如预期的那样,我们获得的最大好处是,至少业务用户现在可以在生产环境中轻松更改静态内容,而无需任何帮助。更改文本无需开发。此外,代码现在更加整洁,只处理模板代码及其替换逻辑。添加新列现在稍微容易一些,我们只需要修改 Data.xml 并为 for each 循环添加一行,仅此而已,但对于所有此类实例仍然需要开发周期。

上述代码中棘手的部分是 **<ORDERS>** 代码。ORDERS 代码是可重复的代码,会根据订单的数量重复。

<ORDERS>
    <ORDER_NUMBER/>                   <ORDER_TYPE/>                      <ORDER_AMOUNT/>
</ORDERS>

在我们的例子中有两个订单,因此它会重复两次。

  ORDER_NUMBER ORDER_TYPE ORDER_AMOUNT
订单 1 1 ABC 1111
订单 2 2 DEC 2222

正则表达式是一个强大的工具,有助于搜索和替换特定模式的内容。RegEx 可以以类似的方式用于识别从开始到结束的重复代码。例如,在下面的消息中,可以使用 RegEx 轻松识别 ORDERS 标签内的内容。一个简单的 regex 可以是 **<ORDERS>(.*\s)*</ORDERS>**,它返回 ORDERS 标签之间的静态内容。

一旦我们获得了模板内容,重复它就会很容易,正如代码中所述。代码的第一轮迭代会产生相同的文本,但方式更优雅。但我们离目标还很远。接下来我们需要关注的是将代码与数据关联起来的机制。在第二轮迭代中,我们将泛化数据获取机制。

第二轮迭代

再次回到分析阶段,如果我们看一下解析模板代码的方式,我们会注意到我们正在使用 XPATH 进行模板解析。XPATH 的优点是它非常强大,可以覆盖复杂的情况。坦率地说,选择 XML 作为底层数据源是有原因的,原因当然是 XPATH。XPATH 最重要的属性是它可以与代码隔离。是的,我们可以将 XPATH 放在一个单独的文件中,从而泛化模板解析机制。我们可以创建一个元配置 XML,它将所有模板代码与其 XPATH 形式的解析函数集中在一起。一个示例 metaconfiguration.xml 可以是这样的:

<?xml version="1.0" encoding="utf-8" ?>
<ROOT>
  <CODE NAME="CUSTOMER_NAME"
        MULTIPLERESULT="FALSE"
        DATAFUNCTION="/d/Customers/Customer/@Name"/>
  <CODE NAME="ORDERS"
        MULTIPLERESULT="TRUE"
        DATAFUNCTION="/d/Customers/Customer/Orders/Order/@No"/>
  <CODE NAME="ORDER_NUMBER"
        MULTIPLERESULT="FALSE"  
        DATAFUNCTION="/d/Customers/Customer/Orders/Order[@No='{0}']/@No"/>
  <CODE NAME="ORDER_AMOUNT"
        MULTIPLERESULT="FALSE"
        DATAFUNCTION="/d/Customers/Customer/Orders/Order[@No='{0}']/@Amount"/>
  <CODE NAME="ORDER_TYPE"
        MULTIPLERESULT="FALSE"
        DATAFUNCTION="/d/Customers/Customer/Orders/Order[@No='{0}']/@Type"/>
</ROOT>

上述 XML 是一个元配置,它充当所有模板代码及其解析函数的数据库。例如,CUSTOMERS 代码将通过在下面的 XML 数据源上应用 XPATH “/D/CUSTOMERS/CUSTOMER/@ID” 来解析。

<d>
  <Customers>
    <Customer Name='Irfan'>
      <Orders Total='99999.99'>
        <Order No='1' Type='ABC' Amount='1111'/>
        <Order No='2' Type='DEC' Amount='222'/>
      </Orders>
     </Customer>
  </Customers>
</d>

 所有三个要素:模板代码、客户数据以及代码与数据之间的链接都已与代码分离,从而使我们能够完全按照我们想要的方式进行操作。让我们再次修改我们的代码以整合这项新学的技术,并看看它如何影响我们的代码。

仅为说明目的,我们可以用伪代码的形式写出这个概念。

Begin
    Var  message = Read Template Message
    Var CustomerDataXml =  Read CustomerData Xml
    Var MetaConfiguratio = Read MetaConfiguration.xml
    Transform the message into a temporary xml string by following below rule <root> + message + </root>
    Load Xml String in the XmlDocument
    Var XmlNodeList =  Get All Xml Nodes by applying XPath(@"/root/*")
    For each TemplateCode in XmlLNodeList
    Begin
        Var TempCodeXPath = MetaConfiguration[TemplateCode]
        Var TempCodeValue = ResolveXPath(TemplateCode, CustomerDataXml,
                                         MetaConfigruationXml,TempCodeInnerText);
        message = Replace TemplateCode in Message with TempCodeValue
    End
    Print Message
End

这个概念非常简单,并且利用了 XPath 的强大功能。让我们通过重新审视之前的代码来实现这个概念,看看它的效果。

static void Attemp2()
{
    XmlDocument xmlDocument = new XmlDocument(), dom = new XmlDocument();
    Dictionary<string, Tuple<string, string>> metaConfiguration = new Dictionary<string, Tuple<string, string>>();

    XmlNodeList nodeList = null;

    string message = System.IO.File.ReadAllText(@"TextTemplate2.txt");
    string xmlData = System.IO.File.ReadAllText(@"Data2.xml");

    metaConfiguration = ReadMetaConfiguration();

    xmlDocument.LoadXml(xmlData);

    string xmlSourceMessage = string.Format("<{0}> + {1} + </{0}>", "root", message);

    dom.PreserveWhitespace = true;
    dom.LoadXml(xmlSourceMessage);

    nodeList = dom.SelectNodes(@"/root/*");

    foreach (XmlNode XNode in nodeList)
    {
        string resolvedTagValue = Resolve(XNode.Name, xmlDocument, metaConfiguration, XNode.InnerXml);
        message = message.Replace(XNode.OuterXml, resolvedTagValue);
    }
}

Dictionary 是用于快速查找模板代码的理想数据结构。下面的代码片段读取 xml 文件并将数据转换为 Dictionary,以消除模板代码查找的复杂性。

static Dictionary<string, Tuple<string, string>> ReadMetaConfiguration()
{
    Dictionary<string, Tuple<string, string>> dictionary = new Dictionary<string, Tuple<string, string>>();

    XmlDocument metaXmlDocument = new XmlDocument();
    string metaData = System.IO.File.ReadAllText(@"MetaData.xml");
    metaXmlDocument.LoadXml(metaData);
    XmlNodeList nodeList = metaXmlDocument.SelectNodes(@"/ROOT/CODE");

    foreach (XmlNode xmlNode in nodeList)
    {
        string name = xmlNode.Attributes["NAME"].Value;
        string dataFunction = xmlNode.Attributes["DATAFUNCTION"].Value;
        string multiResult = xmlNode.Attributes["MULTIPLERESULT"].Value;
        dictionary[name] = new Tuple<string, string>(multiResult, dataFunction);
    }

    return dictionary;
}

实际的模板代码解析逻辑是以递归方式定义的。该函数以递归方式识别消息中的模板代码,使用 CustomerData xml 解析代码,并将模板代码替换为值。

static string Resolve(string code, XmlDocument dataSource, Dictionary<string, Tuple<string, string>> metaConfiguration, string dataContext)
{
    string outString = string.Empty;
    Tuple<string, string> codeAttrib = metaConfiguration[code];
    string xPath = string.Format(codeAttrib.Item2, dataContext);

    XmlNodeList nodeList1 = dataSource.SelectNodes(xPath);

    if (nodeList1.Count == 1)
    {
        outString = nodeList1[0].Value;
    }
    else if (nodeList1.Count > 1)
    {
        string multiResult = codeAttrib.Item1;
        string innerXmlMessage = string.Format("<{0}> + {1} + </{0}>", "root", dataContext);
        XmlDocument innerDocument = new XmlDocument();
        innerDocument.LoadXml(innerXmlMessage);
        string origInnerMessage = dataContext;
        XmlNodeList innerNodeList = innerDocument.SelectNodes(@"/root/*");

        foreach (XmlNode node in nodeList1)
        {
            outString = outString + origInnerMessage;
            foreach (XmlNode XNode in innerNodeList)
            {
               string resolvedTagValue = Resolve(XNode.Name, dataSource, metaConfiguration, node.Value);
               outString = outString.Replace(XNode.OuterXml, resolvedTagValue);
            }
        }
    }
    return outString;
}

上述代码片段将使我们在生产环境中完全灵活地更改消息。我们不仅可以更改消息的静态文本,现在还可以为消息添加新的模板代码。所需要的只是 Meta Configuration Xml 中的一个新模板代码,以及 Customer Data Xml 中对应的 XPath,仅此而已。

为了便于理解,我们不会停止我们的优化周期。仍然存在上述方法无法解决的更复杂的情况。让我们也将消息复杂化,转向 HTML,因为在现实生活中文本消息几乎已经过时了。

<html>
<body>

<h1 align='center'>Sample Report</h1>

<div id="accordion">

       <CUSTOMERS>
       <h3><CUSTOMER_NAME/></h3>
       <div>
              <table id="t01">
                <tr>
                  <th>Order Number</th>
                  <th>Order Amount</th>        
                  <th>Order Type</th>
                </tr>

                <CUSTOMER_ORDERS>
                <tr>
                  <td><ORDER_NUMBER/></td>
                  <td><ORDER_AMOUNT/></td>            
                  <td><ORDER_TYPE/></td>
                </tr>

                </CUSTOMER_ORDERS>
              </table>
       </div>
       </CUSTOMERS>
</div>
</body>
</html>

如果您仔细查看上述消息,您会注意到模板代码有两层深度。

<CUSTOMERS>
      <CUSTOMER_NAME>
          <CUSTOMER_ORDERS>
          <ORDER_NUMBER/>
          <ORDER_AMOUNT/>
          <ORDER_TYPE/>
      </CUSTOMER_ORDERS>
</CUSTOMERS>

Customers 模板代码代表客户数量,对于每个客户,他们的 orders 标签将被评估。原始模板代码形式将是这样的:

客户

Order

<ORDER_NUMBER/>                            

<ORDER_AMOUNT/>

<ORDER_TYPE/>

Asif

1

1

1111

ABC

2

2

2222

DEC

Bharat

3

3

3333

GHI

4

4

4444

JKL

如果我们开始添加迭代标签,增加深度,情况的复杂性就会增加。这说明还需要一次优化迭代。

第三轮迭代 – 构建消息库

现在我们已经到了这样一个地步,一个专门的消息库似乎是一个好选择,而且很明显。在所有项目中做重复的工作是很痛苦的,因为消息无论是电子邮件还是报告,都是任何系统的组成部分。使用独立的库可以显著减少开发工作量,并通过版本管理提供可维护性。

遵循标准实践,该库也将公开接口。

这个想法是拥有一个框架,作为标准业务应用程序的构建块,并且偏向于银行应用程序。Dilizity 是这个想法的具体体现。目前,该库只涵盖了消息的模板化部分。很快我们将为框架添加更多层,如数据访问、通信、表示、审计、报告、日志记录、安全等。让我们简要地看一下消息库的每个组件。

  • IDataContext: 由于模板代码的层次结构,我们需要一种机制来构建堆栈,将上下文传递给对上下文敏感的模板代码。例如,在下面的情况下,**<CUSTOMER_NAME>** 是对上下文敏感的,因此我们需要将上下文传递给该标签,以识别我们在此引用的是哪个客户。
    <CUSTOMERS>
          <CUSTOMER_NAME/>
    <CUSTOMERS>
  • IMessagingDataAgent: 底层的客户数据可以以任何形式存在,可以是 XML、表格或其他任何形式。这里需要一个接口来隐藏数据表示的内部机制。
  • ITagAgent: 每个标签都可以有自己的解析科学,并且其语义也是如此。我们已经使用 XML 元素作为标签;未来版本可以有其他表示标签的方式。因此,这里需要一个接口来隐藏标签的表示和解析的内部机制。
  • IMessagingAgent: 这是核心接口,负责模板代码解析。消息可以有许多版本;消息可以是简单的文本、HTML、Word 文档、RTF、SMS、报告等形式。如果需要,每个版本都可以有自己的模板解析方式,因此需要一个接口来隔离不同版本的实现。

代码遵循在不同迭代中阐述的相同原则,因此我们无需重复解释。但是,我们确实想展示处理上面使用的 HTML 模板后的最终结果;但我们确实想展示元配置、客户数据和模板。

我们使用了以下元配置:

以及以下客户数据:

<D>
  <CUSTOMERS>
    <CUSTOMER ID="0001" NAME='SYED ASIF IQBAL'/>
    <ORDERS TOTAL='99999.99'>
      <ORDER ID='1' TYPE='MOBILE' AMOUNT='2000.00'/>
      <ORDER ID='2' TYPE='LEDTV' AMOUNT='50000.00'/>
    </ORDERS>
    <CUSTOMER ID="0002" NAME='BHARAT GOSWAMI'/>
    <ORDERS TOTAL='99999.99'>
      <ORDER ID='3' TYPE='SHOE' AMOUNT='1500.00'/>
      <ORDER ID='4' TYPE='WATCH' AMOUNT='2000.00'/>
    </ORDERS>
  </CUSTOMERS>
</D>

最终结果是:

关注点

使用上述规则,如果一个人想避免 XML 和 XPath,也可以使用数据库中的表来实现目标。元配置是关键要素,目前存储为 XML,可以移至数据库表。使用数据库表作为源有其优点和缺点。通常,开发者发现使用 UPDATE SQL 进行修改很方便。在集群环境中,与集群中所有服务器上的文件系统更改相比,数据库更改很容易。同样,在企业环境中,数据库的单个更新会经历变更管理周期,与文件系统更改相比,其难度相对较大。

让我们看看如果选择使用数据库作为元配置源,我们会预见到哪些变化。

表格中的配置源

第一步是创建一个名为 META_CONFIGURATION 的数据库表。该表应包含 NAME、MULTIPLERESULT 和 DATA_FUNCTION 字段。源数据可以以 INSERT SQL 脚本的形式填充。

第二步是添加一个数据访问层来访问数据库中的表。最后一步是读取表并像之前处理 XPath 一样处理 XPath。

这些是从 XML 元配置切换到基于表的元配置所需的最小更改。但是,将整个基于 XPath 的机制更改为更本地化的数据库内容呢?如何将 XPath 更改为 SQL 或某些标量函数?采用基于 SQL 的策略将使库更加健壮。对此的思考可以如下:这将是消息库下一个主要升级的起点。

历史

第一个版本

© . All rights reserved.