【问题标题】:Programmatically insert a method call on each property of a class以编程方式在类的每个属性上插入方法调用
【发布时间】:2011-09-27 12:37:15
【问题描述】:

我的问题是基于this article

基本上一个类可以实现一个 Freezable 方法来确保一旦对象进入 Frozen 状态就不能更改任何属性。

我有一个遵循这个设计的界面

public interface IFreezableModel
{
    void Freeze();
    bool IsFrozen{get;}
}

目标是确保一旦调用 Freeze 方法,IsFrozen 属性设置为 True 并且对象的属性不能再更改。

为了简化,我将使用抽象基类:

public abstract class BaseFreezableModel : IFreezableModel
{
    public void Freeze()
    {
        _isFrozen = true;
    }
    public bool IsFrozen
    { 
       get {return _isFrozen;}            
    }
    protected ThrowIfFrozen()
    {
       if (IsFrozen)
           throw new Exception("Attempted to change a property of a frozen model");
    }
}

这样我就可以上课了

public class MyModel : BaseFreezableModel
{
     private string _myProperty;
     public string MyProperty
     {
        get{return _myProperty;}
        set 
        {
           ThrowIfFrozen();
           _myProperty = value;
        }
     }
 }

这一切都很好而且很简单,但是我可以采用哪种策略来确保所有属性都遵循上述模式? (除了写 setter 和 getter)

这些是我想出的替代方案:

  • 找到一种机制,或许可以使用 emit 将方法注入每个属性设置器。但我不知道该怎么做,我可能会遇到哪些潜在问题(以及需要多长时间)。如果有人知道这一点并能指出我的方向,那就太好了。

  • 在构建过程中使用一些模板,以便在编译时间之前插入对 OnCheckFrozen 的调用。这具有非常易于理解并且可以用于类似场景的优点。

  • 找到一个可以为我完成所有这些工作的框架,但这只是一个极端情况,因为我不允许在这个项目上使用外部框架。

您会使用什么解决方案来完成此任务?

【问题讨论】:

  • 我看不出这个类是如何线程安全的:冻结状态与其他线程不同步。
  • @Luca Piccioni 它们只是我能想到的最简单的例子。但是,一旦类被冻结,它将是线程安全的(文章中显示的少数情况)
  • 我们有不同的线程安全概念。链接的文章明确地说:只读结构不是线程安全的。如果您这样做是为了线程安全,我建议您更改设计。我的 2 美分。
  • @LucaPiccioni 是的,我已经阅读了这篇文章。我还重新阅读了我写的问题,我很确定我没有提到任何关于线程安全的内容。这篇文章还有一个非常具体的标题:“只读和线程安全是不同的” 我很确定我也没有错过。当然,我可以重新表述我之前提到的内容:对于最简单的情况(值类型属性、不可变对象),一旦对象被冻结,我可以假设它是线程安全的。
  • 就个人而言,我会将OnCheckFrozen 更改为ThrowIfFrozen

标签: c# .net


【解决方案1】:

您正在这里进入Aspect Oriented Programming 的世界。您可以使用PostSharp 在 5 分钟内完成此类功能 - 但您似乎不允许使用外部框架。因此,您的选择归结为实现您自己的非常简单的 AOP 框架,或者只是硬着头皮为每个属性设置器添加检查。

就我个人而言,我只会在任何属性设置器中编写检查。这可能不像你想象的那么痛苦。您可以编写一个 Visual Studio 代码 sn-p 来加快该过程。您还可以编写一个智能单元测试类,该类将使用反射扫描冻结对象的所有属性并尝试设置一个值 - 使用如果没有抛出异常,则测试失败..

编辑响应 VoodooChilds 的请求.. 这是一个使用 NUnit 和出色的 FluentAssertions 库的单元测试类的快速示例。

[TestFixture]
public class PropertiesThrowWhenFrozenTest
{
    [TestCase(typeof(Foo))]
    [TestCase(typeof(Bar))]
    [TestCase(typeof(Baz))]
    public void AllPropertiesThrowWhenFrozen(Type type)
    {
        var target = Activator.CreateInstance(type) as IFreezable;

        target.Freeze();

        foreach(var property in type.GetProperties())
        {
            this.AssertPropertyThrowsWhenChanged(target, property);
        }
    }

    private void AssertPropertyThrowsWhenChanged(object target, PropertyInfo property)
    {
        // In the case of reference types, setting the property to null should be sufficient
        // to test the behaviour...
        object value = null;

        // In the case of value types, just create a default instance...
        if (property.PropertyType.IsValueType)
            value = Activator.CreateInstance(property.PropertyType);

        Action setter = () => property.GetSetMethod().Invoke(target, new object[] { value });

        // ShouldThrow is a handy extension method of the FluentAssetions library...
        setter.ShouldThrow<InvalidOperationException>();
    }
}

此方法使用参数化单元测试来传递正在测试的类型,但您同样可以将所有这些代码封装到通用基类中(其中 T : IFreezable)并为每种正在测试的类型创建扩展类,但是一些测试运行者不喜欢在基类中进行测试.. *ahem*Resharper!ahem

EDIT 2,为了好玩,这里有一个 Gherkin 脚本示例,可用于为此类事情创建更灵活的测试:)

Feature: AllPropertiesThrowWhenFrozen
    In order to make sure I haven't made any oversights in my code
    As a software developer
    I want to be able to assert that all properties of a class throw an exception when the object is frozen

Scenario: Setting the Bar property on the Foo type
  Given I have an instance of the class MyNamespace.MyProject.Foo
    And it is frozen
  When I set the property Bar with a value of 10
  Then a System.InvalidOperationException should be thrown

【讨论】:

  • 是的,我知道。 Postsharp 就在我的脑海里,到目前为止,我一直在考虑做你在这里建议的事情。问题是单元测试不会那么简单。 +1 代码 sn-p。
  • 我将支持使用 PostSharp 的概念。它很可能是我见过的最好的 AOP 框架。但是,如果您的公司对使用 3rd 方框架感到不安,那么 IL Weaving 的想法可能会让他们大吃一惊。
  • @Phil 我经常使用类似的方法来测试一个类的所有属性是否引发 NotifyPropertyChanged 事件等。
  • 如果您确实想使用面向方面的编程,但使用第三方工具会有问题,请考虑使用第一方 (Microsoft) 工具 CciSharp(尽管 Microsoft 不提供支持)。默认情况下,它不附带 freeze mutator,但它确实提供了几个 property mutator 示例,因此创建一个用于 freeze 应该很容易。
  • @MattDavey:如果可能,您能否发布一个“智能单元测试类,该类将使用反射扫描冻结对象的所有属性并尝试设置值”的示例?跨度>
【解决方案2】:

正如 Matt 已经提到的,您可以使用面向方面的编程。另一种可能性是使用一种称为interception, as it is provided by the Unity application block 的技术。

【讨论】:

  • 这看起来不错,但出于好奇,这种方法的性能会受到什么影响?
  • 好吧,我担心你必须自己找出答案。我在 wcf 服务客户端基类上使用过一次,以对来自派生客户端实现的所有服务调用实现通用异常处理,并且没有发现任何性能影响。但是我没有测量任何东西,只是感觉没有任何变慢。所以我不能对性能做出有弹性的陈述。
【解决方案3】:

正如马特所说,添加了一个 FxCop 规则来检查方法调用

【讨论】:

    【解决方案4】:

    如何使用代理模式进行额外的间接操作,以便您可以在那里注入冻结检查?如果代理引用的对象被冻结抛出,如果不继续。但是,这意味着您需要为每个 IFreezableModel 设置一个代理(尽管泛型可能会克服这个问题),并且它将适用于您正在访问的每个类成员(或者代理需要更复杂)。

    【讨论】:

    • 谢谢,我认为这就是 PVitt 的建议,但在这种情况下,必须编写大量代码才能从框架中输出可用的东西。
    • 这就是模板想法派上用场的地方——可以想象你可以使用模板来自动生成这些代理类。困难在于如何让系统的其余部分使用它们:)
    • 是的,这需要一些工作,我想说估计这个以及采用框架并挑战“无第三方框架”要求所需的工作
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-03-06
    • 1970-01-01
    • 1970-01-01
    • 2020-09-25
    • 1970-01-01
    • 2014-03-03
    相关资源
    最近更新 更多