在C# WinForms中消除继承“魔法”的最佳方法是什么?

3
我正在处理一个遗留应用程序,由于继承关系存在一些缺陷,但我很难妥善解决它。
目前,WinForms的结构如下:
- BaseForm - ListViewForm : BaseForm - ListViewFormReadOnly : ListViewForm - ListViewFormWithDetailForm : ListViewForm - DetailForm : BaseForm - ConcreteForm : ListViewFormWithDetailForm
在BaseForm中有一个名为“InitializeMyStuff()”的方法,是被继承实例覆盖的虚拟方法。
例如:
public class BaseForm {
    public BaseForm() {
       //.. do stuff

       //.. do other stuff like initialize DB connection or read app.config values and initialize properties..
}

public virtual void InitializeMyStuff() {
        throw new NotImplementedException();
    }
}

public class ListViewForm : BaseForm {
    protected BindingSource GridBindingSource { get; set; }

    public ListViewForm {
       //do special stuff like adding the grid and some buttons
    }
}
public class ConcreteForm : ListViewForm {
public override void InitializeMyStuff() {
        GridBindingSource = my_bindingSource;
        SomeOtherUsefulProperty = myValue;
        Foo = new Bar();
        // etc.
    }
}

//Usage:
var myForm = new ConcreteForm();
myForm.InitializeMyStuff();

正如您所想象的那样,这会带来一些问题,例如: - “在这一点上我必须设置哪些东西才能使表单工作” - “哪些东西可能还没有初始化?” - “哪些属性和方法调用可以使用?” 以及关于那个神奇的黑匣子中可能发生的事情的一些其他有趣想法。
我该如何重构代码,以便更清楚地了解正在发生的事情?请记住,这是一个具有约150个或更多具体表单的项目。
我的初始想法是将这些神奇的属性(例如GridBindingSource)封装到一个对象(例如FormConfiguration)中,并将其设为BaseForm中的私有属性。
例如,像这样:
public class BaseForm {
    private FormConfigObject _formConfig = new FormConfigObject();

    protected override void OnLoad()
    {
        InitializeMyStuff(_formConfig);
    }

    protected virtual void InitializeMyStuff(FormConfigObject config)
    {}
}

我在这里遇到的问题是:ListForm的FormConfig对象必须具有其他属性,例如GridBindingSource,但我不能仅仅将派生类中的签名更改为ListFormConfigObject而不是FormConfigObject。有人可以建议可能的解决方案来摆脱这个困境吗?
// 编辑:将代码整理到实际发生的情况,并消除构造函数中的虚拟调用违规。

2
这也违反了CA2214规则:http://msdn.microsoft.com/en-us/library/ms182331.aspx - 构造函数中不应调用虚方法。 - BartoszKP
你有什么好的理由去改变它吗?起初我的想法是完全删除InitializeMyStuff,在构造函数中进行虚拟调用是一个不好的想法,但这需要很多工作。 - Alessandro D'Andria
1
这个问题可能更适合于http://codereview.stackexchange.com/。 - Matthew Watson
BaseForm实际上是Form(或Control)类型吗?如果是这样,您可以将对InitialiseMyStuff()的调用移动到OnCreateControl()的实现中,这避免了在构造函数中调用虚拟方法的问题,并且这也意味着您可以确保所有包含的控件在那时已经被创建。 - Matthew Watson
我编辑了我的问题以反映实际发生的情况。很抱歉在创建过程中犯了错误,实际上这并没有违反CA2214,但在现实中比这更糟,因为你基本上必须从外部初始化对象。 - bberger
显示剩余3条评论
1个回答

3
主要问题是:在BaseForm内部是否有任何对象:
  • 必须在BaseForm的构造函数中进行初始化
  • 依赖于子类的具体实现
如果存在这样的对象,则可能应该使它们成为多态的,并从子类传递到BaseForm的构造函数中。
一个简单的示例,其中有许多可能的情况之一:
abstract class RandomPicture
{
    public RandomPicture()
    {
        shapes = new List<Shape>();
        InitializeRandomShapes();

        // do some initial drawing calculations
    }

    protected abstract void InitializeRandomShapes();

    protected List<Shape> shapes;
}

//... subclasses initialize the shapes

这可以改为:
abstract class RandomPicture
{
    public RandomPicture(AbstractShapeCollection shapeCollection)
    {
        shapes = shapeCollection;

        // do some initial drawing calculations
    }

    private AbstractShapeCollection shapes;
}

现在,子类通过抽象对象提供所需的信息,因此基类可以继续完成其任务。

将信息拆分为不同的对象是很好的重构起点,因为您创建了更多更小、更易于测试和管理的对象,并揭示了您遇到的混乱的底层结构。这也有助于减少违反单一职责原则的数量。


我编辑了我的问题,以解决写示例时出现的一些错误。实际上,在构造函数中并没有虚拟调用,因为初始化是从外部调用的... :/ - bberger
@bberger 我已经从答案中删除了规则信息。但我认为它基本上保持不变 - 重构成较小的对象。OnLoad类似于构造函数,因此原则上我建议采用相同的方案。 - BartoszKP
是的,如果我可以从头开始,这就是我会做的 - 但这将是太多的重构,公司中没有人会批准 :/ 我需要另一个解决方案 :/ - bberger
1
@bberger 完全重构是几乎从来不被推荐的。在添加新功能时,一次只提取一个对象。当您需要出于业务原因修改代码时,请在本地修复它。没有其他目的而进行的“重写一切以使其看起来更好”的行为是毫无意义的。 - BartoszKP
完全同意。但是按照您建议的重构代码几乎等同于整个项目的完全重构,因为有太多依赖关系导致出现了“魔法”。我的第一目标是将我发布的那部分重构为更易于维护的解决方案,而不完全破坏“架构”,这样至少应用程序的某些部分会变得更清晰,并分成更小的部分,以便在必要时进行处理。 - bberger
@bberger好的,当我现在看这个配置对象时,你的配置对象是像这样走的。下一步是将它添加为构造函数参数,但不要删除旧的参数。因此,您的代码的某些部分可以保持不变,而其他部分和新部分可以采用新的方式。始终保留两条可能的执行线路-旧的和新的总是为我解决了破坏当前架构的问题。 - BartoszKP

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接