“Rule-O-Nator” - 运行时动态加载类的示例
在静态类型世界中保持动态。
更新
此解决方案基于较旧的技术(.Net 4 之前)。虽然它是接口和后期绑定的一个不错入门示例,但我强烈建议学习 MEF。请查看 Tim Corey 的文章 《从零到精通 MEF》 以了解更多信息。 要了解更多信息。
我工作的公司拥有并运营大型远洋船舶。美国海事薪资系统独树一帜。一名普通的健康海员一次出海就是 60 到 90 天。通信成本高昂,他们在港口之间几乎无法访问电子邮件和互联网。因此,我们会在船员在船时,根据他们的收入代为支付工资或发放预支款。我们支付汽车贷款、房贷,给他们的妻子、孩子和女朋友寄钱(后者往往意味着我们也要支付赡养费和子女抚养费!)除了基本工资外,他们还会获得加班费和值班费。有几种不同类型,都有工会协商的价格。这相当复杂。典型的薪资支票或工资凭证可能涵盖 30 到 90 天的薪资,直到他们离船或抵达特定港口才能拿到最终款项。凭证摘要可能长达两三页是很常见的。
“凭证检查器 1.0 版”应运而生。我下午随便搭建了一个小型实用工具,用于在办公室验证凭证。基本上,该应用程序会从薪资系统的数据库中收集所有工作小时数以及所有扣除项和费用(通信费用、采购、预支等),然后计算海员的凭证。如果凭证检查器计算出的金额与薪资系统计算出的金额不同,它会在凭证摘要报告中报告异常。随着时间的推移,我会根据需要进行修改,并在内部使用 ClickOnce 部署重新发布。这在办公室运行得很好。然后管理层希望将其推送到船队。我创建了一张安装 CD,然后将其发送给船队中的每一艘美国注册的船只。
一切都还好,直到年初 FICA 税率从 4.2% 回升到 6.2%。我只需更改雇主 FICA 税率并重新分发 EXE。问题是 EXE 文件大小超过 10 兆字节。修改后的 EXE 需要重新发送给船队中的每艘船,费用将超过 5000 美元。即使将 CD 发送到船舶的下一个停靠港也是很昂贵的。通常,如果我们必须由岸上航运代理亲自递送 CD,我们会支付超过 250 美元的费用才能将 CD 送上船。是时候想出更好的替代方案了。
解决方案
显而易见的出路是将规则移到一个单独的库中,然后可以修改这些规则,我们只需将新库发送给船队。文件大小很小(50K),我们可以直接通过电子邮件发送。不过,让我感到困扰的是,如果将来我只需要更改一条规则中的一个值,我仍然需要重新发送所有规则,那么如果管理层将来想添加更多规则呢?如何编写一个程序,允许你在未来编写一个未知的规则?
我需要做的是创建一个方法,以便在运行时动态加载我的规则。应用程序必须自动检测是否添加了新规则,并且必须这样做,而无需重新编译 EXE。
幸运的是,.NET 有一个叫做“接口”的概念。将接口想象成一个合同或协议,说明一个类将*如何*工作。它描述了方法、属性和事件,但不包含实现类的代码。我准备了一个简单的例子来演示这有多容易。
Rule-O-Nator
“Rule-O-Nator”是一个基于控制台的应用程序,它在运行时加载任意数量的规则,然后将这些规则应用于输入字符串,以确定规则是否通过或失败,并检索处理规则的详细结果。
这一切都始于接口。在示例解决方案中,您会找到一个名为*RuleTemplate*的项目。它包含一个名为*IRule.cs*的文件。
namespace RuleTemplate
{
// My Publicly Available Interface
// to be implemented by all rules.
public interface IRule
{
//Read Only Properties
string RuleName { get; }
bool Passed { get; }
string RuleDescrtiption { get;}
//methods
bool CheckRule(string Input);
string Results();
}
}
再次强调,接口的作用只是定义一个类必须实现的属性、方法和事件,才能*符合*接口的要求。我们希望接口 DLL 文件非常小,并且不希望接口被任何其他代码所拖累。关于这一点稍后会详细介绍。
好了,接下来呢???向你的解决方案添加另一个项目文件。添加对上述*RuleTemplate*的引用,然后添加一个新类。例如:
class ContainsBlue : IRule
{
}
Visual Studio 2012 有一个很棒的“作弊”快捷方式。如果你右键单击 `IRule`,然后点击“实现接口”>“实现接口”,VS 会自动为你构建类的骨架。你将得到一个空白结构,看起来像这样:
class ContainsBlue : IRule
{
public string RuleName
{
get { throw new NotImplementedException(); }
}
public bool Passed
{
get { throw new NotImplementedException(); }
}
public string RuleDescrtiption
{
get { throw new NotImplementedException(); }
}
public bool CheckRule(string Input)
{
throw new NotImplementedException();
}
public string Results()
{
throw new NotImplementedException();
}
现在,只需要构建规则。例如,此规则将检查输入并查看它是否包含单词“blue”。我通过将输入字符串转换为小写来实现不区分大小写。
public class ContainsBlue : IRule
{
bool? passed = null; //A local variable to maintain the Passed State
bool IRule.Passed
{
get
{
// A little monkeying around because I allow passed to be null
// so I can send different results if the rule hasn't been run.
return (passed == true);
}
}
string IRule.RuleName
{
get
{
return "Contains Blue Rule";
}
}
string IRule.RuleDescrtiption
{
get
{
return "This rule checks to see if input contains the word \"Blue\"";
}
}
bool IRule.CheckRule(string Input)
{
passed = Input.ToLower().Contains("blue");
return (passed == true);
}
string IRule.Results()
{
string rtn = "";
//passed is either null, true, or false
if (passed == null)
{
rtn = "The rule hasn't been run yet.";
}
else
{
if (passed == true)
rtn = "Yes, The input had blue in it.";
else
rtn = "No blue in the input.";
}
return rtn;
}
}
这是另一个计算句子中元音数量的规则
public class VowelCount : IRule
{
// Internally I'm using an int to determine
// if the rule has passed or run in this class
int vcount = -1;
bool IRule.Passed
{
get
{
return (vcount >= 0 );
}
}
string IRule.RuleName
{
get
{
return "Count The vowels";
}
}
string IRule.RuleDescrtiption
{
get
{
return "Counts the vowels in the input.";
}
}
bool IRule.CheckRule(string Input)
{
Regex r = new Regex("(a|e|i|o|u)", RegexOptions.IgnoreCase);
vcount = r.Matches(Input).Count;
return (vcount >= 0);
}
String IRule.Results()
{
string rtn = "";
//Passed is based on vcount in this rule
if (vcount < 0 )
{
rtn = "The rule hasn't been run yet.";
}
else
{
if (vcount > 0 )
rtn = "Yes, The input had " + vcount.ToString() + " vowels in it.";
else
rtn = "No vowels in the input.";
}
return rtn;
}
}
这里有两点需要注意:虽然 `Passed` 属性在接口中作为布尔值发布,但内部我可以以任何方式实现它。第二,这个规则与上面的 BlueRule 所做的完全不同。Blue 查找输入中的一个单词,Vowels 返回元音的数量。
zip 文件中有更多规则,不过,我认为你已经明白了规则的样子。
那么我如何加载规则呢?
项目中还有一个库叫做*RulesFactory*。规则工厂在运行时加载 DLL,扫描 DLL 中的规则,并将它们加载到字典集合中。
public class RuleFactory
{
public Dictionary<string, IRule> Rules = new Dictionary<string,IRule>();
public void LoadRules(string DllPath)
{
// Load the DLL asmebly
Assembly asm = Assembly.LoadFrom(DllPath);
var DllRuleSet = from t in asm.GetTypes()
where t.GetInterfaces().Contains(typeof(IRule))
select Activator.CreateInstance(t) as IRule;
foreach (IRule RuleInst in DllRuleSet)
{
Rules.Add(RuleInst.RuleName, RuleInst);
}
}
}
应用程序将所有内容粘合在一起
现在我们应用程序所要做的就是:
- 加载所有规则
- 用规则检查字符串,然后
- 显示结果
这是一个基于控制台的示例
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using RuleTemplate;
namespace RuleONator
{
class Program
{
static void Main(string[] args)
{
Console.ForegroundColor = ConsoleColor.White;
Console.WriteLine("This is an example of dynamically " +
"Loaded classes using a common interface \r\n");
//Create a Rule Factory to load the Rules
RuleFactory f = new RuleFactory();
//I chose to put all of my rule DLLs
//in a subdirectory in the debug folder for this example
//Now I can use Getfiles to load up all the DLLs,
//and it will load up and dlls I write
//in the future.
string[] dllfiles = System.IO.Directory.GetFiles(".\\Rules\\", "*.dll");
foreach (string filename in dllfiles)
{
Console.WriteLine("Loading " + filename);
f.LoadRules(filename);
}
//That's done, what rules did we load up?
Console.WriteLine("List of Loaded Rules:");
foreach (var entry in f.Rules)
{
Console.WriteLine(" " + entry.Key);
}
Console.WriteLine("Processing Rules:\r\n");
//Skip to the Process Rules to see how we do this:
ProcessRules(f, "This is a string with the word BLUE in it.");
Console.WriteLine("");
ProcessRules( f, "No Color");
Console.Write("\r\nPress Any Key To Continue..."); Console.ReadKey();
}
static void ProcessRules(RuleFactory f, string Input)
{
Console.WriteLine("Processing ProcessRules against the folowing string:");
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine( "\"" + Input + "\"");
Console.ForegroundColor = ConsoleColor.White;
// The rules are loaded up in the RuleFactory Dictionary.
// All we have to do is run each rule against the input
// and display the results.
foreach (KeyValuePair<string, IRule> Rule in f.Rules)
{
//Because we're using an interface, you can refrence the properties
//methods and events
Console.WriteLine(" Rule: " + Rule.Value.RuleName);
bool result = Rule.Value.CheckRule(Input);
if (result == true)
Console.ForegroundColor = ConsoleColor.Green;
else
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine(" Results: " + Rule.Value.Results());
Console.ForegroundColor = ConsoleColor.White;
}
}
}
}
运行时的输出看起来有点像:
List of Loaded Rules:
Contains Blue Rule
How Big Is It?
Count The vowels
Processing Rules:
Processing ProcessRules against the folowing string:
"This is a string with the word BLUE in it."
Rule: Contains Blue Rule
Results: Yes, The input had blue in it.
Rule: How Big Is It?
Results: Yes, It's bigger than 10 characters. Infact it's 42 chacaters.
Rule: Count The vowels
Results: Yes, The input had 11 vowels in it.
Processing ProcessRules against the folowing string:
"No Color"
Rule: Contains Blue Rule
Results: No blue in the input.
Rule: How Big Is It?
Results: Nope it's only 8 chacaters.
Rule: Count The vowels
Results: Yes, The input had 3 vowels in it.
Press Any Key To Continue...
只是带有漂亮的颜色;o)
结论
关于接口,你需要知道的最重要的一点是:一旦你创建了接口并将其部署到应用程序中,你就不能更改它们,否则需要更新所有使用它们的对象并重新编译整个应用程序。一旦你部署了你的应用程序,*永远*不要*再*更改接口*。如果你真的必须更改接口,最好是创建一个新的接口并在你的下一个版本中部署它。这是*Microsoft*关于此的说法,如果你想要更多信息。
最后,请善待。这是我的第一个 C# 帖子,我期待任何建设性的改进建议。