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






4.67/5 (4投票s)
将组件、切面和动态装饰器原理应用于 ASP.NET 应用程序
引言
正如《组件、切面和动态装饰器》中所讨论的,应用程序开发涉及特定于应用程序类型的任务和独立于应用程序类型的任务。这种任务分离不仅有助于您根据与应用程序类型相关的特定技术来决定处理策略、部署策略、测试策略、UI 策略等,还有助于您系统地处理独立于应用程序类型(即通用)的任务,例如设计/扩展组件和解决横切关注点。本文讨论了开发这些通用任务的几项指导原则。动态装饰器用于遵循这些指导原则来解决这些任务。一个 WinForm 应用程序被用作示例。
在本文中,我将演示如何通过使用动态装饰器将这些原理和指导原则应用于不同类型的应用程序——ASP.NET 应用程序。
选择应用程序类型
.NET 世界中有几种应用程序类型:WinForm、ASP.NET、ASP.NET MVC 和 Silverlight。WinForm 应用程序提供丰富的用户交互和客户端处理能力。ASP.NET 应用程序具有通过 Web 进行部署和维护的优势。ASP.NET MVC 提供可测试性以及 ASP.NET 的优势。Silverlight 应用程序通过 Web 提供丰富的用户界面。不同类型的应用程序具有不同的技术特性,这些特性可能符合或不符合您的要求。您应该根据您的处理策略、部署策略、维护策略、测试策略、UI 策略等来选择您的应用程序类型。
不同类型的应用程序也有不同的应用程序编程模型、UI 元素、状态管理、事件处理模型等。一旦您为应用程序选择了应用程序类型,您就需要处理与应用程序类型相关的任务。
处理通用任务
除了特定于应用程序类型的任务之外,应用程序开发中还存在独立于特定应用程序类型的任务。例如,无论应用程序类型如何,您都面临着设计、扩展组件和解决横切关注点等任务。它们是所有应用程序类型的共同任务。
在文章《组件、切面和动态装饰器》中,给出了一组用于开发这些通用任务的原则。它们再次列举如下:
- 以通用方式设计组件以满足业务需求
- 将方面设计为各自模块中的全局方法
- 按需为对象添加方面
- 按需扩展对象
有关这些原则的详细讨论,请参阅《组件、切面和动态装饰器》。您可能还需要阅读文章《动态装饰器模式》以理解动态装饰器,并阅读文章《使用动态装饰器向对象添加切面》以理解使用动态装饰器的切面编程。
示例
在以下章节中,将讨论一个示例应用程序,以演示如何通过使用动态装饰器将上述原则应用于 ASP.NET 应用程序。
这里使用了在《组件、切面和动态装饰器》中讨论的相同问题作为示例。这次,我们选择 ASP.NET 应用程序作为应用程序类型,而不是 WinForm 应用程序。为了方便起见,我再次阐述问题如下。
问题
根据部门选择显示员工。
Components
假设有两个组件,Employee
和 Department
。对于 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
组件,遵循了“以通用方式设计组件以满足业务需求”的原则。这样,组件是稳定和封闭的。
HRSite
HRSite
是一个 ASP.NET 应用程序,它使用上述组件根据部门选择显示员工。由于它是一个 ASP.NET 应用程序,它遵循 ASP.NET 的应用程序编程模型和事件模型。代码如下所示:
public partial class DepEmp : System.Web.UI.Page
{
private IRepository<IEmployee> rpEmployee = null;
private IRepository<IDepartment> rpDepartment = null;
private int selectedDepId = 0;
private static int iStaticDep = 0;
public DepEmp()
{
iStaticDep = 0;
rpEmployee = new RepositoryEmployee();
rpDepartment = new RepositoryDepartment();
}
protected void Page_Load(object sender, EventArgs e)
{
if (IsPostBack)
{
if (DropDownList1.Items[0].Value == "")
selectedDepId = DropDownList1.SelectedIndex - 1;
else
selectedDepId = DropDownList1.SelectedIndex;
}
try
{
rpDepartment.GetAll();
DropDownList1.DataSource = rpDepartment.RepList;
DropDownList1.DataValueField = "DepartmentID";
DropDownList1.DataTextField = "Name";
DropDownList1.DataBind();
if (!IsPostBack)
DropDownList1.Items.Insert(0, new ListItem(""));
else
DropDownList1.SelectedIndex = selectedDepId;
rpEmployee.GetAll();
GridView1.DataSource = rpEmployee.RepList;
GridView1.DataBind();
}
catch (Exception ex)
{
//MessageBox.Show(ex.Message);
}
}
protected void DropDownList1_SelectedIndexChanged(object sender, EventArgs e)
{
IDepartment dpSel = rpDepartment.RepList[selectedDepId];
iStaticDep = dpSel.DepartmentID.Value;
List<IEmployee> empSel = null;
if (rpEmployee.RepList != null)
{
empSel = rpEmployee.RepList.FindAll(
(IEmployee emp) => { return emp.DepartmentID.Value == iStaticDep; });
}
GridView1.DataSource = empSel;
GridView1.DataBind();
}
}
运行时,所有员工显示如下

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

对员工列表进行排序
现在,假设您希望对象 rpEmployee
(RepositoryEmployee
组件的一个实例)具有按姓氏排序员工的功能。这是您需要做的。
首先,您创建一个用于排序的比较器类,如下所示
internal class EmployeeLastnameComparer : IComparer<IEmployee>
{
public int Compare(IEmployee e1, IEmployee e2)
{
return String.Compare(e1.LastName, e2.LastName);
}
}
然后,在事件处理程序 Page_Load
中,在 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));
就是这样。现在,您的 HRSite 会按员工的姓氏排序显示员工。构建并运行它。您会看到员工按姓氏显示和排序,如下所示:

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

请注意,这里使用 Lambda 表达式为这个员工存储库对象提供一个匿名方法来添加排序功能。当然,您可以为排序逻辑使用一个普通方法。然而,由于这个排序逻辑专门用于员工存储库对象 rpEmployee
,而不是由其他对象共享,因此将其保留在匿名方法中更简洁。
这里有几点值得注意。首先,遵循了“按需扩展对象”的原则。当我们设计 RepositoryEmployee
组件时,排序要求尚不明确。到我们在应用程序中使用对象 rpEmployee
时,很明显我们需要按员工的姓氏对员工列表进行排序。因此,我们扩展了这个对象以按员工的姓氏对员工列表进行排序。其次,排序功能附加到对象 rpEmployee
,而没有修改其组件或从其派生。第三,对象 rpEmployee
是 RepositoryEmployee
组件中唯一具有排序功能的实例,独立于 RepositoryEmployee
创建的其他实例。
设计方面
假设您希望您的 HRSite
应用程序处理进入/退出日志和安全检查的横切关注点。通过遵循“将切面设计为独立模块中的全局方法”的原则,这些切面被放置在一个名为 SysConcerns
的类中,作为独立的 public
方法,并打包在它们自己的模块中。以下是这些关注点的代码:
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
方法之前添加安全检查切面。您还想向同一个对象添加进入日志和退出日志。您在页面加载事件处理程序 Page_Load
中 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
方法添加进入日志和退出日志,只需在页面加载事件处理程序 Page_Load
中 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));
最后,假设您想跟踪哪些部门被访问,您可以在部门选择事件处理程序 DropDownList1_SelectedIndexChanged
中,在使用 Department
组件的选定对象 dpSel
的部门 ID 属性(iStaticDep = dpSel.DepartmentID.Value
)之前,添加以下代码。
dpSel = (IDepartment)ObjectProxyFactory.CreateProxy(
dpSel,
new String[] { "get_DepartmentID" },
new Decoration(new DecorationDelegate(SysConcerns.EnterLog), null),
null);
现在,HRSite
应用程序的横切关注点已得到解决。
请注意,切面是按需添加到对象中的。组件类没有发生任何变化。并且只有用动态装饰器装饰的对象才具有这些切面,独立于组件类的其他对象。此外,一个切面可以应用于不同的对象,无论是相同类型还是不同类型。例如,SysConcerns.EnterLog
用于 rpDepartment
(RepositoryDepartment
的一个对象)、rpEmployee
(RepositoryEmployee
的一个对象)和 dpSel
(Department
的一个对象)。
HRSite 扩展版
为了方便起见,扩展组件并添加切面后的 HRSite
代码如下所示:
public partial class DepEmp : System.Web.UI.Page
{
internal class EmployeeAgeComparer : IComparer<IEmployee>
{
public int Compare(IEmployee e1, IEmployee e2)
{
return DateTime.Compare(e1.DateOfBirth, e2.DateOfBirth);
}
}
private IRepository<IEmployee> rpEmployee = null;
private IRepository<IDepartment> rpDepartment = null;
private int selectedDepId = 0;
private static int iStaticDep = 0;
public DepEmp()
{
iStaticDep = 0;
rpEmployee = new RepositoryEmployee();
rpDepartment = new RepositoryDepartment();
}
protected void Page_Load(object sender, EventArgs e)
{
if (IsPostBack)
{
if (DropDownList1.Items[0].Value == "")
selectedDepId = DropDownList1.SelectedIndex - 1;
else
selectedDepId = DropDownList1.SelectedIndex;
}
try
{
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();
DropDownList1.DataSource = rpDepartment.RepList;
DropDownList1.DataValueField = "DepartmentID";
DropDownList1.DataTextField = "Name";
DropDownList1.DataBind();
if (!IsPostBack)
DropDownList1.Items.Insert(0, new ListItem(""));
else
DropDownList1.SelectedIndex = selectedDepId;
//Add sorting logic to employee list
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));
//Add entering log to employee list
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();
GridView1.DataSource = rpEmployee.RepList;
GridView1.DataBind();
}
catch (Exception ex)
{
//MessageBox.Show(ex.Message);
}
}
protected void DropDownList1_SelectedIndexChanged(object sender, EventArgs e)
{
IDepartment dpSel = rpDepartment.RepList[selectedDepId];
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; });
}
GridView1.DataSource = empSel;
GridView1.DataBind();
}
}
需要注意的一点是,ObjectProxyFactory.CreateProxy
返回的对象被重新赋值给最初指向目标的变量。例如,rpEmployee
最初被赋值为 RepositoryEmployee
的一个对象——即目标。在调用 ObjectProxyFactory.CreateProxy
之后,它被赋值为返回的对象,该对象是目标的代理。这很微妙但很重要。ObjectProxyFactory.CreateProxy
返回的对象是目标的代理。通过对目标及其代理使用相同的变量,原始代码保持不变。这意味着目标及其代理是可互换的。如果变量指向目标,则按原样使用目标。如果变量指向目标的代理,则在目标使用之前或之后执行附加功能。实际上,如果您删除所有调用 ObjectProxyFactory.CreateProxy
的代码,您将获得在扩展对象和向对象添加切面之前的原始代码。
最后,在运行应用程序之前,您需要修改 Global.asax 方法,将控制台输出重定向到一个文件 hrlog.txt。这些修改仅适用于此应用程序,因为进入/退出日志切面使用了控制台。您的应用程序可能使用不同的日志机制。在这种情况下,您可能需要进行相应的更改。
void Application_Start(object sender, EventArgs e)
{
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;
}
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 DepEmp.Page_Load(Object sender, EventArgs e)
in c:\CBDDynDecoratorSite\HRSiteExtended\DepEmp.aspx.cs:line 70
Entering ThirdPartyHR.RepositoryDepartment.GetAll()
Has right to call
Exiting ThirdPartyHR.RepositoryDepartment.GetAll()
at DepEmp.Page_Load(Object sender, EventArgs e)
in c:\CBDDynDecoratorSite\HRSiteExtended\DepEmp.aspx.cs:line 104
Entering ThirdPartyHR.RepositoryEmployee.GetAll()
Exiting ThirdPartyHR.RepositoryEmployee.GetAll()
at DepEmp.Page_Load(Object sender, EventArgs e) in
c:\CBDDynDecoratorSite\HRSiteExtended\DepEmp.aspx.cs:line 70
Entering ThirdPartyHR.RepositoryDepartment.GetAll()
Has right to call
Exiting ThirdPartyHR.RepositoryDepartment.GetAll()
at DepEmp.Page_Load(Object sender, EventArgs e)
in c:\CBDDynDecoratorSite\HRSiteExtended\DepEmp.aspx.cs:line 104
Entering ThirdPartyHR.RepositoryEmployee.GetAll()
Exiting ThirdPartyHR.RepositoryEmployee.GetAll()
at DepEmp.DropDownList1_SelectedIndexChanged(Object sender, EventArgs e)
in c:\CBDDynDecoratorSite\HRSiteExtended\DepEmp.aspx.cs:line 123
Entering ThirdPartyHR.Department.get_DepartmentID()
at DepEmp.Page_Load(Object sender, EventArgs e)
in c:\CBDDynDecoratorSite\HRSiteExtended\DepEmp.aspx.cs:line 70
Entering ThirdPartyHR.RepositoryDepartment.GetAll()
Has right to call
Exiting ThirdPartyHR.RepositoryDepartment.GetAll()
at DepEmp.Page_Load(Object sender, EventArgs e)
in c:\CBDDynDecoratorSite\HRSiteExtended\DepEmp.aspx.cs:line 104
Entering ThirdPartyHR.RepositoryEmployee.GetAll()
Exiting ThirdPartyHR.RepositoryEmployee.GetAll()
at DepEmp.Page_Load(Object sender, EventArgs e)
in c:\CBDDynDecoratorSite\HRSiteExtended\DepEmp.aspx.cs:line 70
Entering ThirdPartyHR.RepositoryDepartment.GetAll()
Has right to call
Exiting ThirdPartyHR.RepositoryDepartment.GetAll()
at DepEmp.Page_Load(Object sender, EventArgs e)
in c:\CBDDynDecoratorSite\HRSiteExtended\DepEmp.aspx.cs:line 104
Entering ThirdPartyHR.RepositoryEmployee.GetAll()
Exiting ThirdPartyHR.RepositoryEmployee.GetAll()
at DepEmp.DropDownList1_SelectedIndexChanged(Object sender, EventArgs e)
in c:\CBDDynDecoratorSite\HRSiteExtended\DepEmp.aspx.cs:line 123
Entering ThirdPartyHR.Department.get_DepartmentID()
在源代码下载中,项目 HRSite
包含在扩展组件和添加切面之前的初始代码。项目 HRSiteExtended
包含在扩展组件和添加切面之后的代码。
该应用程序在 Visual Studio 2010 默认 Web 服务器中进行了调试。如果您计划在 IIS 中托管这些应用程序,您可能需要开启 Windows 集成安全并赋予 ASPNET 账户足够的权限来测试它们。
关注点
“按需向对象添加切面”和“按需扩展对象”的理念已应用于 ASP.NET 应用程序。ASP.NET 开发人员可能会发现以下原则对于使用动态装饰器处理常见任务很有用。
- 以通用方式设计组件以满足业务需求
- 将方面设计为各自模块中的全局方法
- 按需为对象添加方面
- 按需扩展对象