程序员创业指南与企业应用构建 - 第 6 篇
商业开源许可和业务规则引擎。
引言
这是我讲述创办 SplendidCRM Software, Inc. 系列专栏的第六篇。我希望我的创业经验能够激励您。在我看来,创建一家公司可以是一次美妙的冒险。
第六篇文章
在 SplendidCRM,我们正在更改用于社区版的许可证,而这个事件似乎是解释我们许可策略的好时机。首先,我想明确一点,SplendidCRM Software 是一家企业,和大多数企业一样,我们营利。虽然这个说法与开源文化有些不符,但我们并不是第一个进入商业开源领域的人,也不会是最后一个。商业开源普遍存在的问题是,它违背了源代码是软件企业关键知识产权、软件企业家应不惜一切代价保护它的普遍商业理念。
在我解释我们新的许可方法之前,提供一些历史可能很有帮助。SplendidCRM 最初是基于 Microsoft 技术栈对 SugarCRM 的重新实现。我们使用“重新实现”而不是“移植”这个词,是因为我们在 SplendidCRM 产品中没有使用 SugarCRM 的任何源代码。当时,PHP 尚未面向对象,移植 PHP 并不可行。但我们重新实现该软件的真正原因是我们拥有使用 Microsoft 企业软件模式的丰富经验,我们认为这些模式远优于原始 SugarCRM 开发人员使用的设计。区分 SplendidCRM 和 SugarCRM 的一个关键模式是使用 SQL 对象,如存储过程和视图,来实现业务逻辑。我曾在之前的文章中讨论过存储过程的使用,但在此提及是因为它们代表了我们认为是知识产权基础的源代码。
在过去 5 年里,我们发布了 4 个主要版本的 SplendidCRM。我们的许多 ASP.NET 源代码都已在 SugarCRM 公共许可证(http://www.sugarcrm.com/crm/SPL)下发布,但我们一直采用双重许可方法,这在商业开源供应商中很常见。SugarCRM 公共许可证源自 Mozilla 公共许可证,主要区别是将“Mozilla”一词替换为“SugarCRM”。此时,您可能想知道为什么我们不遵循相同的方法创建 SplendidCRM 公共许可证。原因有两个。第一个是我们使用了原始的 SugarCRM 图像、图标和样式表,因此我们在法律上有义务将 SugarCRM 公共许可证与这些文件一起包含。第二个原因是纯粹为了营销目的。虽然 SugarCRM 公共许可证未授予使用 SugarCRM 商标的许可,但我们认为我们可以通过始终在许可归属的上下文中来使用其商标。SugarCRM 通过创建 SugarCRM 公共许可证有效地迫使我们使用其商标。
需要注意的是,直到现在,我们都没有在社区版中发布任何 SQL 源代码。我们一直分发加密版本的 SQL 代码,并将数据库中的 SQL 对象保持加密状态。我们这样做有两个原因:第一个是为了让我们的客户有购买专业版的商业理由,该版本包含所有 SQL 源代码。第二个原因是为了防止 SugarCRM 窃取我们的核心知识产权并用于其产品。现在 5 年过去了,SugarCRM 的动态 SQL 设计对他们来说运作良好,而 SplendidCRM 的 SQL 对象设计对我们来说运作良好,这一点已经很清楚了。因此,我们不再担心 SugarCRM 会对我们的 SQL 代码感兴趣,尽管他们也许能从中学习一些东西,比如如何正确地将数据库从旧版本应用程序升级。
2007 年 7 月,SugarCRM 发布了使用 GPLv3 许可证的社区版。表面上看,这似乎是一份慷慨的礼物,但我们采取了更以商业为中心的观点,将其视为一种知识产权保护。SugarCRM 迁移到 GPL 实际上阻止了像 SplendidCRM Software 这样的公司使用该软件的任何文件,甚至包括图像或图标。引用史蒂夫·鲍尔默(Steve Ballmer)的话来说,GPL 许可证的问题在于,它像病毒一样感染使用该软件包任何部分的软件。因此,虽然我们可以自由使用 SugarCRM 公共许可证下的任何单个文件,但如果我们使用 GPL 许可证下的任何内容,我们将立即被要求在 GPL 许可证下发布所有源代码,包括 SQL 对象。
我们认为 SugarCRM 迁移到 GPLv3 许可证是为了阻止竞争对手使用他们的任何代码。我们的理论是,任何营利性企业都不会想创建一个派生产品,其中包含要求所有新知识产权也必须公开的要求。此时,您可能会认为 SugarCRM, Inc. 也是如此,例如,他们现在需要向公众发布他们所有的专业版和企业版源代码。但这就是作为软件供应商的好处:您可以根据多个许可证发布您的源代码,而无需担心一个许可证会影响另一个。虽然开放软件基金会鼓励所有供应商在其所有代码下发布公开许可证,但他们并不介意公司根据多个许可证或双重许可证发布相同的源代码。
2010 年 4 月,SugarCRM 发布了 AGPLv3(简称 AGPLv3)的 6.0 社区版。转向 AGPLv3 进一步保护了他们的代码,使其免受那些定制社区版但不重新分发的竞争对手的侵害。您看,GPLv3 和 AGPLv3 的关键区别在于,只要供应商不“分发”更改,他们就不需要共享 GPLv3 项目的派生作品。因此,虽然 SugarCRM 的高层可能想让你相信他们只是想确保社区版的源代码被所有人共享,但我们认为他们的目标是阻止一切形式的竞争。
此时,您可能会认为我们只是一个心怀不满的竞争对手,抱怨我们再也不能从主要竞争对手那里窃取东西了,但事实绝非如此。我们对 SugarCRM 的管理团队非常尊敬。我们已经密切关注他们 5 年了,我们看到了他们商业实践的成功。出于显而易见的原因,我们试图采用相同的商业实践,希望取得同样的成功。主要区别在于,我们的目标受众是倾向于 Microsoft 软件的公司,而 SugarCRM 的目标受众是不喜欢 Microsoft 软件的公司。
现在您知道了 SplendidCRM 和 SugarCRM 的软件许可历史,我们正在以 AGPLv3 许可证发布 SplendidCRM 5.0 社区版也就不足为奇了。我们这样做是充分认识到,这可能会阻止任何商业公司将 SplendidCRM 5.0 社区版作为其服务的基石。此时,需要指出的是,我们有一个使用完全不同软件许可的合作伙伴计划,允许我们的合作伙伴创建定制服务,而无需共享其知识产权。
您可能会想,如果我们如此努力地阻止竞争对手使用源代码,为什么还要发布它呢?首先,源代码对那些需要强大且可定制的 CRM 的企业来说是一个营销工具。由于这些企业通常需要定制 CRM,他们通常希望查看源代码,以确定其质量是否达到了所需的水平,以及设计模式是否能适应他们现有的环境。发布源代码的第二个原因是与开发者分享我们的经验。我们不能分享一切,因为那样的话企业就没有购买的理由了,但我们可以分享一些我们的关键设计模式和精彩的实现。
说到精彩的东西,通过 SplendidCRM 5.0,我们将 Microsoft 的业务规则引擎集成到了我们所有的版本中,包括社区版。业务规则通常是我们只包含在专业版或企业版中的内容,但这个解决方案如此简单优雅,我们忍不住要与世界分享。
业务规则引擎
什么是业务规则?在 CRM 的上下文中,“业务规则”是一种非程序员定义条件和应用某些逻辑的方式。举个例子可能更容易解释。假设您正在重新安排您的销售区域,需要将加利福尼亚州的所有客户从区域 1 重新分配到区域 2。您将创建一个规则,使用条件(STATE = 'California' or STATE = 'CA'
)并应用逻辑(REGION = 'Region 2'
)。Scott Allen 在 Rules and Conditions 一文中有很好的介绍,网址是 http://odetocode.com/Articles/458.aspx。
规则非常棒且功能强大,但作为程序员,您如何让最终用户应用这种逻辑?如何在不违反应用程序安全模型的情况下应用这种逻辑?这就是 Microsoft Rules Engine 的作用。根据 Google 搜索结果的稀少程度,很明显 Microsoft Rules Engine 的使用率不高。这很可惜,因为它非常强大且易于使用。Microsoft Rules Engine 随 Windows Workflow Foundation 一起提供,但 Workflow Engine 很复杂,而 Microsoft Rules Engine 却很简单。
SplendidCRM 在四个区域使用规则引擎。第一个区域称为 Rules Wizard(规则向导),它为最终用户提供了一种将规则应用于特定模块所有记录的方法。第二个区域称为 Business Rules(业务规则),这是一个仅限管理员的功能,允许将规则应用于 EditView
、DetailView
或 GridView
,以便将规则应用于用户界面。第三个区域是 Reporting Engine(报表引擎),在该区域中,规则可以在报表渲染之前应用于结果集。第四个区域是 Workflow Engine(工作流引擎),其中字段可以包含计算。Rules Wizard 是最容易描述的区域,因此本文将重点介绍 Rules Wizard。Rules Wizard 将规则应用于一组记录,因此第一步是创建一个数据表并填充记录。以下代码在 SplendidCRM 中很常见。它可能比您习惯的要复杂一些,因为我们使用通用的 ADODB.NET 接口,以便同一代码可以在多个数据库提供商之间工作。
DbProviderFactory dbf = DbProviderFactories.GetFactory();
using ( IDbConnection con = dbf.CreateConnection() )
{
string sSQL;
sSQL = "select * vwACCOUNTS";
using ( IDbCommand cmd = con.CreateCommand() )
{
cmd.CommandText = sSQL;
using ( DbDataAdapter da = dbf.CreateDataAdapter() )
{
((IDbDataAdapter)da).SelectCommand = cmd;
using ( DataTable dtCurrent = new DataTable() )
{
da.Fill(dtCurrent);
}
}
}
}
我将跳到最后,向您展示处理数据表中每行规则的循环。最需要注意的是以下代码中使用了名为 SplendidWizardThis
的特殊对象(我们将该对象称为 This
对象)。规则需要应用于某个东西,而我们的特殊对象就是规则引擎将要修改的那个东西。规则引擎只是将规则应用于 This
对象,然后您就可以根据需要处理结果。在后台,我相信规则引擎会使用反射来确定它可以对 This
对象做什么或不能做什么。This
对象的使用也很重要,因为它建立了一个包含规则引擎的沙箱。拥有沙箱很重要,因为您不希望给黑客破坏数据库的能力。
RuleValidation validation =
new RuleValidation(typeof(SplendidWizardThis), null);
RuleSet rules = RulesUtil.BuildRuleSet(dtRules, validation);
foreach ( DataRow row in dt.Rows )
{
SplendidWizardThis swThis = new SplendidWizardThis(row);
RuleExecution exec = new RuleExecution(validation, swThis);
rules.Execute(exec);
}
以下代码是我们特殊 This
对象的简化版本。实际的 SplendidCRM 版本具有用于管理字段级安全性的其他属性,但这是一个更高级的主题,您可以稍后研究。目前,您会注意到我们的特殊 This
对象只是包装了一个 DataRow
对象并提供了一个单一的访问器。这个简单的对象有效地创建了一个沙箱,只允许规则引擎获取和设置单个数据行中的值。
public class SplendidWizardThis
{
private DataRow Row;
public SplendidWizardThis(DataRow Row)
{
this.Row = Row;
}
public object this[string columnName]
{
get { return Row[columnName]; }
set { Row[columnName] = value; }
}
}
此时,我将采取一个重大的捷径。我将假设您可以创建自己的规则录入屏幕,以便我能够跳到创建规则集的部分。BuildRuleSet
帮助函数接受一个包含规则集合的数据表,并将其转换为可执行的规则集。我只有几点说明。首先,您需要确保规则名称是唯一的(因为如果不是,将抛出异常)。其次,根据您希望规则运行的顺序设置优先级,并确保规则已激活。第三,您几乎总会希望将 Reevaluation Behavior(重新评估行为)设置为 Never(从不)。这是一个微妙的设置,因为“Always”(总是)的值可能导致无限循环。使用“Never”通常更好。
public static RuleSet BuildRuleSet(DataTable dtRules, RuleValidation validation)
{
RuleSet rules = new RuleSet("RuleSet 1");
RulesParser parser = new RulesParser(validation);
foreach ( DataRow row in dtRules )
{
string sRULE_NAME = Guid.NewGuid().ToString();
string sCONDITION = row["CONDITION" ] as string;
string sTHEN_ACTIONS = row["THEN_ACTIONS"] as string;
string sELSE_ACTIONS = row["ELSE_ACTIONS"] as string;
RuleExpressionCondition condition =
parser.ParseCondition (sCONDITION );
List<ruleaction> lstThenActions =
parser.ParseStatementList(sTHEN_ACTIONS);
List<ruleaction> lstElseActions =
parser.ParseStatementList(sELSE_ACTIONS);
System.Workflow.Activities.Rules.Rule r =
new System.Workflow.Activities.Rules.Rule
(sRULE_NAME, condition, lstThenActions, lstElseActions);
r.Priority = 0;
r.Active = true;
r.ReevaluationBehavior = RuleReevaluationBehavior.Never;
rules.Rules.Add(r);
}
return rules;
}
BuildRuleSet
的真正核心是 Condition
、Then
和 Else
操作的值。这里的事情变得不那么明显了。事实证明,Microsoft 并没有轻易解析语句,但一些聪明的开发者使用反射,并弄清楚了如何在工作流之外使用 Rules Parser。我特别感谢 Beau Crawford 发表了关于该主题的文章;请参阅 URL http://beaucrawford.net/post/Hacking-into-the-Windows-Workflow-Rules-Engine.aspx。
public class RulesParser
{
private static ConstructorInfo m_ctorParser = null;
private static MethodInfo m_methParseCondition = null;
private static MethodInfo m_methParseStatementList = null;
private static MethodInfo m_methParseSingleStatement = null;
private object objParser = null;
static RulesParser()
{
Assembly asmActivities = Assembly.GetAssembly
(typeof(System.Workflow.Activities.Rules.RuleValidation));
Type typParser = asmActivities.GetType
("System.Workflow.Activities.Rules.Parser");
m_ctorParser = typParser.GetConstructor
(BindingFlags.Instance | BindingFlags.NonPublic, null,
new Type[] { typeof(System.Workflow.Activities.Rules.RuleValidation) }, null);
m_methParseCondition = typParser.GetMethod
("ParseCondition" , BindingFlags.Instance | BindingFlags.NonPublic);
m_methParseStatementList = typParser.GetMethod
("ParseStatementList" , BindingFlags.Instance | BindingFlags.NonPublic);
m_methParseSingleStatement = typParser.GetMethod
("ParseSingleStatement", BindingFlags.Instance | BindingFlags.NonPublic);
}
public RulesParser(RuleValidation validation)
{
objParser = m_ctorParser.Invoke(new object[] { validation } );
}
public RuleExpressionCondition ParseCondition(string expressionString)
{
try
{
return m_methParseCondition.Invoke(objParser, new object[]
{ expressionString } ) as RuleExpressionCondition;
}
catch(TargetInvocationException ex)
{
// Instead of displaying "Exception has been thrown by the
// target of an invocation.",
// catch the error and return the more useful inner exception.
throw ex.InnerException;
}
}
public List<ruleaction> ParseStatementList(string statementString)
{
try
{
return m_methParseStatementList.Invoke(objParser, new object[]
{ statementString } ) as List<ruleaction>;
}
catch(TargetInvocationException ex)
{
throw ex.InnerException;
}
}
public RuleAction ParseSingleStatement(string statementString)
{
try
{
return m_methParseSingleStatement.Invoke
(objParser, new object[] { statementString } ) as RuleAction;
}
catch(TargetInvocationException ex)
{
throw ex.InnerException;
}
}
}
现在我们已经准备好所有组件,就可以定义一个运行并对其数据表进行运行了。例如,以下规则将在国家未定义时将其设置为 USA
Condition: this["COUNTRY"] == DBNull.Value
Then: this["COUNTRY"] = "USA"
请注意这里的 this 的用法。代码中的 this
指的是 SplendidWizardThis
。希望您喜欢本系列文章的第六篇。有关带有规则引擎的 SplendidCRM 的完整演示,请访问 http://demo.splendidcrm.com。登录用户为 will/will。