使用 Humax Web Framework 的 JavaScript 面向方面编程






2.50/5 (2投票s)
使用 Humax v0.2,本文将解释如何在 JavaScript 中编写和应用面向切面/元数据驱动的编程。
我们程序员(架构师/设计师/开发人员)在开发应用程序时,习惯于采用不同的编程方式。旅程始于顺序汇编编程。二十年前,我们主要采用 C 语言的面向过程方法。尽管 SmallTalk 引入了面向对象的方法,但 Bjarne Stroustrup 的 C++ 在面向对象编程中产生了巨大影响。其余的已成为历史。
除了这些,还有两种编程方法:函数式编程和面向切面编程 (AOP) 与我们并存。JavaScript 默认是主要面向过程、少量函数式和基于原型的面向对象语言的组合。没有其他现代语言(至今)具备此功能。
为 JavaScript 提供 AOP 支持并非难事,因为 JavaScript 的灵活性。有许多框架可用于在 JavaScript 中应用面向切面的方法。Humax 是 0.2 版本中提供 AOP 功能的 Web 框架之一。
AOP - 简介
在典型的软件应用程序开发中,我们以对象为中心进行系统设计和开发。例如,在医疗保健应用程序中,您可能会找到 Patient、Clinical Request 等类型的对象。大多数现代技术,如 Java、.NET,都基于 OO,因为它是更成熟且广为人知的编程方法。除此以外,如今我们感到应用程序中需要跨领域的功能。
跨领域关注点
假设您正在维护一个企业保险系统。该系统没有特别的日志记录仪器。当管理层要求在系统中实现日志记录功能时,典型的 OO 方法是,您需要设计和开发用于日志记录的类,然后在保险系统的每个类中实例化并调用必要的函数。这被称为跨领域关注点。
AOP 旨在通过切面来处理跨领域关注点,以描述这些关注点并将它们自动集成到系统中。
请注意,AOP 不是 OOP 的替代品,而是与 OOP 一起解决这些问题。
代码散播
我们在 OOP 或其他编程方法中面临的一个问题是代码的可维护性,当我们不得不更改方法签名时。这会影响到所有使用此方法的代码。尽管我们可以通过版本控制简单地解决这个问题,但 AOP 试图通过允许将跨领域关注点表示为名为切面的独立模块来解决这个问题。
切面及其概念
方面
切面是一个编程模块,用于捕获跨越应用程序的功能。切面可以改变应用程序非切面部分的行为。这可以通过在量化或查询(称为切入点)指定的各种连接点上进行通知来实现。
连接点
目标程序中一个或多个切面适用的点。该点可以是类或类集合的方法、构造函数、字段、异常和/或事件。
切入点
切入点是一个实体,用于检测给定的连接点是否匹配。简而言之,切入点是指定切面适用的连接点集合的量化或查询。例如,我们可以在类中所有以“get”开头的方法上应用切面。
通知
通知是一种在连接点指定要运行的代码的方式。通知中的行为是针对与关联切入点相交的所有连接点执行的。存在三种类型的通知:
- 前置。通知在连接点执行之前执行
- 后置。通知在连接点执行之后执行
- 环绕。通知在连接点执行周围(通知代码的一部分在连接点之前执行,通知代码的一部分在连接点执行之后执行)执行。
不同编程语言的不同 AOP 框架遵循不同的模型来采用 AOP。要了解更多关于 AOP 的概念,请从 Wikipedia: AOP 页面开始。
使用元数据编程
元数据编程是一种向编程元素(如类、属性和方法)添加描述性语句以进行注解的方法。这些描述性语句可以在运行时用于根据其值影响应用程序行为。在 .NET 中,这可以通过 Attributes(它在元数据编程方面更成熟且领先)来实现,在 Java 中,可以通过 Annotation 来实现。
Humax 允许您通过 Facet 同时使用 AOP 和元数据编程。
Humax 中的 Facet 编程
Humax 从 0.2 版本开始引入了 **Facet**,以支持面向切面编程和元数据编程。
下图展示了 Humax Facet 的高层视图。
本节将介绍一个简单的 Humax Facet 库存管理示例。此示例介绍了编写切面的语法。
请注意,示例代码是基于 Humax 框架编写的。如果您不熟悉某些代码,请访问 Humax Web Framework at SourceForge.net。
库存管理应用程序
我们在这里使用的示例是一个库存管理系统,用于填写和/或订购库存。
Humax.declareNamespace("HxTest");
//Inventory Class
//Constructor
HxTest.Inventory = function()
{
// Fields
this._capacity = 10;
this._available = 0;
this._reorderLevel = 3;
// Declare a multicast delegate called StockAlert -
//If you do not know delegates, visit tutorial
// section at http://humax.sourceforge.net
this.StockAlert = new Humax.MulticastDelegate("StockAlert",
Function("message, ordered, available",""));
// Declare an event called OnStockAlert
this.OnStockAlert = new Humax.Event(this.StockAlert);
}
HxTest.Inventory.prototype =
{
fillInventory : function(quantity)
{
if(this._available + quantity <= this._capacity)
{
this._available += quantity;
}else
{
// if inventory is full, raise the OnStockAlert event
this.OnStockAlert(Humax.EventAction.Trigger,
"Stock full", quantity,this._available);
}
},
orderItem : function(quantity)
{
if(quantity <= (this._available - this._reorderLevel))
{
this._available -= quantity;
}else
{
// if inventory reached minimum reorder level, raise the OnStockAlert event
this.OnStockAlert(Humax.EventAction.Trigger,
"Fill the stock, reached minium reorder level", quantity,this._available);
}
},
getAvailableStockCount : function() {return this._available;}
}
// declare the class Inventory as Humax compatible
Humax.declareClass("HxTest.Inventory",HxTest.Inventory);
Inventory 类有两个主要方法 fillInventory 和 orderItem。_available 和 _reorderLevel 字段参与库存的填写和订购。fillInventory 和 orderItem 方法分别用于填写和订购库存中的商品。当库存数量发生变化时,这两个方法都会触发 OnStockAlert 事件。
运行应用程序
在 html 页面中,您可以将上述代码放入 html->head->script 块中。在页面中添加以下 HTML 声明。
<body önload="page_onload()">
填充分量 | <input type="text" id="fillTextBox" /> | <input type="button" id="fillButton" value="Fill" önclick="fillButton_onclick()" /> |
订分数量 | <input type="text" id="orderTextBox" /> | <input type="button" id="orderButton" value="Order" önclick="orderButton_onclick()" /> |
在 html->head->script 块中添加以下事件处理程序。此外,会声明一个全局变量 myInventory 来保存 Inventory 实例。
var myInventory = null;
function page_onload()
{
myInventory = new HxTest.Inventory();
myInventory.OnStockAlert(Humax.EventAction.AttachHandler,
stockAlertHandler)
}
function fillButton_onclick()
{
document.getElementById("output").innerHTML = "";
myInventory.fillInventory(
parseInt(document.getElementById("fillTextBox").value)
);
document.getElementById("status").innerHTML =
"Available Stock: " + myInventory.getAvailableStockCount();
}
function orderButton_onclick()
{
document.getElementById("output").innerHTML = "";
myInventory.orderItem(parseInt(document.getElementById("orderTextBox").value));
document.getElementById("status").innerHTML =
"Available Stock: " + myInventory.getAvailableStockCount();
}
function stockAlertHandler(message, ordered, available)
{
document.getElementById("output").innerHTML =
message + " Ordered:" + ordered.toString() + " Available: " + available.toString();
}
页面加载事件处理程序实例化 myInventory,并将 stockAlertHandler 作为 OnStockAlert 事件的处理程序之一。fillButton_onclick 调用 fillInventory 方法,并将可用库存显示在 UI 上。orderButton_onclick 调用 orderItem 方法,并将可用库存显示在 UI 上。当填写或订购无效时,stockAlertHandler 会在 UI 上显示消息,说明当前已订购多少以及库存中还剩多少。
第一个 Facet
我们知道 AOP 可以通过 Humax Facet 实现。现在我们将继续开发我们的第一个 Facet。这个 Facet 通过在 Inventory 类的 fillInventory 方法执行之前和之后显示消息来监视每个已填写的项目。在上述事件处理程序下方添加以下脚本。
HxTest.InventoryValidatorFacet = function()
{
//calls base class "Humax.Facet" constructor implementation
this.base();
//Facet usage
this.$usage = Humax.FacetUsage.AllowMultiple |
Humax.FacetUsage.AllowAdvices;
}
$usage 和 $target 字段
与 Humax 类声明一样,您可以使用函数构造函数声明一个 Facet。上面的代码声明了 InventoryValidatorFacet。在构造函数中,它调用基类构造函数的实现,并指定该 Facet 的目标用法。基类受保护的成员 $usage 允许您通过设置 FacetUsage 枚举来控制其使用方式。在上面的代码中,可以使用 AllowMultiple 枚举值对给定元素指定多个 InventoryValidatorFacet。如果要使用此 Facet 来允许通知,则应指定 AllowAdvices。令人惊讶吧!
本质上,Facet 是 Humax 用于元数据编程和 AOP 的一种方法。默认情况下,Facet 只是一个元数据容器。如我们所知,**通知**是指定要在连接点运行的代码的一种方式。即使您已经实现了一个切面,如果没有指定 AllowAdvices 枚举,您的 Facet 也不能被视为 Aspect。
除了 $usage,Facet 还提供了一个受保护的字段 $target。这允许您指定 Facet 是一个切面时连接点的类型,或者 Facet 是一个有效的元数据时它的应用程序元素。基类默认指定 Humax 类和接口。请注意,默认情况下,Facet 的用法是 AllowMultiple。
在上面的代码中,我们指定了我们将把这个 Facet 用作切面,并且可以为给定的元素指定多个。并且它只针对类和接口。
现在,我们将实现这个切面。在 Facet 声明下方添加以下代码。
HxTest.InventoryValidatorFacet.prototype =
{
beforeFillInventory : function(sourceObject, joinPointOriginalArgs)
{
alert("Going to fill");
},
afterFillInventory : function(sourceObject, joinPointOriginalArgs)
{
alert(joinPointOriginalArgs[0] + " item(s) has been filled");
},
attachHandlers : function()
{
//calls the base class "Humax.Facet" method attachHandlers
// to initiate before/after/around advice delegates
this.callBaseMethod("attachHandlers");
this.BeforeAdvice(Humax.EventAction.AttachHandler,
this.beforeFillInventory, this);
this.AfterAdvice(Humax.EventAction.AttachHandler,
this.afterFillInventory, this);
}
}
Humax.declareClass("HxTest.InventoryValidatorFacet",
HxTest.InventoryValidatorFacet, Humax.Facet);
// apply the facet on Inventory class to the member "fillInventory"
Humax.applyFacet(new HxTest.InventoryValidatorFacet(),
HxTest.Inventory, "fillInventory");
上面的 Facet 有三个方法 beforeFillInventory、afterFillInventory 和 attachHandlers。前两个方法实际上是为我们自己的 Facet 方法。这些方法将在 myInventory.fillInventory() 方法执行之前和之后被调用。beforeFillInventory 方法显示弹出消息“Going to fill”,afterFillInventory 方法显示弹出消息“No of items has been filled”。
BeforeAdvice、AfterAdvice 和 AroundAdvice 事件
基类 Facet 提供了三个事件 BeforeAdvice、AfterAdvice 和 AroundAdvice,分别用于指定切面的前置、后置和环绕通知。
BeforeAdvice 和 AfterAdvice 事件期望其处理程序的签名是“**sourceObject, joinPointOriginalArgs**”,其中 sourceObject 包含当前通知将要执行的实际对象;joinPointOriginalArgs 包含传递给目标方法的参数。在上面的代码中,sourceObject 是 myInventory,joinPointOriginalArgs 是实际传递给 fillInventory() 方法的参数。
AroundAdvice 与前置和后置通知不同。它期望其处理程序的签名是“**sourceObject, sourceMethod, joinPointOriginalArgs**”。下一节将详细介绍这一点。
attachHandlers 方法
attachHandlers 是 Facet 类的一个成员,用于指定切面的哪个实现应该在特定的通知上执行。此方法首先调用基类的实现。
请注意,必须调用基类的 attachHandlers 实现来进行一些初始化工作。
在上面的实现中,我们将 beforeFillOrder 添加到前置通知,将 afterFillOrder 添加到后置通知。
Humax 提供了 applyFacet() 方法来向 Humax 类型添加 Facet。这是指定切入点的方式。第一个参数应该是有效的 Facet 实例,第二个参数应该是 Facet 将要应用的类型。第三个参数应该是实际的切入点。在这种情况下,它是 fillInventory()。
注意:从 0.2.1 版本开始,Humax 支持使用正则表达式格式指定切入点,例如 Humax.applyFacet(new HxTest.MyFacet(), HxTest.MyClass, "get*");,这意味着该 Facet 适用于所有以“get”开头的方法。
现在执行应用程序。
更多 Facet 编程和元数据编程
AroundAdvice
在前一节中,我们已经看到了 BeforeAdvice 和 AfterAdvice 的用法。在本节中,我们将看到如何使用 AroundAdvice。我们使用了与上一节相同的代码。
在本节中,我们对 fillInventory() 进行了修改。
... fillInventory : function(quantity) { if(quantity <= 0) throw new Humax.Exception("Invalid quantity", "HxTest.Inventory.fillInventory()"); if(this._available + quantity <= this._capacity) { this._available += quantity; }else { this.OnStockAlert(Humax.EventAction.Trigger, "Stock full", quantity,this._available); } }, ...
在上面的代码中,请看粗体部分。如果填写的商品数量为零或负数,它会抛出带有消息“Invalid quantity”的异常。假设该方法的调用者在大多数代码中不编写异常处理,例如:
function fillButton_onclick() { document.getElementById("output").innerHTML = ""; myInventory.fillInventory(parseInt( document.getElementById("fillTextBox").value) ); document.getElementById("status").innerHTML = "Available Stock: " + myInventory.getAvailableStockCount(); }
但是他需要在调用 fillInventory() 的所有地方都实现异常处理。这就是我们在上一节中学到的代码散播。但是,通过在 InventoryValidatorFacet 中实现 AroundAdvice,我们可以轻松实现这一点。现在我们将实现 InventoryValidatorFacet 中的 AroundAdvice。为了方便起见,修改过的或新插入的代码以粗体显示。
HxTest.InventoryValidatorFacet.prototype = { beforeFillInventory : function(sourceObject, joinPointOriginalArgs) { alert("Going to fill"); }, afterFillInventory : function(sourceObject, joinPointOriginalArgs) { if(joinPointOriginalArgs[0] > 0) alert(joinPointOriginalArgs[0] + " item(s) has been filled"); }, aroundFillInventory : function(sourceObject, sourceMethod, joinPointOriginalArgs) { try { //callSource is the Facet method to invoke actual joinpoint implementation this.callSource(sourceObject, sourceMethod, joinPointOriginalArgs); }catch(e) { alert(e.getMessage()); } }, attachHandlers : function() { this.callBaseMethod("attachHandlers"); this.BeforeAdvice(Humax.EventAction.AttachHandler, this.beforeFillInventory, this); this.AfterAdvice(Humax.EventAction.AttachHandler, this.afterFillInventory, this); this.AroundAdvice(Humax.EventAction.AttachHandler, this.aroundFillInventory, this); } }
为了在数量小于或等于零时停止在 afterFillInventory 中显示消息,在 afterFillInventory() 中应用了一个条件。
callSource 方法
我们知道 aroundFillInventory 有一个额外的参数 sourceMethod,它指向当前连接点。下一个重要的问题是如何在 aroundFillInventory 中调用当前连接点。Facet 提供了一个名为 callSource() 的方法,它实际上调用连接点。您可以将 AroundAdvice 实现写在该.callSource() 周围。
现在执行应用程序。
使用 Facet 进行元数据编程
我们可以使用元数据将信息传递给各种编程组件,这些组件可以在运行时被应用组件解析。信息可以是以下任何一项:
- 代码版本信息
- 需要在运行时更改程序流程的描述性信息。
Humax 允许您通过 Facet 声明元数据,该 Facet 可以是切面,也可以不是。假设在 HxTest.Inventory 类中,我们计划为 fillInventory() 和 orderItem() 方法提供一个密钥码。这些方法只需解析内容,如果有效则继续执行。
声明元数据
HxTest.InventoryPassportFacet = function(keyCode) { this.base(); this.$usage = Humax.FacetUsage.AllowMultiple; this._keyCode = keyCode; } HxTest.InventoryPassportFacet.prototype = { getKeyCode : function() { return this._keyCode;} } Humax.declareClass("HxTest.InventoryPassportFacet", HxTest.InventoryPassportFacet, Humax.Facet);
在上面的代码中,我们定义了一个不带通知支持的 Facet InventoryPassportFacet,这意味着我们声明此 Facet 纯粹用于元数据。它有一个 getter 方法 getKeyCode()。
最后,我们在代码的其他位置将此 Facet 应用于 Inventory 类的成员 fillInventory 和 orderItem。
Humax.applyFacet(new HxTest.InventoryPassportFacet("abc123zyx987"), HxTest.Inventory, "fillInventory", "orderItem");
如前所述,这些方法应该获取当前传递的 keyCode 以供进一步处理,在我们的例子中,我们将简单地显示 keycode。请看下面代码中的粗体行。
fillInventory : function(quantity) { var facet = Humax.Facet.getFacet("HxTest.InventoryPassportFacet", this, "fillInventory"); alert("This is fill inventory: Your key code is " + facet.getKeyCode()); if(quantity <= 0) throw new Humax.Exception("Invalid quantity", "HxTest.Inventory.fillInventory()"); if(this._available + quantity <= this._capacity) { this._available += quantity; }else { this.OnStockAlert(Humax.EventAction.Trigger, "Stock full", quantity,this._available); } }, orderItem : function(quantity) { var facet = Humax.Facet.getFacet("HxTest.InventoryPassportFacet", this, "orderItem"); alert("This is order item: Your key code is " + facet.getKeyCode()); if(quantity <= (this._available - this._reorderLevel)) { this._available -= quantity; }else { this.OnStockAlert(Humax.EventAction.Trigger, "Fill the stock, reached minium reorder level", quantity,this._available); } }
Humax.Facet 类提供 getFacet() 方法来获取应用于当前执行上下文的指定 Facet。它还提供 getFacets() 来获取应用于当前执行上下文的所有 Facet。
现在,执行程序。
有关 Humax Web 框架的更多详细信息或您的贡献,请访问 http://humax.sourceforge.net 或
写信给我 udooz@hotmail.com。