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

组件、方面和动态装饰器在 ASP.NET MVC 应用程序中的应用

starIconstarIconstarIconstarIconemptyStarIcon

4.00/5 (2投票s)

2011 年 5 月 17 日

CPOL

9分钟阅读

viewsIcon

20486

downloadIcon

224

将组件、方面和动态装饰器原则应用于 ASP.NET MVC 应用程序。

引言

正如在 组件、方面和动态装饰器 中所讨论的,应用程序开发涉及特定于应用程序类型的任务和独立于应用程序类型的任务。这种任务的分离非常重要,它不仅有助于您根据与应用程序类型相关的特定技术来决定处理策略、部署策略、测试策略、UI 策略等,还有助于您系统地解决应用程序类型无关的、也称为公共的任务,如设计/扩展组件和处理横切关注点。文章讨论了开发这些公共任务的几种指导原则。动态装饰器用于遵循这些指导原则来处理这些任务。文中以 WinForms 应用程序为例。

在本文中,我将通过使用动态装饰器来演示这些原则和指导原则如何应用于另一种应用程序类型——ASP.NET MVC 应用程序。

选择应用程序类型

在 .NET 世界中存在几种应用程序类型:WinForms、ASP.NET、ASP.NET MVC 和 Silverlight。WinForms 应用程序提供丰富的用户交互和客户端处理能力。ASP.NET 应用程序通过 Web 具有部署和维护的优势。ASP.NET MVC 提供可测试性以及 ASP.NET 的优势。Silverlight 应用程序通过 Web 提供丰富的用户界面。不同类型的应用程序具有不同的技术细节,这些细节可能符合也可能不符合您的要求。您应该根据您的处理策略、部署策略、维护策略、测试策略、UI 策略等来选择您的应用程序类型。

不同类型的应用程序还具有不同的应用程序编程模型、UI 元素、状态管理、事件处理模型等。一旦您选择了应用程序类型,就需要处理与该应用程序类型相关的任务。

处理通用任务

除了特定于应用程序类型的任务外,应用程序开发中还存在独立于特定应用程序类型的任务。例如,无论应用程序类型如何,您都会遇到设计、扩展组件和处理横切关注点等任务。它们对于所有应用程序类型都是通用的。

在文章 组件、方面和动态装饰器 中给出了一套用于开发这些公共任务的原则。它们在此处再次列出如下:

  • 以通用方式设计组件以满足业务需求
  • 将方面设计为各自模块中的全局方法
  • 按需为对象添加方面
  • 按需扩展对象

有关这些原则的详细讨论,请参阅 组件、方面和动态装饰器。您可能还需要阅读文章 动态装饰器模式 来理解动态装饰器,并阅读文章 使用动态装饰器向对象添加方面 来理解使用动态装饰器的方面编程。

示例

在接下来的部分中,将讨论一个示例应用程序,通过使用动态装饰器演示如何将上述原则应用于 ASP.NET MVC 应用程序。

这里使用了与 组件、方面和动态装饰器 中作为示例讨论的相同问题。这一次,我们选择 ASP.NET MVC 应用程序作为应用程序类型,而不是 WinForms 应用程序。为方便起见,我在此再次陈述问题。

问题

根据部门选择显示员工。

Components

假设有两个组件:EmployeeDepartment。对于 Employee,有一个相应的 RepositoryEmployee 组件,其中包含 Employee 对象的集合。对于 Department,有一个相应的 RepositoryDepartment 组件,其中包含 Department 对象的集合。这些组件的代码如下所示:

public interface IEmployee
{
    System.Int32? EmployeeID { get; set; }
    System.String FirstName { get; set; }
    System.String LastName { get; set; }
    System.DateTime DateOfBirth { get; set; }
    System.Int32? DepartmentID { get; set; }
    System.String FullName();
    System.Single Salary();
}

public class Employee : IEmployee
{
    #region Properties

    public System.Int32? EmployeeID { get; set; }
    public System.String FirstName { get; set; }
    public System.String LastName { get; set; }
    public System.DateTime DateOfBirth { get; set; }
    public System.Int32? DepartmentID { get; set; }

    #endregion

    public Employee(
        System.Int32? employeeid
        , System.String firstname
        , System.String lastname
        , System.DateTime bDay
        , System.Int32? departmentID
    )
    {
        this.EmployeeID = employeeid;
        this.FirstName = firstname;
        this.LastName = lastname;
        this.DateOfBirth = bDay;
        this.DepartmentID = departmentID;
    }

    public Employee() { }

    public System.String FullName()
    {
        System.String s = FirstName + " " + LastName;
        return s;
    }

    public System.Single Salary()
    {
        System.Single i = 10000.12f;
        return i;
    }
}

public interface IDepartment
{
    System.Int32? DepartmentID { get; set; }
    System.String Name { get; set; }
}

public class Department : IDepartment
{
    #region Properties

    public System.Int32? DepartmentID { get; set; }
    public System.String Name { get; set; }

    #endregion

    public Department(
        System.Int32? departmentid
        , System.String name
    )
    {
        this.DepartmentID = departmentid;
        this.Name = name;
    }

    public Department() { }
}

public interface IRepository<T>
{
    List<T> RepList { get; set; }
    void GetAll();
}

public class RepositoryEmployee : IRepository<IEmployee>
{
    private List<IEmployee> myList = null;

    public List<IEmployee> RepList
    {
        get { return myList; }
        set { myList = value; }
    }

    public RepositoryEmployee()
    {
    }

    public void GetAll()
    {
        myList = new List<IEmployee> { 
            new Employee(1, "John", "Smith", new DateTime(1990, 4, 1), 1), 
            new Employee(2, "Gustavo", "Achong", new DateTime(1980, 8, 1), 1), 
            new Employee(3, "Maxwell", "Becker", new DateTime(1966, 12, 24), 2), 
            new Employee(4, "Catherine", "Johnston", new DateTime(1977, 4, 12), 2), 
            new Employee(5, "Payton", "Castellucio", new DateTime(1959, 4, 21), 3), 
            new Employee(6, "Pamela", "Lee", new DateTime(1978, 9, 16), 4) };
    }
}

public class RepositoryDepartment : IRepository<IDepartment>
{
    private List<IDepartment> myList = null;

    public List<IDepartment> RepList
    {
        get { return myList; }
        set { myList = value; }
    }

    public RepositoryDepartment()
    {
    }

    public void GetAll()
    {
        myList = new List<IDepartment> { new Department(1, "Engineering"), 
            new Department(2, "Sales"), 
            new Department(3, "Marketing"), 
            new Department(4, "Executive") };
    }
}

在此应用程序中,员工和部门的数据被硬编码在两个列表中以简化我们的讨论。在实际应用程序中,这些数据通常会持久化到关系数据库中。然后,您需要创建一个数据层来检索它们并将它们放入列表中。

值得注意的是,员工列表是按照插入顺序填充的,没有任何排序。此时很难预料将支持此组件的哪种排序。在应用程序中,一个组件对象可能需要按姓氏排序。另一个对象可能需要按生日排序。第三个对象可能根本不需要排序。因此,最好将排序的实现推迟到组件在应用程序中使用时。通过设计 RepositoryEmployee 组件时不考虑排序,就遵循了“以通用方式设计组件以满足业务需求”的原则。这样,组件就是稳定且封闭的。

HRMVC

HRMVC 是一个 ASP.NET MVC 应用程序,它使用上述组件根据部门选择来显示员工。由于它是一个 ASP.NET MVC 应用程序,它遵循 ASP.NET MVC 的应用程序编程模型和事件模型。Controller 代码如下所示:

public class DepEmployeesController : Controller
{
    private IRepository<IEmployee> rpEmployee = null;
    private IRepository<IDepartment> rpDepartment = null;

    private static int iStaticDep = 0;

    public DepEmployeesController()
    {
        rpEmployee = new RepositoryEmployee();
        rpDepartment = new RepositoryDepartment();
    }

    public ActionResult Index()
    {
        rpDepartment.GetAll();

        rpEmployee.GetAll();
            
        if (Request.Form.GetValues("depSel") == null)
        {
            List<SelectListItem> depList = new List<SelectListItem>();
            SelectListItem sli = null;
            sli = new SelectListItem();
            sli.Value = "";
            sli.Text = "";
            depList.Add(sli);

            foreach (IDepartment d in rpDepartment.RepList)
            {
                sli = new SelectListItem();
                sli.Value = d.DepartmentID.Value.ToString();
                sli.Text = d.Name;
                depList.Add(sli);
            }

            ViewData["depSel"] = new SelectList(depList, "Value", "Text");
            return View(rpEmployee.RepList);
        }
        else
        {
            string selVal = "";
            selVal = Request.Form.GetValues("depSel")[0];

            List<SelectListItem> depList = new List<SelectListItem>();
            SelectListItem sli = null;
            foreach (IDepartment d in rpDepartment.RepList)
            {
                sli = new SelectListItem();
                sli.Value = d.DepartmentID.Value.ToString();
                sli.Text = d.Name;
                depList.Add(sli);
            }

            IDepartment dpSel = rpDepartment.RepList[Convert.ToInt16(selVal) - 1];
            iStaticDep = dpSel.DepartmentID.Value;

            List<IEmployee> empSel = null;
            if (rpEmployee.RepList != null)
            {
                empSel = rpEmployee.RepList.FindAll(
                    (IEmployee emp) => { return emp.DepartmentID.Value == iStaticDep; });
            }

            ViewData["depSel"] = new SelectList(depList, "Value", "Text", selVal);

            return View(empSel);
        }
    }
}

运行时,所有员工显示如下

选择部门后,将显示该部门的员工。

对员工列表进行排序

现在,假设您希望 rpEmployee 对象(RepositoryEmployee 组件的实例)具有按姓氏排序员工的功能。您需要执行以下操作:

首先,您创建一个用于排序的比较器类,如下所示

internal class EmployeeLastnameComparer : IComparer<IEmployee>
{
    public int Compare(IEmployee e1, IEmployee e2)
    {
        return String.Compare(e1.LastName, e2.LastName);
    }
}

然后,在 Index 操作中的 rpEmployee.GetAll() 调用之前,调用动态装饰器,如下所示:

rpEmployee = (IRepository<IEmployee>)ObjectProxyFactory.CreateProxy(
    rpEmployee,
    new String[] { "GetAll" },
    null,
    new Decoration((x, y) =>
    {
        object target = x.Target;
        if (target.GetType().ToString() == "ThirdPartyHR.RepositoryEmployee")
        {
            List<IEmployee> emps = ((IRepository<IEmployee>)target).RepList;
            IEnumerable<IEmployee> query = emps.OrderByDescending(emp => emp,
                new EmployeeLastnameComparer()).ToList<IEmployee>();
            ((IRepository<IEmployee>)target).RepList = (List<IEmployee>)query;
        }
    }, null));

就是这样。现在,您的 HRMVC 将按姓氏排序显示员工。构建并运行它。您将看到员工按姓氏排序并显示如下:

当您选择一个部门时,将显示与该部门关联的员工,并按姓氏排序。

请注意,这里使用了 Lambda 表达式为该员工存储库对象提供匿名方法,以添加排序功能。当然,您也可以使用普通方法来实现排序逻辑。但是,由于此排序逻辑特别针对 rpEmployee 对象,并且不与其他对象共享,因此将其保留在匿名方法中更简洁。

这里有几点值得注意。首先,遵循了“根据需要扩展对象”的原则。当我们设计 RepositoryEmployee 组件时,排序要求尚不明确。在我们应用程序中使用 rpEmployee 对象时,很明显我们需要按员工姓氏对员工列表进行排序。因此,我们扩展了该对象以按员工姓氏对员工列表进行排序。其次,排序功能被附加到 rpEmployee 对象,而无需修改其组件或从中派生。第三,rpEmployee 对象是具有排序功能的 RepositoryEmployee 组件的唯一实例,独立于 RepositoryEmployee 创建的其他实例。

设计方面

假设您希望您的 HRMVC 应用程序处理进入/退出日志记录和安全检查的横切关注点。遵循“将方面设计为各自模块中的全局方法”的原则,这些方面作为单独的公共方法放置在 SysConcerns 类中,并打包在它们自己的模块中。以下是这些关注点的代码:

public class SysConcerns
{
    public static void EnterLog(AspectContext ctx, object[] parameters)
    {
        StackTrace st = new StackTrace(new StackFrame(4, true));
        Console.Write(st.ToString());
            
        IMethodCallMessage method = ctx.CallCtx;
        string str = "Entering " + 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(AspectContext ctx, object[] parameters)
    {
        IMethodCallMessage method = ctx.CallCtx;
        string str = "Exiting " + 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 AdminCheck(AspectContext ctx, object[] parameters)
    {
        Console.WriteLine("Has right to call");
        return;
    }
}

EnterLog 写入进入日志,而 ExitLog 写入退出日志。AdminCheck 写入日志并返回。

您可能需要根据您的系统要求修改这些方法。您还可以通过访问上下文、目标和输入参数中的各种信息来增强它们。要了解如何使用上下文、目标和输入参数来增强您的方面,请参阅 使用动态装饰器向对象添加方面

使用方面

定义了方面之后,您就可以在应用程序中根据需要将它们添加到对象中。

假设您希望在调用 RepositoryDepartment 组件的 rpDepartment 对象的 GetAll 方法之前添加安全检查方面。您还希望向同一对象添加进入日志和退出日志。在 Index 操作中的 rpDepartment.GetAll() 调用之前插入以下代码:

rpDepartment = (IRepository<IDepartment>)ObjectProxyFactory.CreateProxy(
    rpDepartment,
    new String[] { "GetAll" },
    new Decoration(new DecorationDelegate(SysConcerns.AdminCheck), 
                   new object[] { Thread.CurrentPrincipal }),
                   null);

rpDepartment = (IRepository<IDepartment>)ObjectProxyFactory.CreateProxy(
    rpDepartment,
    new String[] { "GetAll" },
    new Decoration(new DecorationDelegate(SysConcerns.EnterLog), null),
    new Decoration(new DecorationDelegate(SysConcerns.ExitLog), null));

然后,假设您想向 RepositoryEmployee 组件的 rpEmployee 对象 的 GetAll 方法添加进入日志和退出日志。只需在 Index 操作中的 rpEmployee.GetAll() 调用之前插入以下代码:

rpEmployee = (IRepository<IEmployee>)ObjectProxyFactory.CreateProxy(
    rpEmployee,
    new String[] { "GetAll" },
    new Decoration(new DecorationDelegate(SysConcerns.EnterLog), null),
    new Decoration(new DecorationDelegate(SysConcerns.ExitLog), null));

最后,假设您想跟踪访问了哪个部门。您可以在 Index 操作中使用选定的 Department 组件对象 dpSel 的部门 ID 属性 iStaticDep = dpSel.DepartmentID.Value 之前添加以下代码:

dpSel = (IDepartment)ObjectProxyFactory.CreateProxy(
    dpSel,
    new String[] { "get_DepartmentID" },
    new Decoration(new DecorationDelegate(SysConcerns.EnterLog), null),
    null);

现在,HRMVC 应用程序的横切关注点得到了解决。

请注意,方面是在需要时添加到对象的。组件类没有变化。并且只有用动态装饰器装饰的对象才具有方面,独立于组件类的其他对象。此外,方面可以应用于不同的对象,无论是同一类型还是不同类型。例如,SysConcerns.EnterLog 用于 rpDepartmentRepositoryDepartment 对象)、rpEmployeeRepositoryEmployee 对象)和 dpSelDepartment 对象)。

HRMVC 扩展版

为方便起见,扩展组件并添加方面后的 HRMVC 代码如下所示:

public class DepEmployeesController : Controller
{
    internal class EmployeeLastnameComparer : IComparer<IEmployee>
    {
        public int Compare(IEmployee e1, IEmployee e2)
        {
            return String.Compare(e1.LastName, e2.LastName);
        }
    }

    private IRepository<IEmployee> rpEmployee = null;
    private IRepository<IDepartment> rpDepartment = null;

    private static int iStaticDep = 0;

    public DepEmployeesController()
    {
        rpEmployee = new RepositoryEmployee();
        rpDepartment = new RepositoryDepartment();
    }

    public ActionResult Index()
    {
        rpDepartment = (IRepository<IDepartment>)ObjectProxyFactory.CreateProxy(
            rpDepartment,
            new String[] { "GetAll" },
            new Decoration(new DecorationDelegate(SysConcerns.AdminCheck), 
                           new object[] { Thread.CurrentPrincipal }),
            null);

        rpDepartment = (IRepository<IDepartment>)ObjectProxyFactory.CreateProxy(
            rpDepartment,
            new String[] { "GetAll" },
            new Decoration(new DecorationDelegate(SysConcerns.EnterLog), null),
            new Decoration(new DecorationDelegate(SysConcerns.ExitLog), null));

        rpDepartment.GetAll();

        rpEmployee = (IRepository<IEmployee>)ObjectProxyFactory.CreateProxy(
            rpEmployee,
            new String[] { "GetAll" },
            null,
            new Decoration((x, y) =>
            {
                object target = x.Target;
                if (target.GetType().ToString() == "ThirdPartyHR.RepositoryEmployee")
                {
                    List<IEmployee> emps = ((IRepository<IEmployee>)target).RepList;
                    IEnumerable<IEmployee> query = emps.OrderByDescending(emp => emp,
                        new EmployeeLastnameComparer()).ToList<IEmployee>();
                    ((IRepository<IEmployee>)target).RepList = (List<IEmployee>)query;
                }
            }, null));

        rpEmployee = (IRepository<IEmployee>)ObjectProxyFactory.CreateProxy(
            rpEmployee,
            new String[] { "GetAll" },
            new Decoration(new DecorationDelegate(SysConcerns.EnterLog), null),
            new Decoration(new DecorationDelegate(SysConcerns.ExitLog), null));

        rpEmployee.GetAll();

        if (Request.Form.GetValues("depSel") == null)
        {
            List<SelectListItem> depList = new List<SelectListItem>();
            SelectListItem sli = null;
            sli = new SelectListItem();
            sli.Value = "";
            sli.Text = "";
            depList.Add(sli);

            foreach (IDepartment d in rpDepartment.RepList)
            {
                sli = new SelectListItem();
                sli.Value = d.DepartmentID.Value.ToString();
                sli.Text = d.Name;
                depList.Add(sli);
            }

            ViewData["depSel"] = new SelectList(depList, "Value", "Text");
            return View(rpEmployee.RepList);
        }
        else
        {
            string selVal = "";
            selVal = Request.Form.GetValues("depSel")[0];

            List<SelectListItem> depList = new List<SelectListItem>();
            SelectListItem sli = null;
            foreach (IDepartment d in rpDepartment.RepList)
            {
                sli = new SelectListItem();
                sli.Value = d.DepartmentID.Value.ToString();
                sli.Text = d.Name;
                depList.Add(sli);
            }

            IDepartment dpSel = rpDepartment.RepList[Convert.ToInt16(selVal) - 1];
            dpSel = (IDepartment)ObjectProxyFactory.CreateProxy(
                dpSel,
                new String[] { "get_DepartmentID" },
                new Decoration(new DecorationDelegate(SysConcerns.EnterLog), null),
                null);

            iStaticDep = dpSel.DepartmentID.Value;

            List<IEmployee> empSel = null;
            if (rpEmployee.RepList != null)
            {
                empSel = rpEmployee.RepList.FindAll(
                    (IEmployee emp) => { return emp.DepartmentID.Value == iStaticDep; });
            }

            ViewData["depSel"] = new SelectList(depList, "Value", "Text", selVal);

            return View(empSel);
        }
    }
}

需要注意的一点是,ObjectProxyFactory.CreateProxy 返回的对象被重新赋值给最初指向目标变量。例如,rpEmployee 最初被赋值为 RepositoryEmployee(目标)的一个对象。调用 ObjectProxyFactory.CreateProxy 后,它被赋值为返回的对象,该对象是目标的代理。这一点很微妙但很重要。ObjectProxyFactory.CreateProxy 返回的对象是目标的代理。通过使用相同的变量来表示目标及其代理,原始代码保持不变。这意味着目标及其代理是可互换的。如果变量指向目标,则按原样使用目标。如果变量指向目标的代理,则在实际使用目标之前或之后执行其他功能。实际上,如果您删除所有调用 ObjectProxyFactory.CreateProxy 的代码,您将得到扩展对象并向对象添加方面之前的原始代码。

最后,在运行应用程序之前,您需要修改 Global.asax 方法,将控制台输出重定向到文件 hrlog.txt。这些修改仅针对此应用程序,因为进入/退出日志记录方面使用控制台。您的应用程序可能使用不同的日志记录机制。在这种情况下,您可能需要进行相应的更改。修改后的应用程序类如下所示:

public class MvcApplication : System.Web.HttpApplication
{
    public static void RegisterRoutes(RouteCollection routes)
    {
        routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

        routes.MapRoute(
            "Default", // Route name
            "{controller}/{action}/{id}", // URL with parameters
            new { controller = "DepEmployees", action = "Index", 
            id = UrlParameter.Optional } // Parameter defaults
        );
    }

    protected void Application_Start()
    {
        AreaRegistration.RegisterAllAreas();

        RegisterRoutes(RouteTable.Routes);

        //StreamWriter swLog = null;
        FileStream fileStream = null;

        string path = Path.GetDirectoryName(Server.MapPath("~"));
        if (!File.Exists(path + "\\hrlog.txt"))
        {
            fileStream = new FileStream(path + "\\hrlog.txt", FileMode.Create);
        }
        else
            fileStream = new FileStream(path + "\\hrlog.txt", FileMode.Truncate);

        TextWriter tmp = Console.Out;
        Application["origOut"] = tmp;

        StreamWriter sw1 = new StreamWriter(fileStream);
        Console.SetOut(sw1);

        Application["logStream"] = sw1;
    }

    protected void Application_End(object sender, EventArgs e)
    {
        TextWriter origStrm = (TextWriter)Application["origOut"];
        Console.SetOut(origStrm);

        StreamWriter tmp = (StreamWriter)Application["logStream"];
        Stream fileStream = tmp.BaseStream;

        tmp.Close();
        fileStream.Close();
    }
}

应用程序运行时,您将在文件 hrlog.txt 中看到以下输出。

   at HRMVCExtended.Controllers.DepEmployeesController.Index() in 
      C:\CBDDynDecoratorMVC\HRMVCExtended\Controllers\DepEmployeesController.cs:line 57
Entering ThirdPartyHR.RepositoryDepartment.GetAll()
Has right to call
Exiting ThirdPartyHR.RepositoryDepartment.GetAll()
   at HRMVCExtended.Controllers.DepEmployeesController.Index() in 
      C:\CBDDynDecoratorMVC\HRMVCExtended\Controllers\DepEmployeesController.cs:line 82
Entering ThirdPartyHR.RepositoryEmployee.GetAll()
Exiting ThirdPartyHR.RepositoryEmployee.GetAll()
   at HRMVCExtended.Controllers.DepEmployeesController.Index() in 
      C:\CBDDynDecoratorMVC\HRMVCExtended\Controllers\DepEmployeesController.cs:line 57
Entering ThirdPartyHR.RepositoryDepartment.GetAll()
Has right to call
Exiting ThirdPartyHR.RepositoryDepartment.GetAll()
   at HRMVCExtended.Controllers.DepEmployeesController.Index() in 
      C:\CBDDynDecoratorMVC\HRMVCExtended\Controllers\DepEmployeesController.cs:line 82
Entering ThirdPartyHR.RepositoryEmployee.GetAll()
Exiting ThirdPartyHR.RepositoryEmployee.GetAll()
   at HRMVCExtended.Controllers.DepEmployeesController.Index() in 
      C:\CBDDynDecoratorMVC\HRMVCExtended\Controllers\DepEmployeesController.cs:line 126
Entering ThirdPartyHR.Department.get_DepartmentID()

在源代码下载中,HRMVC 项目包含扩展组件和添加方面之前的初始代码。HRMVCExtended 项目包含扩展组件并添加方面之后的代码。

关注点

“根据需要添加方面到对象”和“根据需要扩展对象”的理念应用于 ASP.NET MVC 应用程序。ASP.NET MVC 开发人员可能会发现以下原则对于使用动态装饰器处理公共任务很有用。

  • 以通用方式设计组件以满足业务需求
  • 将方面设计为各自模块中的全局方法
  • 按需为对象添加方面
  • 按需扩展对象
© . All rights reserved.