动态对象编程






4.95/5 (5投票s)
讨论动态对象编程以及它如何在 .NET 中与 CBO Extender 一起使用。
引言
什么是动态对象编程?要回答这个问题,我们需要先回答另外两个问题:什么是动态编程?什么是对象编程?
在本文中,我将尝试回答这些问题,然后提供一些示例来说明动态对象编程如何改进 .NET 中的软件开发和维护。
什么是动态编程
C# 4 中的 dynamic
类型允许操作绕过编译时类型检查,并在运行时解析。众所周知,dynamic
类型可用于简化对来自动态语言的对象的访问。
由于 dynamic
类型是 C# 的原生类型,因此您可以利用其运行时功能,而无需涉及来自动态语言的对象。一个 C# 程序是否可以在不使用动态语言的情况下拥有动态行为?或者换句话说,使用 C# 语言进行动态编程是否很方便?这就是本文的重点。在本文中,我将向您展示 dynamic
类型如何在 C# 中用于动态编程。
什么是对象编程
在面向对象编程中,重点在于类。我们设计类,创建类的对象,并在应用程序中使用这些对象。对象表现得就像在其类中定义的那样,不多也不少。然而,在对象编程中,重点在于对象。可以在不更改其类的情况下为对象添加额外的功能,这意味着对象可以具有在其类中未定义的行为。
通过对象编程,基于业务和系统需求,将一组行为定义为方法。然后,根据应用程序中的需要,将这些行为附加到对象的行为上。在运行时,在调用对象的行为之前和/或之后执行额外的行为。
什么是动态对象编程
通过对动态编程和对象编程的解释,我们现在应该能够回答什么是动态对象编程。动态对象编程是使用 dynamic
类型在额外行为的定义中的对象编程。
通过动态对象编程,额外的行为可以绕过编译时类型检查并在运行时解析。它使额外行为的定义更加灵活和强大。它还简化了在应用程序中将行为附加到对象的过程。
使用 CBO Extender 进行动态对象编程
基于组件的对象扩展器(CBO Extender)是一个对象可扩展性框架。它与接口协同工作并直接扩展对象。它是一种通用工具,通过将行为附加到对象的接口方法来为对象添加功能。随着其方面方法中 dynamic
类型的引入(有关更多详细信息,请参阅文章 带组件对象扩展器的动态类型),它可用于进行动态对象编程。
在本文中,我将尝试解决使用 CBO Extender 进行动态对象编程的应用程序的一些软件关注点,例如日志记录、安全检查、事务管理等。这些关注点被视为业务对象功能之外的额外行为,并被定义为具有 dynamic
类型的方面方法。然后,根据应用程序的需要,将它们添加到对象中。
CBOExtender 1.2 是 CBO Extender 的最新版本,可以作为 NuGet 包下载。您还可以通过 http://centurytechs.com/CBOExtender.html 下载源代码、最新更新和更多示例。
要从 Visual Studio 2010 将 CBOExtender 安装到您的项目中,请单击“工具”->“库包管理器”->“管理 NuGet 程序包...”以打开“管理 NuGet 程序包”对话框。输入 CBOExtender,如图所示。
在下载该程序包之前,您可能需要先为 Visual Studio 2010 安装 NuGet。
Using the Code
在接下来的章节中,将定义几个方面方法。然后,它们将通过将它们附加到应用程序中的对象来增强应用程序。
定义方面
方面方法应匹配 DecorationDelegate2
的签名,该签名定义如下。
public delegate void DecorationDelegate2(AspectContext2 ctx, dynamic dynPara);
以下是一些方面方法的定义。
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!");
}
JoinSqlTransaction
设置 Target
的 Command
的 Transaction
属性。请注意,Target
和 parameter
都是 dynamic
类型,编译器不会解析它们的类型。当您在应用程序中使用该方法时,确保 Target
和 parameter
具有正确的类型是您的责任。否则,您将收到运行时异常。
EnterLog
和 ExitLog
分别写入控制台的进入日志和退出日志。
SecurityCheck
检查 parameter
是否在管理员角色中。同样,当您在应用程序中使用该方法时,确保 parameter
具有正确的类型是您的责任,因为编译器在编译期间不会解析其类型。
在下一节中,我将向您展示如何使用这些动态方法来为应用程序添加日志记录、安全检查和事务管理功能。
使用 CreateProxy2
在使用上述方面方法之前,让我们编写一个小应用程序,将记录插入 Microsoft SQL Server 附带的 AdventureWorks 数据库的 [Sales].[SalesOrderHeader] 和 [Sales].[SalesOrderDetail] 表中。应用程序代码如下所示。
static void Main(string[] args)
{
string connStr = "Integrated Security=true;" +
"Data Source=(local);Initial Catalog=AdventureWorks";
using(IDbConnection conn = new SqlConnection(connStr))
{
IDbTransaction transaction = null;
try
{
conn.Open();
IOrder 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;
((ISqlOperation)o).Command = new SqlCommand();
((ISqlOperation)o).Command.Connection = (SqlConnection)conn;
int iStatus;
iStatus = o.InsertOrder();
//throw new Exception();
IOrderDetail od = new OrderDetail();
od.SalesOrderID = o.OrderID;
od.OrderQty = 5;
od.ProductID = 708;
od.SpecialOfferID = 1;
od.UnitPrice = 28.84;
((ISqlOperation)od).Command = new SqlCommand();
((ISqlOperation)od).Command.Connection = (SqlConnection)conn;
iStatus = od.InsertOrderDetail();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
if (transaction != null)
transaction.Rollback();
}
finally
{
conn.Close();
}
Console.ReadLine();
}
}
执行时,Order
组件的 InsertOrder
方法将记录插入 [Sales].[SalesOrderHeader]
表,而 OrderDetail
组件的 InsertOrderDetail
方法将记录插入 [Sales].[SalesOrderDetail] 表。
上面的代码存在一个问题:两次插入不是在一个事务中进行的。如果程序在第一次插入后但在第二次插入之前失败,那么 [Sales].[SalesOrderDetail] 表的数据将会丢失。您可以通过取消注释行 //throw new Exception();
来模拟此问题。
为了解决这个问题,我们需要为该应用程序添加事务管理功能,将插入操作放在单个事务中,以便所有操作都执行或都不执行。除了事务管理,我们还希望为该应用程序添加安全检查,以便只有管理员可以通过运行此程序来插入记录到表中。最后,我们还希望应用程序在执行时生成一些日志。
正如您可能已经知道的,我们使用上一节定义的方面来为应用程序添加这些增强功能。ObjectProxyFactory.CreateProxy2
方法用于将方面添加到应用程序中的对象。ObjectProxyFactory.CreateProxy2
方法具有以下签名
public static T CreateProxy2<T>(object target, String[] arrMethods,
Decoration2 preAspect, Decoration2 postAspect)
其中
T
- 要返回的接口target
- 原始对象arrMethods
- 要添加预处理方面和/或后处理方面的字符串方法名称数组preAspect
- 用于预处理方面的装饰postAspect
- 用于后处理方面的装饰
Decoration2
封装了一个 DecorationDelegate2
和一个 dynamic
,并具有一个带有以下签名的构造函数
public Decoration2(DecorationDelegate2 aspectHandler, dynamic parameter)
aspectHandler
是一个方面方法的委托,并且在运行时调用方面方法时,parameter
对象作为第二个参数传递给方面方法。
使用 ObjectProxyFactory.CreateProxy2
和上述方面方法,我们可以增强应用程序以具备事务管理、安全检查和日志记录功能。完整的应用程序代码如下所示
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)
);
IOrder 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;
((ISqlOperation)o).Command = new SqlCommand();
((ISqlOperation)o).Command.Connection = (SqlConnection)conn;
o = ObjectProxyFactory.CreateProxy2<IOrder>(
o,
new string[] { "InsertOrder" },
new Decoration2(AppConcerns.JoinSqlTransaction, transactionObj),
null
);
o = ObjectProxyFactory.CreateProxy2<IOrder>(
o,
new string[] { "InsertOrder" },
new Decoration2(AppConcerns.EnterLog, null),
new Decoration2(AppConcerns.ExitLog, null)
);
o = ObjectProxyFactory.CreateProxy2<IOrder>(
o,
new string[] { "InsertOrder" },
new Decoration2(AppConcerns.SecurityCheck, Thread.CurrentPrincipal),
null
);
int iStatus;
iStatus = o.InsertOrder();
//throw new Exception();
IOrderDetail od = new OrderDetail();
od.SalesOrderID = o.OrderID;
od.OrderQty = 5;
od.ProductID = 708;
od.SpecialOfferID = 1;
od.UnitPrice = 28.84;
((ISqlOperation)od).Command = new SqlCommand();
((ISqlOperation)od).Command.Connection = (SqlConnection)conn;
od = ObjectProxyFactory.CreateProxy2<IOrderDetail>(
od,
new string[] { "InsertOrderDetail" },
new Decoration2(AppConcerns.JoinSqlTransaction, transactionObj),
null
);
od = ObjectProxyFactory.CreateProxy2<IOrderDetail>(
od,
new string[] { "InsertOrderDetail" },
new Decoration2(AppConcerns.EnterLog, null),
new Decoration2(AppConcerns.ExitLog, null)
);
od = ObjectProxyFactory.CreateProxy2<IOrderDetail>(
od,
new string[] { "InsertOrderDetail" },
new Decoration2(AppConcerns.SecurityCheck, Thread.CurrentPrincipal),
null
);
iStatus = od.InsertOrderDetail();
transaction.Commit();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
if (transaction != null)
transaction.Rollback();
}
finally
{
conn.Close();
}
Console.ReadLine();
}
}
代码
transaction = ObjectProxyFactory.CreateProxy2<IDbTransaction>(
transactionObj,
new string[] { "Commit", "Rollback" },
null,
new Decoration2(AppConcerns.ExitLog, null)
);
将退出日志方法作为后处理方面附加到 transactionObj
的 Commit
和 Rollback
方法。当使用 transaction
访问这些方法时,在方法执行后立即输出退出日志。
代码
o = ObjectProxyFactory.CreateProxy2<IOrder>(
o,
new string[] { "InsertOrder" },
new Decoration2(AppConcerns.JoinSqlTransaction, transactionObj),
null
);
将加入事务方法作为预处理方面附加到 Order
组件对象 o
的 InsertOrder
方法。请注意,变量 o
最初引用 Order
组件的一个对象。现在,它引用该对象的代理。您可以这样链接多个方面到对象。当使用 o
调用方法时,它首先将方法添加到事务中,然后调用该方法。
代码
o = ObjectProxyFactory.CreateProxy2<IOrder>(
o,
new string[] { "InsertOrder" },
new Decoration2(AppConcerns.EnterLog, null),
new Decoration2(AppConcerns.ExitLog, null)
);
将进入日志方法和退出日志方法作为预处理方面和后处理方面分别附加到代理 o
的 InsertOrder
方法。当使用 o
调用方法时,它首先写入进入日志,然后调用方法,最后写入退出日志。
代码
o = ObjectProxyFactory.CreateProxy2<IOrder>(
o,
new string[] { "InsertOrder" },
new Decoration2(AppConcerns.SecurityCheck, Thread.CurrentPrincipal),
null
);
将安全检查方法作为预处理方面附加到代理 o
的 InsertOrder
方法。当使用 o
调用方法时,它首先执行安全检查,然后调用方法。
从上面的代码可以看出,您可以将 ObjectProxyFactory.CreateProxy2
返回的代理用作另一个 ObjectProxyFactory.CreateProxy2
调用的目标,以链接多个方面。当执行行 iStatus = o.InsertOrder();
时,如果一切按预期进行,它会首先检查安全性,然后写入进入日志,然后将原始对象的方法加入事务。此时,它完成了所有预处理方面,并开始调用原始对象的 InsertOrder()
方法。在此之后,它写入退出日志。
其余代码对 OrderDetail
组件的对象 od
执行相同的操作。它向其添加加入事务、进入日志、退出日志和安全检查方面。代码 iStatus = od.InsertOrderDetail();
检查安全性,写入进入日志,将方法加入事务,调用方法,并写入退出日志。
代码 transaction.Commit();
提交事务并写入退出日志。
代码 transaction.Rollback();
回滚事务并写入退出日志。
运行应用程序,您将看到以下屏幕。并且,新记录已分别插入 [Sales].[SalesOrderHeader] 和 [Sales].[SalesOrderDetail] 表中。
取消注释行 //throw new Exception();
并再次运行,您将看到以下屏幕。并且,没有记录插入 [Sales].[SalesOrderHeader] 或 [Sales].[SalesOrderDetail]。
注释掉行 Thread.GetDomain().SetPrincipalPolicy(PrincipalPolicy.WindowsPrincipal);
并再次运行,您将看到以下屏幕。安全检查失败。没有生成日志,也没有记录插入 [Sales].[SalesOrderHeader] 或 [Sales].[SalesOrderDetail]。
关注点
对象编程揭示了一种编程范例,旨在通过在运行时扩展对象而不是在设计时扩展类来改进软件开发。与动态编程结合,它使软件开发更具灵活性,更能适应变化。
通过 CBO Extender,您可以通过定义一组方法并在运行时根据需要将它们附加到对象来实现动态对象编程。它通过避免更改类来补充面向对象编程,从而提高了软件系统的灵活性并降低了维护成本。它将编程限制在接口和接口方法。