使用 Impromptu-Interface 进行对象装饰






4.67/5 (2投票s)
讨论使用 Impromptu-Interface 进行对象装饰来为对象添加动态行为
引言
最近,在我博客文章 Object Decoration is Functional Programming 与 jpolvora 的讨论中,他提到了 impromptu-interface。这正是我想要的 对象装饰 (OD)。使用 impromptu-interface,对象装饰可以为任何对象添加动态行为。
背景
对象装饰的概念来自 Component-Based Object Extender (CBO Extender) - 一个在运行时为对象添加动态行为的对象可扩展性框架。CBO Extender 的一个先决条件是它仅适用于接口方法。因此,未在接口中定义的方法实例无法直接附加动态行为。
使用 impromptu-interface,任何对象都可以用接口进行包装。这意味着任何对象现在都可以具有动态行为。如果一个对象具有接口方法,您可以根据需要直接为其附加行为。如果一个对象没有接口方法,您可以定义一个接口并用它来包装该对象,然后根据需要将行为附加到接口方法上。
在本文中,我首先创建了一个简单的应用程序,用于将记录插入 Microsoft SQL Server 附带的 AdventureWorks 数据库的 [Sales].[SalesOrderHeader] 和 [Sales].[SalesOrderDetail] 表中。然后,使用 impromptu-interface 来包装只有实例(非接口)方法的对象。最后,使用 CBO Extender 通过添加日志记录、安全检查和事务功能作为动态行为来增强应用程序。
您可以使用 Visual Studio 2010 中的 NuGet,在“管理 NuGet 程序包”对话框的搜索框中分别键入 impromptu-interface 和 CBOExtender,来获取 impromptu-interface 和 CBOExtender。您还可以单击 ImpromptuInterface 和 CBOExtender 链接下载它们。
使用代码
首先,我们定义两个 POCO(Plain Old CLR Object)类 Order
和 OrderDetail
。它们分别是对应 AdventureWorks 数据库中 [SalesOrderHeader] 表和 [SalesOrderDetail] 表的业务对象。
public class Order { public int OrderID { get; set; } public int CustomerID { get; set; } public DateTime DueDate { get; set; } public string AccountNumber { get; set; } public int ContactID { get; set; } public int BillToAddressID { get; set; } public int ShipToAddressID { get; set; } public int ShipMethodID { get; set; } public double SubTotal { get; set; } public double TaxAmt { get; set; } private SqlCommand commd; public SqlCommand Command { get { return commd;} set { commd = value; } } public int InsertOrder() { string sqlStr = @"INSERT [Sales].[SalesOrderHeader] ([CustomerID], [DueDate], [AccountNumber], [ContactID], [BillToAddressID], [ShipToAddressID], [ShipMethodID], [SubTotal], [TaxAmt]) values (@CustomerID, @DueDate, @AccountNumber, @ContactID, @BillToAddressID, @ShipToAddressID, @ShipMethodID, @SubTotal, @TaxAmt); SET @scopeId = SCOPE_IDENTITY()"; commd.CommandText = sqlStr; commd.CommandType = CommandType.Text; SqlParameter CustomerIDParameter = new SqlParameter("@CustomerID", SqlDbType.Int); CustomerIDParameter.Direction = ParameterDirection.Input; CustomerIDParameter.Value = CustomerID; commd.Parameters.Add(CustomerIDParameter); SqlParameter DueDateParameter = new SqlParameter("@DueDate", SqlDbType.DateTime); DueDateParameter.Direction = ParameterDirection.Input; DueDateParameter.Value = DueDate; commd.Parameters.Add(DueDateParameter); SqlParameter AccountNumberParameter = new SqlParameter("@AccountNumber", SqlDbType.Text); AccountNumberParameter.Direction = ParameterDirection.Input; AccountNumberParameter.Value = AccountNumber; commd.Parameters.Add(AccountNumberParameter); SqlParameter ContactIDParameter = new SqlParameter("@ContactID", SqlDbType.Int); ContactIDParameter.Direction = ParameterDirection.Input; ContactIDParameter.Value = ContactID; commd.Parameters.Add(ContactIDParameter); SqlParameter BillToAddressIDParameter = new SqlParameter("@BillToAddressID", SqlDbType.Int); BillToAddressIDParameter.Direction = ParameterDirection.Input; BillToAddressIDParameter.Value = BillToAddressID; commd.Parameters.Add(BillToAddressIDParameter); SqlParameter ShipToAddressIDParameter = new SqlParameter("@ShipToAddressID", SqlDbType.Int); ShipToAddressIDParameter.Direction = ParameterDirection.Input; ShipToAddressIDParameter.Value = ShipToAddressID; commd.Parameters.Add(ShipToAddressIDParameter); SqlParameter ShipMethodIDParameter = new SqlParameter("@ShipMethodID", SqlDbType.Int); ShipMethodIDParameter.Direction = ParameterDirection.Input; ShipMethodIDParameter.Value = ShipMethodID; commd.Parameters.Add(ShipMethodIDParameter); SqlParameter SubTotalParameter = new SqlParameter("@SubTotal", SqlDbType.Float); SubTotalParameter.Direction = ParameterDirection.Input; SubTotalParameter.Value = SubTotal; commd.Parameters.Add(SubTotalParameter); SqlParameter TaxAmtParameter = new SqlParameter("@TaxAmt", SqlDbType.Int); TaxAmtParameter.Direction = ParameterDirection.Input; TaxAmtParameter.Value = TaxAmt; commd.Parameters.Add(TaxAmtParameter); SqlParameter scopeIDParameter = new SqlParameter("@scopeId", SqlDbType.Int); scopeIDParameter.Direction = ParameterDirection.Output; commd.Parameters.Add(scopeIDParameter); int i = commd.ExecuteNonQuery(); OrderID = (int)scopeIDParameter.Value; return i; } } public class OrderDetail { public int SalesOrderID { get; set; } public int OrderQty { get; set; } public int ProductID { get; set; } public int SpecialOfferID { get; set; } public double UnitPrice { get; set; } private SqlCommand commd; public SqlCommand Command { get { return commd; } set { commd = value; } } public int InsertOrderDetail() { string sqlStr = @"INSERT INTO [Sales].[SalesOrderDetail] ([SalesOrderID], [OrderQty], [ProductID], [SpecialOfferID], [UnitPrice]) values (@orderID, @OrderQty, @ProductID, @SpecialOfferID, @UnitPrice)"; commd.CommandText = sqlStr; commd.CommandType = CommandType.Text; SqlParameter orderIDParameter = new SqlParameter("@orderID", SqlDbType.Int); orderIDParameter.Direction = ParameterDirection.Input; orderIDParameter.Value = SalesOrderID; commd.Parameters.Add(orderIDParameter); SqlParameter OrderQtyParameter = new SqlParameter("@OrderQty", SqlDbType.Int); OrderQtyParameter.Direction = ParameterDirection.Input; OrderQtyParameter.Value = OrderQty; commd.Parameters.Add(OrderQtyParameter); SqlParameter ProductIDParameter = new SqlParameter("@ProductID", SqlDbType.Int); ProductIDParameter.Direction = ParameterDirection.Input; ProductIDParameter.Value = ProductID; commd.Parameters.Add(ProductIDParameter); SqlParameter SpecialOfferIDParameter = new SqlParameter("@SpecialOfferID", SqlDbType.Int); SpecialOfferIDParameter.Direction = ParameterDirection.Input; SpecialOfferIDParameter.Value = SpecialOfferID; commd.Parameters.Add(SpecialOfferIDParameter); SqlParameter UnitPriceParameter = new SqlParameter("@UnitPrice", SqlDbType.Float); UnitPriceParameter.Direction = ParameterDirection.Input; UnitPriceParameter.Value = UnitPrice; commd.Parameters.Add(UnitPriceParameter); return commd.ExecuteNonQuery(); } }
以下代码创建了一个 Order
对象和一个 OrderDetail
对象,设置它们的属性,并将它们插入到相应的数据库表中。
static void Main(string[] args) { string connStr = "Integrated Security=true;Data Source=(local);Initial Catalog=AdventureWorks"; using(IDbConnection conn = new SqlConnection(connStr)) { try { conn.Open(); var o = new Order(); o.CustomerID = 18759; o.DueDate = DateTime.Now.AddDays(1); o.AccountNumber = "10-4030-018759"; o.ContactID = 4189; o.BillToAddressID = 14024; o.ShipToAddressID = 14024; o.ShipMethodID = 1; o.SubTotal = 174.20; o.TaxAmt = 10; o.Command = new SqlCommand(); o.Command.Connection = (SqlConnection)conn; int iStatus; iStatus = o.InsertOrder(); var od = new OrderDetail(); od.SalesOrderID = o.OrderID; od.OrderQty = 5; od.ProductID = 708; od.SpecialOfferID = 1; od.UnitPrice = 28.84; od.Command = new SqlCommand(); od.Command.Connection = (SqlConnection)conn; iStatus = od.InsertOrderDetail(); } catch (Exception ex) { Console.WriteLine(ex.Message); } finally { conn.Close(); } Console.ReadLine(); } }
运行上述代码,您将看到一条记录插入到 [SalesOrderHeader] 中,另一条记录插入到 [SalesOrderDetail] 中。
上述代码实现了最基本的业务逻辑。在实际应用中,您很可能需要安全检查和日志记录功能。您可能还希望这两个插入操作由事务管理,以便它们一起成功或一起失败。
定义动态行为
使用 CBO Extender,日志记录、安全检查和事务管理功能被定义为函数,这些函数作为动态行为附加到对象上。这些函数具有以下签名。
void func(AspectContext2 ctx, dynamic parameter)
以下分别是事务管理、进入日志记录、退出日志记录和安全检查的函数定义。
public static void JoinSqlTransaction(AspectContext2 ctx, dynamic parameter) { try { ctx.Target.Command.Transaction = parameter; return; } catch (Exception ex) { throw new Exception("Failed to join transaction!", ex); } } public static void EnterLog(AspectContext2 ctx, dynamic parameters) { IMethodCallMessage method = ctx.CallCtx; string str = "Entering " + ((object)ctx.Target).GetType().ToString() + "." + method.MethodName + "("; int i = 0; foreach (object o in method.Args) { if (i > 0) str = str + ", "; str = str + o.ToString(); } str = str + ")"; Console.WriteLine(str); Console.Out.Flush(); } public static void ExitLog(AspectContext2 ctx, dynamic parameters) { IMethodCallMessage method = ctx.CallCtx; string str = ((object)ctx.Target).GetType().ToString() + "." + method.MethodName + "("; int i = 0; foreach (object o in method.Args) { if (i > 0) str = str + ", "; str = str + o.ToString(); } str = str + ") exited"; Console.WriteLine(str); Console.Out.Flush(); } public static void SecurityCheck(AspectContext2 ctx, dynamic parameter) { if (parameter.IsInRole("BUILTIN\\" + "Administrators")) return; throw new Exception("No right to call!"); }
定义接口
在将上述动态行为附加到对象之前,我们需要确保对象具有接口方法。如您所见,Order
和 OrderDetail
对象没有任何接口方法。要将动态行为附加到它们,我们需要用接口包装它们。每个对象的接口定义如下。
public interface IOrder { int OrderID { get; set; } int CustomerID { get; set; } DateTime DueDate { get; set; } string AccountNumber { get; set; } int ContactID { get; set; } int BillToAddressID { get; set; } int ShipToAddressID { get; set; } int ShipMethodID { get; set; } double SubTotal { get; set; } double TaxAmt { get; set; } SqlCommand Command { get; set; } int InsertOrder(); } public interface IOrderDetail { int SalesOrderID { get; set; } int OrderQty { get; set; } int ProductID { get; set; } int SpecialOfferID { get; set; } double UnitPrice { get; set; } SqlCommand Command { get; set; } int InsertOrderDetail(); }
包装对象和附加行为
对象的扩展方法 ActLike<I>
可用于使用接口包装对象。例如,对象 o
如下所示用 IOrder
包装:
var iOrder = o.ActLike<IOrder>();
然后,iOrder
被用作 IOrder
的接口变量。我们可以使用 CBO Extender 的 CreateProxy2<T>
开始向此接口变量附加行为,它具有以下签名。
static T CreateProxy2<T>(object target, string[] arrMethods, Decoration2 preAspect, Decoration2 postAspect);
例如,EnterLog
函数作为动态行为附加到 iOrder
,如下所示:
iOrder = ObjectProxyFactory.CreateProxy2<IOrder>( iOrder, new string[] { "InsertOrder" }, new Decoration2(AppConcerns.EnterLog, null), null );
接口包装和行为附加后,此应用程序的完整代码如下。
static void Main(string[] args) { //Commenting out this line, the security check aspect will throw out an exception Thread.GetDomain().SetPrincipalPolicy(PrincipalPolicy.WindowsPrincipal); string connStr = "Integrated Security=true;Data Source=(local);Initial Catalog=AdventureWorks"; using (IDbConnection conn = new SqlConnection(connStr)) { IDbTransaction transaction = null; try { conn.Open(); IDbTransaction transactionObj = conn.BeginTransaction(); transaction = ObjectProxyFactory.CreateProxy2<IDbTransaction>( transactionObj, new string[] { "Commit", "Rollback" }, null, new Decoration2(AppConcerns.ExitLog, null) ); var o = new Order(); o.CustomerID = 18759; o.DueDate = DateTime.Now.AddDays(1); o.AccountNumber = "10-4030-018759"; o.ContactID = 4189; o.BillToAddressID = 14024; o.ShipToAddressID = 14024; o.ShipMethodID = 1; o.SubTotal = 174.20; o.TaxAmt = 10; o.Command = new SqlCommand(); o.Command.Connection = (SqlConnection)conn; var iOrder = o.ActLike<IOrder>(); iOrder = ObjectProxyFactory.CreateProxy2<IOrder>( iOrder, new string[] { "InsertOrder" }, new Decoration2(AppConcerns.JoinSqlTransaction, transactionObj), null ); iOrder = ObjectProxyFactory.CreateProxy2<IOrder>( iOrder, new string[] { "InsertOrder" }, new Decoration2(AppConcerns.EnterLog, null), new Decoration2(AppConcerns.ExitLog, null) ); iOrder = ObjectProxyFactory.CreateProxy2<IOrder>( iOrder, new string[] { "InsertOrder" }, new Decoration2(AppConcerns.SecurityCheck, Thread.CurrentPrincipal), null ); int iStatus; iStatus = iOrder.InsertOrder(); //throw new Exception(); var od = new OrderDetail(); od.SalesOrderID = o.OrderID; od.OrderQty = 5; od.ProductID = 708; od.SpecialOfferID = 1; od.UnitPrice = 28.84; od.Command = new SqlCommand(); od.Command.Connection = (SqlConnection)conn; var iOrderDetail = od.ActLike<IOrderDetail>(); iOrderDetail = ObjectProxyFactory.CreateProxy2<IOrderDetail>( iOrderDetail, new string[] { "InsertOrderDetail" }, new Decoration2(AppConcerns.JoinSqlTransaction, transactionObj), null ); iOrderDetail = ObjectProxyFactory.CreateProxy2<IOrderDetail>( iOrderDetail, new string[] { "InsertOrderDetail" }, new Decoration2(AppConcerns.EnterLog, null), new Decoration2(AppConcerns.ExitLog, null) ); iOrderDetail = ObjectProxyFactory.CreateProxy2<IOrderDetail>( iOrderDetail, new string[] { "InsertOrderDetail" }, new Decoration2(AppConcerns.SecurityCheck, Thread.CurrentPrincipal), null ); iStatus = iOrderDetail.InsertOrderDetail(); transaction.Commit(); } catch (Exception ex) { Console.WriteLine(ex.Message); if (transaction != null) transaction.Rollback(); } finally { conn.Close(); } Console.ReadLine(); } }
在上面的代码中,从 conn.BeginTransaction()
返回的事务对象 transactionObj
实现接口 IDbTransaction
。因此,我们可以直接将退出日志函数 AppConcerns.ExitLog
附加到其 Commit
和 Rollback
方法。
由于 Order
没有实现接口,我们使用对象的扩展方法 ActLike<I>
来包装其对象 o
。然后,返回的接口变量 iOrder
用于附加事务管理、进入日志、退出日志和安全检查行为。现在,当执行 iStatus = iOrder.InsertOrder();
时,它将首先检查安全性,写入进入日志,然后加入事务,最后写入退出日志。
同样,由于 OrderDetail
没有实现接口,我们使用对象的扩展方法 ActLike<I>
来包装其对象 od
。然后,返回的接口变量 iOrderDetail
用于附加事务管理、进入日志、退出日志和安全检查行为。现在,当执行 iStatus = iOrderDetail.InsertOrderDetail();
时,它将首先检查安全性,写入进入日志,然后加入事务,最后写入退出日志。
运行代码时,您将看到以下屏幕。

取消注释代码 //throw new Exception();
并运行它,您将看到以下屏幕。

关注点
有了 impromptu-interface 和对象装饰,应用程序开发再简单不过了。您只需根据业务逻辑严格设计业务对象(类)。其他关注点(安全性、日志记录、事务或需求更改)留给客户端处理。使用 impromptu-interface 的对象装饰具有以下优点。
- 它是客户端编程,这意味着您的业务对象是稳定的。
- 它是面向接口编程,这意味着您的系统可以开始松耦合并保持松耦合。
- 它是函数式编程,这意味着您为新行为编写函数。
- 它是动态编程,这意味着动态行为和动态类型。