9

我在图书馆中有一个抽象类。我试图尽可能简单地正确实现这个类的派生。问题是我需要在一个三步过程中初始化对象:获取一个文件,执行几个中间步骤,然后使用该文件。第一步和最后一步是派生类特有的。这是一个精简的示例。

abstract class Base
{
    // grabs a resource file specified by the implementing class
    protected abstract void InitilaizationStep1();

    // performs some simple-but-subtle boilerplate stuff
    private void InitilaizationStep2() { return; }

    // works with the resource file
    protected abstract void InitilaizationStep3();

    protected Base()
    {
        InitilaizationStep1();
        InitilaizationStep2();
        InitilaizationStep3();
    }
}

当然,麻烦的是构造函数中的虚方法调用。如果不能指望派生类被完全初始化,恐怕图书馆的使用者在使用该类时会发现自己受到限制。

我可以将构造函数中的逻辑拉到受保护的Initialize()方法中,但是实现者可能会直接调用Step1()andStep3()而不是调用Initialize(). 问题的症结在于如果Step2()被跳过不会有明显的错误;在某些情况下只是糟糕的表现。

我觉得无论哪种方式,图书馆的未来用户都必须解决一个严重且不明显的“陷阱”。我应该使用其他一些设计来实现这种初始化吗?

如有必要,我可以提供更多详细信息;我只是想提供一个表达问题的最简单的例子。

4

8 回答 8

11

我会考虑创建一个抽象工厂,负责使用模板方法初始化和初始化派生类的实例。

举个例子:

public abstract class Widget
{
    protected abstract void InitializeStep1();
    protected abstract void InitializeStep2();
    protected abstract void InitializeStep3();

    protected internal void Initialize()
    {
        InitializeStep1();
        InitializeStep2();
        InitializeStep3();
    }

    protected Widget() { }
}

public static class WidgetFactory
{
    public static CreateWidget<T>() where T : Widget, new()
    {
        T newWidget = new T();
        newWidget.Initialize();
        return newWidget;
    }
}

// consumer code...
var someWidget = WidgetFactory.CreateWidget<DerivedWidget>();

这个工厂代码可以显着改进——特别是如果你愿意使用 IoC 容器来处理这个责任......

如果您无法控制派生类,您可能无法阻止它们提供可调用的公共构造函数——但至少您可以建立消费者可以遵守的使用模式。

阻止你的类的用户在脚上开枪并不总是可能的——但是,你可以提供基础设施来帮助消费者在熟悉设计时正确使用你的代码。

于 2009-07-20T19:18:38.363 回答
4

放在任何类的构造函数中都太多了,更不用说基类了。我建议您将其分解为单独的Initialize方法。

于 2009-07-20T19:06:42.770 回答
1

编辑:出于某种原因,我为 C++ 回答了这个问题。对不起。对于 C#,我建议不要使用一种Create()方法 - 使用构造函数并确保对象从一开始就保持有效状态。C# 允许来自构造函数的虚拟调用,如果您仔细记录它们的预期功能和前置条件和后置条件,则可以使用它们。我第一次推断出 C++,因为它不允许来自构造函数的虚拟调用。

做个别的初始化函数private。可以是privatevirtual。然后提供一个公共的、非虚拟的Initialize()函数,以正确的顺序调用它们。

如果您想确保在创建对象时一切都发生,请创建构造函数并在您的类中protected使用静态函数,该函数在返回新创建的对象之前调用。Create()Initialize()

于 2009-07-20T19:09:36.117 回答
1

在很多情况下,初始化内容涉及分配一些属性。可以使这些属性本身abstract并让派生类覆盖它们并返回一些值,而不是将值传递给基构造函数进行设置。当然,这个想法是否适用取决于你的具体班级的性质。无论如何,在构造函数中有这么多代码是很臭的。

于 2009-07-20T19:09:50.830 回答
1

乍一看,我建议将这种逻辑移至依赖此初始化的方法中。就像是

public class Base
{
   private void Initialize()
   {
      // do whatever necessary to initialize
   }

   public void UseMe()
   {
      if (!_initialized) Initialize();
      // do work
   }
}
于 2009-07-20T19:12:14.210 回答
1

由于第 1 步“获取文件”,因此最好有 Initialize(IBaseFile) 并跳过第 1 步。这样消费者可以随心所欲地获取文件 - 因为它无论如何都是抽象的。您仍然可以提供“StepOneGetFile()”作为返回文件的抽象,因此如果他们愿意,他们可以以这种方式实现它。

DerivedClass foo = DerivedClass();
foo.Initialize(StepOneGetFile('filepath'));
foo.DoWork();
于 2009-07-20T19:22:51.083 回答
1

您可以使用以下技巧来确保以正确的顺序执行初始化。据推测,您在基类中实现了一些其他方法(DoActualWork),它们依赖于初始化。

抽象类基
{
    私人布尔_初始化;

    受保护的抽象 void InitilaizationStep1();
    私人无效InitilaizationStep2(){返回; }
    受保护的抽象 void InitilaizationStep3();

    受保护的初始化()
    {
        // 这里调用虚方法是安全的
        初始化步骤1();
        初始化步骤2();
        初始化步骤3();

        // 将对象标记为正确初始化
        _initialized = true;
    }

    公共无效 DoActualWork()
    {
        if (!_initialized) 初始化();
        Console.WriteLine("我们现在肯定已经初始化了");
    }
}
于 2009-07-20T19:35:40.137 回答
0

我不会这样做的。我通常发现在构造函数中做任何“真正的”工作最终都是一个坏主意。

至少,有一个单独的方法来从文件中加载数据。您可以提出一个论点,使其更进一步,并有一个单独的对象负责从文件构建您的一个对象,将“从磁盘加载”的关注点与对象的内存中操作分开。

于 2009-07-21T05:46:33.263 回答