如何将此虚拟方法调用移出构造函数?

发布于 2024-11-06 18:44:17 字数 2041 浏览 2 评论 0原文

我有几个类形成了一个过于复杂的对象图。下面是一个较小的场景。假设 INotifyPropertyChanged 已到位。

class A
{
    public InternalType InterestingProperty { get; set; }
}

class B
{
    public A Component { get; set; }
}

我的帮助器类监视这些事件,并在对象属性更改时更新其属性。这样做是为了让对尽可能多的对象的大约十几个属性感兴趣的其他类可以轻松访问。这一切都包含在一个具有多个变体的框架中,因此继承发挥了作用。

我已经完成了第一个场景,并最终得到了一个像这样的具体类:

class ScenarioOnePropertySpy
{
    protected ScenarioOne PropertySpy(Foo thingToMonitor)
    {
        _thingToMonitor = thingToMonitor;
        RegisterForEvents()
    }

    public B InterestingB { get; }

    protected RegisterForEvents()
    {
        // * Register for _thingToMonitor propertyChanged if first time.
        // * If B is different, unregister the old and register the new.
        // * If B hasn't been set yet register for PropertyChanged on it.
        // * If B.Component isn't the same as last time unregister the
        //      old and register the new.
    }

    protected Update()
    {
        // Some monitored object changed; refresh property values and
        // update events in case some monitored object was replaced.
        B = _thingToMonitor.B;
        RegisterForEvents()
    }

    private Handle_PropertyChanged(...) { Update(); } 
}

这是令人讨厌的事件注册,但将这种丑陋从想要了解属性的类中移出才是目的。现在我已经转到场景 2,它监视不同的对象/属性,并使用我的具体类作为抽象类的指南:

abstract class PropertySpy
{
    protected PropertySpy(FooBase thingToMonitor)
    {
        _thingToMonitor = thingToMonitor;
        RegisterForEvents()
    }

    protected abstract void RegisterForEvents()

    // ...
}

哎呀。我在构造函数中有一个虚拟方法调用。理论上,它对于我的所有场景都是安全的,但 R# 警告一直困扰着我。我确信,如果有一天我继续前进,将会导致一个需要一段时间才能解决的问题。该方法肯定需要使用派生类型的属性。

我可以删除该方法并强制派生类型自行进行事件管理。这会违背基类的目的。有人会忘记遵守合同,这就会变成支持事件;我花了足够的时间编写文档。我想到的另一个方法是公开 RegisterForEvents() 并要求用户在构造后调用它。 “创建然后初始化”模式在 .NET 中很糟糕,人们总是忘记。目前,我正在考虑另一个类的概念,该类执行通过构造函数注入的事件注册。然后派生类可以提供该类来实现与虚方法相同的效果,而不会产生危险。但进行注册的操作实际上需要与 PropertySpy 相同的属性接口;这看起来很乏味,但我想“丑陋但有效”比我所拥有的更好。

我缺少什么吗?如果论点令人信服的话,我什至会接受“这是一个警告,而不是规则”作为答案。

I've got a couple of classes that form a too-complicated object graph. Here's a peek at a smaller scenario. Assume INotifyPropertyChanged is in place.

class A
{
    public InternalType InterestingProperty { get; set; }
}

class B
{
    public A Component { get; set; }
}

My helper class watches for these events and updates its properties when the properties of the objects change. It does this so some other class that's interested in about a dozen properties on as many objects are easily accessible. This is all packed in a framework that has several variants, so inheritance is in play.

I've finished the first scenario, and ended up with a concrete class like this:

class ScenarioOnePropertySpy
{
    protected ScenarioOne PropertySpy(Foo thingToMonitor)
    {
        _thingToMonitor = thingToMonitor;
        RegisterForEvents()
    }

    public B InterestingB { get; }

    protected RegisterForEvents()
    {
        // * Register for _thingToMonitor propertyChanged if first time.
        // * If B is different, unregister the old and register the new.
        // * If B hasn't been set yet register for PropertyChanged on it.
        // * If B.Component isn't the same as last time unregister the
        //      old and register the new.
    }

    protected Update()
    {
        // Some monitored object changed; refresh property values and
        // update events in case some monitored object was replaced.
        B = _thingToMonitor.B;
        RegisterForEvents()
    }

    private Handle_PropertyChanged(...) { Update(); } 
}

It's icky event registration, but moving that ugliness out of the class that wants to know about the properties is the purpose. Now I've moved on to scenario 2 that monitors different objects/properties and used my concrete class as a guide for an abstract one:

abstract class PropertySpy
{
    protected PropertySpy(FooBase thingToMonitor)
    {
        _thingToMonitor = thingToMonitor;
        RegisterForEvents()
    }

    protected abstract void RegisterForEvents()

    // ...
}

Whoops. I've got a virtual method call in the constructor. In theory it's safe for all of my scenarios, but the R# warning keeps digging at me. I'm sure if I move forward one day it's going to cause a problem that'll take a while to figure out. That method's definitely going to need to work with properties on the derived types.

I could drop the method and force derived types to do the event management themselves. That'd defeat the purpose for the base class. And someone would forget to follow the contract and it'd turn into a support incident; I spend enough time writing documentation as it is. Another one I thought of was making RegisterForEvents() publich and requiring users to call it after construction. That "create then initialize" pattern stinks in .NET and people always forget. Currently I'm toying with the notion of another class that does the event registration that's injected via the constructor. Then derived classes can provide that class to achieve the same effect as a virtual method without the dangers. But the thing doing the registration would need practically the same property interface as PropertySpy; it seems tedious but I guess "ugly and works" is better than what I've got.

Anything I'm missing? I'll even take "it's a warning, not a rule" as an answer if the argument is convincing.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

一袭白衣梦中忆 2024-11-13 18:44:17

您的场景似乎足够复杂,需要考虑采用完全不同的类实例化方法。用工厂来建造地产间谍怎么样?

public class PropertySpyFactory<T> where T : PropertySpy, new()
{
    public static T Create()
    {
        T result = new T();
        // … whatever initialization needs to be done goes here …
        result.RegisterForEvents();
        return result;
    }
}

ScenarioOnePropertySpy spy = PropertySpyFactory<ScenarioOnePropertySpy>.Create();

它在代码中是可挽救的,实例初始化可以轻松扩展,一旦你转向 IoC,它会感觉很自然,不需要太多重构。

更新:另一种选择是,a)你的间谍层次结构足够平坦,b)你不需要使用共同的祖先,或者你可以用接口替代它:

public abstract class PropertySpy<T> where T : PropertySpy, new()
{
    public static T Create()
    {
        T result = new T();
        // … whatever initialization needs to be done goes here …
        result.RegisterForEvents();
        return result;
    }

    …
}

public class ScenarioOnePropertySpy : PropertySpy<ScenarioOnePropertySpy>
{
    …
}

ScenarioOnePropertySpy spy = ScenarioOnePropertySpy.Create();

换句话说,工厂方法位于共同祖先内。这种方法的缺点是它不是正交的(工厂没有与正在构造的类分离),因此可扩展性和灵活性较差。然而,在某些情况下可能是一个有效的选择。

最后但并非最不重要的一点是,您可以在每个类中再次创建一个工厂方法。优点是您可以保护构造函数,从而强制用户使用工厂方法而不是直接实例化。

Your scenario seems complicated enough to consider a completely different approach to class instantiation. What about using a factory to construct property spies?

public class PropertySpyFactory<T> where T : PropertySpy, new()
{
    public static T Create()
    {
        T result = new T();
        // … whatever initialization needs to be done goes here …
        result.RegisterForEvents();
        return result;
    }
}

ScenarioOnePropertySpy spy = PropertySpyFactory<ScenarioOnePropertySpy>.Create();

It's salvagable in the code, instance initialization can be extended easily, and once you turn to an IoC it will feel quite natural and not much refactoring will be needed.

UPDATE: One another option in case a) your spy hierarchy is flat enough and b) you don't need to use a common ancestor or you can substitute it with an interface:

public abstract class PropertySpy<T> where T : PropertySpy, new()
{
    public static T Create()
    {
        T result = new T();
        // … whatever initialization needs to be done goes here …
        result.RegisterForEvents();
        return result;
    }

    …
}

public class ScenarioOnePropertySpy : PropertySpy<ScenarioOnePropertySpy>
{
    …
}

ScenarioOnePropertySpy spy = ScenarioOnePropertySpy.Create();

In other words, the factory method is located right within the common ancestor. The drawback of this approach is that it isn't that orthogonal (the factory isn't separated from the classes being constructed) and hence less extensible and flexible. However, in certain cases may be a valid option.

Last but not least, you can create a factory method in each class again. The advantage is you can keep constructors protected and hence force users to use factory methods instead of direct instantiation.

薄凉少年不暖心 2024-11-13 18:44:17

我认为关键问题是,当调用虚拟方法时,您的子类构造函数和初始值设定项尚未执行。因此,在您重写的方法中,您的子类可能没有初始化您期望初始化的所有内容。

The key issue I believe is that by the time the virtual method is called, your subclass constructor and initializers have not executed yet. So, in your overridden method, your subclass may not have all the things you expect to be initialized initialized.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文