65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 Impromptu-Interface 进行对象装饰

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.67/5 (2投票s)

2012 年 5 月 29 日

CPOL

4分钟阅读

viewsIcon

22703

downloadIcon

149

讨论使用 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。您还可以单击 ImpromptuInterfaceCBOExtender 链接下载它们。

使用代码

首先,我们定义两个 POCO(Plain Old CLR Object)类 OrderOrderDetail。它们分别是对应 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!");
}

定义接口

在将上述动态行为附加到对象之前,我们需要确保对象具有接口方法。如您所见,OrderOrderDetail 对象没有任何接口方法。要将动态行为附加到它们,我们需要用接口包装它们。每个对象的接口定义如下。

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 附加到其 CommitRollback 方法。

由于 Order 没有实现接口,我们使用对象的扩展方法 ActLike<I> 来包装其对象 o。然后,返回的接口变量 iOrder 用于附加事务管理、进入日志、退出日志和安全检查行为。现在,当执行 iStatus = iOrder.InsertOrder(); 时,它将首先检查安全性,写入进入日志,然后加入事务,最后写入退出日志。

同样,由于 OrderDetail 没有实现接口,我们使用对象的扩展方法 ActLike<I> 来包装其对象 od。然后,返回的接口变量 iOrderDetail 用于附加事务管理、进入日志、退出日志和安全检查行为。现在,当执行 iStatus = iOrderDetail.InsertOrderDetail(); 时,它将首先检查安全性,写入进入日志,然后加入事务,最后写入退出日志。

运行代码时,您将看到以下屏幕。

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

关注点

有了 impromptu-interface 和对象装饰,应用程序开发再简单不过了。您只需根据业务逻辑严格设计业务对象(类)。其他关注点(安全性、日志记录、事务或需求更改)留给客户端处理。使用 impromptu-interface 的对象装饰具有以下优点。

  • 它是客户端编程,这意味着您的业务对象是稳定的。
  • 它是面向接口编程,这意味着您的系统可以开始松耦合并保持松耦合。
  • 它是函数式编程,这意味着您为新行为编写函数。
  • 它是动态编程,这意味着动态行为和动态类型。
© . All rights reserved.