模板消息框架
框架有助于在运行时解析文本中的模板代码。
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 的策略将使库更加健壮。对此的思考可以如下:这将是消息库下一个主要升级的起点。
历史
第一个版本