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

方法对象辅助程序 - 为方法重构增加更多类

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.94/5 (5投票s)

2014年2月19日

CPOL

25分钟阅读

viewsIcon

20476

downloadIcon

129

本文描述了一种创新方法,它借助一个小型的库,能够轻松、成功地应用“用方法对象替换方法”的重构模式。

 

介绍 

被称为用方法对象替换方法的方法重构策略非常有用,值得快速回顾一下,以便为接下来的讨论奠定背景。网上有很多关于该策略的精彩解释。阅读上述链接中的文章加深了我对其的理解和欣赏。

促使使用此策略的问题是“一个长方法[或方法中的代码序列]使用了[许多]局部变量”,以至于无法轻易将其分解成多个参数列表较短的小方法。

密切相关的情况是现有方法参数过多。还可以使用其他几种方法来减少传递的参数数量。

  • 引入参数对象策略要求将一组“自然地结合在一起”的参数分组到一个新的类或结构中。理论上,完成此操作后,人们应该注意到与数据自然相关的行为,这可以带来额外的有益重构。
  • 保留整个对象策略建议,如果一个对象的多个属性作为参数传递给一个方法,为什么不直接传递整个对象呢?

如果上述两种方法都不能令人满意,“用方法对象替换方法”可以作为“重型武器”来解决最棘手的问题。本文及相关代码旨在帮助应用此策略。

背景

我倾向于对“保留整个对象”策略持保留意见,因为传递对象引用和传递该对象中选定属性值的快照之间存在巨大差异。如果我(按值)传递属性值的快照,被调用的方法可以对该有限的数据量做它想做的事情,而不会无意中改变原始对象状态的风险;但如果我传递原始对象的引用,被调用的方法可能会造成各种破坏。考虑到与其他开发人员实时协作以及多个人在可能很长一段时间内对代码进行更改等因素,对我来说,只向方法暴露它们真正需要访问的数据似乎更安全。

当需要分组的参数数量变大时,“引入参数对象”就会遇到障碍。这个障碍实际上已作为“用方法对象替换方法”策略最初阐述的“特性”之一: “为新类提供一个构造函数,它接受源对象和每个参数。”如果该构造函数需要接受超过一些小数字 n(根据个人喜好,可能在 n=5 左右)的参数,那么,那不是我想做的事情。虽然调用一个参数过多的构造函数(或任何方法)可能被视为通过应用给定策略所带来的整体改进中必然不理想的一部分,但它也将涉及用一种“代码异味”换取另一种,人们可能会真诚地希望有更好的替代方案。“参数过多”是我绝对想要解决的问题之一——而不是我愿意接受作为其他问题长期解决方案一部分的问题。

(对我来说)很清楚,应该处理但经常被忽略的基本问题之一是,“方法(或构造函数)参数列表过长有什么问题?为什么这会使代码难以阅读、理解和维护?”抱怨固有的代码复杂性和低抽象级别可能没错,但就本次讨论而言,我将假设我们无法解决这些问题。因此,我们有大量数据需要在某个点,或者可能在多个点,以零碎的方式处理。至少,我们需要一次性将所有这些数据都塞进一个对象中,以便于操作。

C# 的对象初始化器语法,结合良好的命名实践,可以提供帮助。让我们假设我们正在创建一个新的“参数对象”类并控制属性命名。此外,我们理解代码并可以开发有意义的属性名称,从而产生良好的命名,促进下一个接手人或我们自己几天/几周/几个月后的理解。然后,我们可以将每个参数(以其作为属性的新形式)与一个名称关联起来,该名称在我们要创建的新类的上下文中赋予其尽可能多的意义。如果我们正在创建一个“方法对象”来帮助我们重构一个长而复杂的方法,该方法已经接收了太多参数,或者如果我们使用一种不那么有效的方法重构,最终也会接收太多参数,那么这也适用。

因此,由于我们正在创建方法对象或参数对象,并为其属性赋予非常有意义的名称,通过使用对象初始化器语法而不是向方法传递位置参数,我们能够向代码阅读者显示更多信息——特别是,关于我们收集的数据位将意味着什么以及它们在方法对象的新上下文中如何使用的强烈提示。

为了确保这一点阐明,请考虑我刚才从 Code Review Stack Exchange 上发布的一个问题中截取的方法签名,该问题涉及参数过多……?

XmlNode LastNArticles(
    int NumArticles,
    int Page,
    String Category = null, 
    String Author = null,
    DateTime? Date = null,
    String Template = null,
    ParseMode ParsingMode = ParseMode.Loose,
    PageParser Parser = null);

方法调用可能看起来像这样

var node = LastNArticles(20, 5, Categories.Miscellaneous, currentAuthor, selectedDate, null,
    ParseMode.Strict, selectedParser);

无论对于已经了解代码及其所属应用程序的人来说,这样的调用是否“足够清晰”,如果我们能够并且确实明确指定每个参数被分配到哪个实参值,会不会更容易理解?可以这样写:

var node = LastNArticles(
    NumArticles: 20,
    Page: 5,
    Category: Categories.Miscellaneous,
    Author: currentAuthor,
    Date: selectedDate,
    ParseMode: ParseMode.Strict,
    Parser: selectedParser);

这已经是合法的 C# 语法,尽管我们通常不鼓励将其用于上述目的。(请注意,我们可以省略 Template 的初始化,因为方法签名指定了该参数的默认值。通过显式命名参数,我们还可以重新排列它们的顺序,按字母顺序或根据其他方案列出它们,从而更容易查找给定参数被初始化到的值。)

这看起来非常像对象初始化器语法,两者之间最大的区别在于,是使用赋值运算符还是冒号来匹配参数名称和要赋的值。

如果我们要为这个方法构造一个参数对象,它可能被称为 `ArticleSearchParameters`,并可能像这样初始化:

var parameters = new ArticleSearchParameters
{
    Author = currentAuthor,
    Category = Categories.Miscellaneous,
    Date = selectedDate,
    NumArticles = 20,
    Page = 5,
    ParseMode = ParseMode.Strict,
    Parser = selectedParser
};

尽管这在向代码和阅读器提供信息方面与前面显示的方法调用基本相同,但这是我们在 C# 中更习惯看到的语法——并且不会被风格检查工具标记为“冗余”!

在我们急于创建新类和结构来替换冗长的参数列表之前,我们最好停下来考虑一下围绕方法参数的语义比与对象创建相关的语义要丰富得多。对于方法,我们有 `in`、`ref` 和 `out` 参数,并且方法签名也明确说明了哪些参数是必需的,哪些是可选的,如果未向可选参数传递参数值,则提供默认值。当我们使用“引入参数对象”或“用方法对象替换方法”策略时,我们放弃了所有这些!

“如何才能在转向使用对象传递参数时,不必放弃方法规范和调用语义的优势呢?”正是这个问题引导我走上了一条设计和开发之路,最终创造出了一些潜在有用的东西:一个 `MethodObjectHelper` 类,它提供了特定的、有目的的基础设施,以及少量自定义属性和异常类型;以及一个相关的标准模式,用于创建健壮的方法对象,而无需以不幸的副作用形式放弃与方法签名和调用相关的益处。诚然,在这种方法中,`ref` 和 `out` 参数的模拟是不完美的;事实上,所有事情都需要比指定方法签名和调用(C# 中直接作为语言的句法特性使其变得经济)更多的思考、仪式和敲击——但我认为代码和相关模式可能会有所裨益。因此,我在此发布这个“系统”,看看 C# 开发社区中有多少人同意我的看法。而且,我认为 VB.NET 开发人员也可以使用它,只需进行适当的翻译。:)

我将把 `MethodObjectHelper` 类放在核心位置,并称我将要描述的方法为“MOH 辅助”方法,用于“用方法对象替换方法”的过程和结果。

`MethodObjectHelper` 及其相关属性、异常和代码工件杂项可供下载,如果我尝试详细解释它们的工作原理,这将成为一篇非常长的文章(没有人会阅读)。相反,我将以一种非常直接、算法化(明确定义、按步骤操作)的方式解释如何使用它们来构建一个 MOH 辅助方法对象类。

使用代码

第一步:如果还没有方法签名,请构建一个。请考虑所有常用选项并自由使用它们。

  • 哪些参数(如果有的话)需要声明为 `ref` 或 `out` 参数?
  • 哪些参数必须由调用代码初始化,哪些参数可以给定默认值,以便调用代码的初始化是可选的?
  • 如果方法有返回值,其返回值和类型应该是什么?

如果你已经有一个参数过多的“大丑陋方法”(BUM),那就更好了,因为你可以跳过一步,或者至少你已经领先一步了。如果现有方法目前正在引用其上下文中从其角度来看是“全局”的值,则必须通过将它们添加到其参数列表来保留对这些值的访问。(事情可能在好转之前看起来更糟。)


第二步:构建一个类,其中第一步中生成的 BUM 的每个参数都成为一个属性。在此阶段,实际开始编写类是合适的;只需创建一个与 BUM 的每个参数具有相同类型和名称的属性即可。将它们都赋予 `public` 访问修饰符——尽管在正常情况下,由于稍后将解释的实现技巧,它们在运行时实际上不会对外部代码可见。`In` 参数只需要一个 `get` 访问器——现在,请相信我这一点。`Ref` 和 `out` 参数应该同时具有 `get` 和 `set` 访问器。除非你的方法签名将返回类型指定为 `void`,否则还要创建一个表示返回值的属性,称为 `Result` 或 `ReturnValue`。

现在,将所有访问器都设为空;我们将在后面的步骤中对它们进行特殊处理。


第三步:用属性装饰第二步中创建的属性,这些属性将定义它们如何使用。`In` 和 `ref` 参数属性需要用 `ParameterProperty` 属性装饰。`Ref` 和 `out` 参数属性必须用 `ResultProperty` 属性装饰。请注意,`ref` 参数必须同时用这两个属性装饰。

`ResultProperty` 属性不带参数。

`ParameterProperty` 有一个必填参数,它设置一个名为 `MustBeInitialized` 的 `Boolean` 属性。

  • 如果传递 `true`,则当方法对象使用时(我称之为它的调用或激活时间)必须初始化被修饰的属性;像方法一样(除非它最终被视为数据容器,虽然可能但不是特别推荐),方法对象将具有非常短暂的生命周期。
  • 如果传递 `false`,则调用/激活方法对象的代码可以不初始化被装饰的属性。

与可选方法参数一样,每个可选初始化属性都应该有一个默认值,可以通过两种方式指定:

  • 最直接的方法是将 `ParameterProperty` 属性的 `DefaultValue` 属性设置为被装饰属性所需的默认值。此值可以是“常量表达式、typeof 表达式或数组创建表达式”(引用如果尝试分配其他任何内容时出现的错误消息),并且与属性的类型一致。
  • 如果这些限制过于严格,任何可以分配给被装饰属性的默认值都可以在代码中设置。在控制流中有一个特定的点适合这样做,我将在这些说明的后面适当的位置提及。如果您发现有必要采用这种技术,最好将 `ParameterProperty` 属性的 `DefaultValueSetInCode` 属性设置为 true,它具有文档目的:它引导代码的读者在适当的惯常位置查找默认设置代码,并起到通知作用,即开发人员并没有(草率地、可能灾难性地)省略为属性分配默认值。

第四步:您可能已经猜到,`ParameterProperty` 和 `ResultProperty` 属性具有实际的功能用途,因为 `MethodObjectHelper` 类中的代码实际上在运行时使用它们。为了使用我在此介绍的 MOH 辅助方法,每个方法对象都必须拥有 `MethodObjectHelper` 类的一个实例。辅助实例通过组合插入,使用此代码(请复制并粘贴):

/// <summary>
/// Provides input, output and associated validation services.
/// </summary>
protected readonly MethodObjectHelper _methodObjectHelper = new MethodObjectHelper();

考虑这行代码时,最可能出现的“为什么?”问题是:“为什么该字段具有受保护的访问权限?”答案是方法对象类可以是基类(被继承或派生),并且每个方法对象实例都恰好需要一个 `MethodObjectHelper` 实例。


第五步:方法对象必须有一个显式默认构造函数。同样,我将请您复制并粘贴:

/// <summary>
/// Initializes a new instance of the <see cref="MethodObject"/> class.
/// The protected access modifier prevents instantiation by classes that are not derived from this class.
/// </summary>
protected MethodObject()
{
}

当然,请将“`MethodObject`”的实例替换为您的类名,我希望它具有很强的描述性。关于命名您的类的指导,请考虑这个例子:如果您正在替换的方法名为 `Gonkulate`,或者本应命名为 `Gonkulate`,那么您的方法对象类应该命名为 `Gonkulator`。:)


第六步:使用一个方法,该方法从访问属性的表达式中提取属性的名称,以提供一组完整的属性名称作为 `public static readonly string`。示例如下:

/// <summary>
/// Provides the name of the <see cref="Name"/> property as a <see cref="String"/>.
/// </summary>
public static readonly string NameOfName = MemberInfoHelper.GetMemberName(() => Instance.Name);

您定义的所有参数和结果属性的名称都应以类似方式提供。

执行此步骤时,您还需要一个类的静态实例,供属性名称提取器方法使用。以下是该实例化的代码(请复制粘贴,然后替换类名):

/// <summary>
/// Facilitates initialization of the static readonly property names; probably
/// shouldn't be used for other purposes. 
/// </summary>
private static readonly MethodObject Instance = new MethodObject();

第七步:现在是编写特殊方法的时候了,该方法提供对方法对象类功能的公共访问。我建议创建两个重载,它们使用不同类型的数据传输对象来协助输入和输出,并强制执行在开发参数和结果属性时通过属性表达的要求。我更喜欢将此方法命名为 `Invoke`;如果您喜欢,也可以将其命名为其他名称——但请保持一致。:)

`Invoke`(无论其名称如何)的重载都是简短的样板式方法,它们调用辅助对象中的 `Activate` 方法,该方法执行所有有趣且有用的基础设施任务,例如验证数据传输对象、初始化参数、将输出数据(本质上是 `ref` 和 `out` 参数)绑定到调用上下文,以及调用方法对象提供的一个入口点方法——这将在未来步骤中处理;现在不必担心。

我将在此处提供这两个重载的代码(为简洁起见,删除了大部分代码内注释),以便您可以比较和对比它们。您的实现应与此处给出的代码不同的唯一方面是参数值默认代码,该代码紧随第一个参数不为 null 的验证之后。

public static string Invoke(
    Dictionary<string, object> parameterValueByName,
    Action<Dictionary<string, object>> bindResultValueByName = null)
{
    if (parameterValueByName == null)
    {
        throw new ArgumentNullException(MethodObjectHelper.ParameterInitializer);
    }

    // Parameter value defaulting code:
    if (!parameterValueByName.ContainsKey(NameOfTimestamp))
    {
        parameterValueByName.Add(NameOfTimestamp, DateTime.Now);
    }

    var instance = new MethodObject();
    instance._methodObjectHelper.Activate(instance, parameterValueByName, bindResultValueByName);
    return instance.Result;
}

public static string Invoke(
    dynamic parameterPackage,
    Action<dynamic> bindResultPackage = null)
{
    if (parameterPackage == null)
    {
       throw new ArgumentNullException(MethodObjectHelper.ParameterInitializer);
    }

    // Parameter value defaulting code:
    if (!DynamicHelper.ContainsProperty(parameterPackage, NameOfTimestamp))
    {
        parameterPackage = DynamicHelper.SetPropertyValue(parameterPackage, NameOfTimestamp, DateTime.Now);
    }

    var instance = new MethodObject();
    instance._methodObjectHelper.Activate(instance, parameterPackage, bindResultPackage);
    return instance.Result;
}

为提供两种重载及其特有的数据传输对象类型是有道理的。简言之:使用 `Dictionary` 对象更安全,但使用匿名 `dynamic` 对象更方便(且可读)。请比较以下显示这两种技术的简短示例:

var returnValue = MethodObject.Invoke(
    new Dictionary<string, object>
    {
        { MethodObject.NameOfName, "Artemus Gordon" },
        { MethodObject.NameOfRating, 0 }
    });

var returnValue = MethodObject.Invoke(
   new
   {
       Name = "Artemus Gordon",
       Rating = 0
   });

实际的区别在于,在第一种情况下,如果您在键入字典键/参数属性名称时犯了错误,Visual Studio 会立即通知您;但在定义匿名对象时,您可以为属性赋予任何您喜欢的名称,并且 Visual Studio 无法验证它们是否与任何特定内容匹配,或者是否符合 C# 语言规范对属性名称施加的任何限制以外的任何限制。考虑到这一点以及一种选项与另一种选项相比所需的键入量,可以决定是支持一种用法还是另一种用法,或者两者都支持。因此,根据您的决定——或者您是否选择推迟决定——应包含 `Invoke` 方法的一个或两个重载;在这种情况下,我建议在此期间支持两种变体。

请注意,(主要是为了简洁起见)上述示例省略了结果绑定操作参数。

提供两个 `Invoke` 重载并支持两种数据传输对象类型是否会鼓励不一致?也许吧,但这是否必然是一种有害的不一致?值得注意的是,使用一种方法编写的代码可以很容易地修改为使用另一种方法;例如,修改使用字典的代码以转而使用匿名对象的过程主要涉及使用 Delete 和/或 Backspace 键。因此,“初稿”代码可以很容易地使用更安全的基于字典的方法编写,然后“清理”为使用更易读的基于匿名类型对象的方法。

`MethodObjectHelper.Activate` 方法执行运行时验证,这些验证将迅速、主动且果断地告知开发人员(或者,根据异常的处理方式,可能是应用程序用户)在设置和使用方法对象类时所犯的任何错误——通常是指与本文所述规则和程序的所有有害偏差。Visual Studio 和编译器目前无法进行这种检查,这很遗憾,但由辅助类执行的全面验证是次优选择。

(有些读者可能不知道 .NET 中的 `dynamic` 对象实际上只是薄薄包装的 `Dictionary` 实例。这个事实及其一些影响在开发和测试此类代码时会显现出来——详细解释会很有趣,但遗憾的是与本文不直接相关。下载代码中提供的 `DynamicHelper` 类可能对读者有一些兴趣。)

早些时候,我承诺指出应在何处将参数默认值设置为无法分配给 `ParameterProperty` 属性的 `DefaultValue` 属性的值。以防您错过:上面有一个示例,在两个 `Invoke` 重载中,代码都安排将 `Timestamp` 属性默认设置为 `DateTime.Now`。

请注意 `MethodObject` 实例的非常短暂和私有(`Invoke` 方法局部)的存在。在正常情况下,它不打算暴露给外部代码。但是,在特殊情况下,可能需要将对象的大部分或所有属性作为结果数据返回——在这种情况下,实例本身可以直接由 `Invoke` 方法返回。可以控制对对象功能的访问,以便不公开任何方法和任何属性 set 访问器,这是本文示例和随附的可下载代码中遵循的约定,也是我倾向于推荐的。因此,从外部代码的角度和目的来看,该对象可以严格地充当(只读)数据传输对象。

关于这一步的最后一点,我将简要讨论结果绑定操作。它们只是委托,旨在获取任何 `ref` / `out` 参数模拟(结果属性)的值,这些值在方法对象代码被调用后(该操作将在下面简要讨论)由辅助类的 `Activate` 方法自动打包以供导出,并在可选地对其进行任何必要/适当的计算后,将这些值复制到调用上下文中的变量中。与实际方法调用中(减去可选计算)自动完成的情况不同,在使用 MOH 辅助方法对象时,必须提供少量显式代码来完成这项工作。这是一点小小的代价,并且并非在所有情况下都需要;事实上,就像在方法调用中使用 `ref` 和 `out` 参数一样,如果这是您的偏好,它可以被“虔诚地”避免。


第八步:`MethodObjectHelper` 类的 `Activate` 方法非常强大,但此时它要求您对参数和结果属性的实现进行一项更改。(回想一下,我们之前将它们设计得尽可能简单;现在是时候扩展并完成访问器主体的编码了。)要求这些属性使用 `MethodObjectHelper` 类中的字典作为其支持存储,并提供了 `GetValue` 和 `SetValue` 两个方法来促进这一点。下面是一个属性完整代码的示例——显示任意代码可以像往常一样包含在属性访问器中;但通常使用类字段作为支持存储的地方,现在使用 `GetValue` 和 `SetValue` 代替。

[ParameterProperty(false, DefaultValue = 3)]
[ResultProperty]
public int Rating
{
    get { return (int)_methodObjectHelper.GetValue(NameOfRating); }

    protected set
    {
        var adjustedValue = Math.Min(Math.Max(value, 1), 5);
        _methodObjectHelper.SetValue(NameOfRating, adjustedValue);
    }
}

在示例中,`Rating` 被用作 `ref` 方法参数的模拟,因为它用于方法对象的输入和输出,所以它应该有一个 `set` 访问器是有意义的。方法对象实例中的方法代码(尚未编写/讨论)可能或将需要修改 `Rating` 作为参数或其默认值 3 所赋予的任何初始值。

`Set` 访问器需要是 `public` 或 `protected`,除非你想密封该类,从而阻止继承。我认为最明智的做法是遵守将其设为 `protected` 的约定。这样,如果方法对象类的实例在调用后被返回(这当然是可能的),它将对外部代码是只读的。

如果方法对象类最终包含不属于参数或结果属性的额外属性(即未用 `ParameterProperty` 和/或 `ResultProperty` 属性修饰),它们可以使用 MOH 的字典作为支持存储,但这不是必需的;仅对于参数和结果属性,使用它才是必需的。


第九步是我们最终为我们如此精心构建的软件“机器”提供动作。这是方法代码编写的开始步骤。我无法预测或规定方法代码将或应该是什么样子;它必然会在不同的实现之间差异巨大。但是,可以说明或定义的是,将只有一个实例方法,该方法将由辅助对象的 `Activate` 方法自动调用,并且(就像程序的 `Main` 方法一样)方法对象内所有在导入值绑定到参数属性和结果属性值打包导出之间执行的其他代码,都必须由该单个自动调用的实例方法运行。

使用一个属性来告诉 `Activate` 调用哪个方法。示例如下:

[InvokeOnActivation]
protected void Compute()
{
    // ...
}

我已将入口点方法命名为 `Compute`。(显然,如果您选择将我命名的 `Invoke` 方法重命名为 `Compute`,您就必须为该方法选择一个不同的名称。)名称在功能上无关紧要;是 `InvokeOnActivation` 属性使 `Activate` 能够识别并调用该方法。

进一步的限制是该方法必须返回 `void` 且不带参数。这完全合情合理,因为方法对象的参数属性实际上是该方法的参数,而其主要任务是计算并设置结果属性的值。`Invoke` 方法可以返回主要返回值(根据惯例,存储在名为 `Result` 或 `ReturnValue` 的结果属性中),如果还有其他导出(类似于 `ref` 和 `out` 参数),这些导出会连同方法对象的主要返回值一起提供给调用代码可选地传递给 `Invoke` 的结果绑定委托。当然,也可能不需要返回任何值,在这种情况下,`Invoke` 方法应返回 `void`,不提供结果绑定操作参数,也不向 `Activate` 方法传递任何参数。(通过正常方法调用可用的所有变体都可以轻松方便地覆盖。)

强调一点:`Invoke` 是返回 `void` 还是某种类型的值并不重要,它是否具有必需的结果绑定委托参数、可选的结果绑定委托参数或没有这样的参数也不重要;这些细节必须由特定实现的需要来决定。


是否有第十步?是的,有。这一步是在方法对象类的范围内小心翼翼、充满爱意地重构原始代码。尽管在本文中它需要很少的解释,但它或许是最重要的一步,所有其他解释和相关代码都旨在促进这一步。所讨论的技术就是为了将代码从与上下文紧密且复杂的耦合问题中解放出来,以便更容易地将其操作成最有利于应用程序长期利益和福祉的任何形式。

在这一步,你还需要修改原始的上下文代码,使其调用方法对象,而不是之前所做的事情(调用一个带有大量参数的方法?),当然。


第十一步要求您测试您的代码,并在必要时进行调试,最终确保它按您预期的方式工作。

结论

以下是重构步骤的简要概述:

  1. 构建一个完全独立的方法签名;或者,如果您正在处理现有方法,则根据需要修改其签名,以确保其完全独立性。
  2. 构造一个类,该类具有与步骤 1 中创建的方法签名的每个参数和返回值(如果有)对应的属性。(按照惯例,将表示返回值的属性命名为 `Result` 或 `ReturnValue`。)
  3. 用属性装饰属性。
    • 与 `in` 参数对应的属性必须使用 `ParameterProperty` 属性进行装饰。
      1. 必填参数为 `mustBeInitialized`,表示调用代码是否必须为该参数提供初始值。
      2. 如果指定了 `mustbeInitialized = false`,则应设置 `DefaultValue` 属性。
      3. 如果需要使用无法分配给 `DefaultValue` 的默认值,请设置 `DefaultValueSetInCode = true` 并计划稍后在代码中设置默认值。
    • 与 `out` 参数和原始方法签名的返回值(如果有)对应的属性必须使用 `ResultProperty` 属性进行装饰。
    • 与 `ref` 参数对应的属性必须同时用 `ParameterProperty` 属性和 `ResultProperty` 属性装饰,如上所述。
  4. 声明并初始化一个包含 `MethodObjectHelper` 实例的受保护只读字段。
  5. 为方法对象类定义一个具有 `protected` 访问权限的空默认构造函数。
  6. 将每个属性的名称作为静态只读字符串提供。(有关操作建议,请参阅本文和示例代码。)
  7. 按照上述示例和详细讨论,提供 `public static Invoke` 方法的一个或两个重载。`Invoke` 方法(如果您愿意,可以重命名)必须:
    1. 确保参数初始化器参数不为 null。[必填]
    2. 处理任何必要的代码内赋值,将默认值赋给参数属性。[可选]
    3. 创建方法对象实例,并将其连同参数初始化器对象和结果绑定操作参数(如果使用)一起传递给方法对象助手实例的 `Activate` 方法。[必填]
    4. 如果合适,返回 `Result` / `ReturnValue` 属性的值。[可选]
  8. 确保所有参数和结果属性都使用方法对象辅助实例的内部字典作为支持存储,通过 `GetValue` 和 `SetValue` 方法访问它。
  9. 创建一个不带参数的受保护 void 方法,并用 `InvokeOnActivation` 属性装饰它。此方法是方法对象所有计算的入口点。(根据惯例,考虑将此方法命名为 `Compute`。)该方法必须确保将值分配给所有结果属性。
  10. 重构原始代码,使其包含在或被前一步定义的入口点方法调用。修改上下文代码以调用方法对象。
  11. 测试您的代码并确保它正常工作。

如果您需要执行“用方法对象替换方法”的重构策略,请考虑使用本文介绍的代码和流程作为辅助,以一种彻底标准化但非常灵活的方法轻松实现成功结果。

MOH 辅助的“用方法对象替换方法”的方法是奇妙的福音,还是过度工程的@#$%练习?您来决定。

© . All rights reserved.