我在库中有一个抽象类。我正在尝试尽可能简单地正确实现此类的派生。问题是我需要在三个步骤中初始化对象:获取文件,执行一些中间步骤,然后使用该文件。第一步和最后一步特别适用于派生类。这是一个精简的例子。
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()
和Step3()
而不是调用Initialize()
。问题的关键是如果跳过Step2()
则不会出现明显的错误;在某些情况下表现糟糕。
我觉得无论哪种方式都存在严重且不明显的“问题”,未来的图书馆用户将不得不解决这些问题。我应该使用其他一些设计来实现这种初始化吗?
如有必要,我可以提供更多细节;我只是想提供表达问题的最简单的例子。
答案 0 :(得分:10)
我会考虑创建一个abstract factory,负责使用template method初始化来实例化和初始化派生类的实例。
举个例子:
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容器来处理这个责任......
如果您无法控制派生类,则可能无法阻止它们提供可以调用的公共构造函数 - 但至少您可以建立消费者可以遵守的使用模式。
并不总是可以阻止您的课程用户自己动手 - 但是,您可以提供基础设施来帮助消费者在熟悉设计时正确使用您的代码。
答案 1 :(得分:3)
这太过于放置在任何类的构造函数中,更不用说基类了。我建议您将其分解为单独的Initialize
方法。
答案 2 :(得分:1)
编辑:我出于某种原因为C ++回答了这个问题。抱歉。对于C#我推荐使用Create()
方法 - 使用构造函数并确保对象从一开始就保持有效状态。 C#允许来自构造函数的虚拟调用,如果您仔细记录其预期的功能以及前后条件,则可以使用它们。我第一次推断C ++是因为它不允许来自构造函数的虚拟调用。
制作单独的初始化函数private
。可以是private
和virtual
。然后提供一个公共的,非虚拟的Initialize()
函数,以正确的顺序调用它们。
如果要确保在创建对象时发生所有事情,请在返回新创建的对象之前创建构造函数protected
并在调用Create()
的类中使用静态Initialize()
函数对象
答案 3 :(得分:1)
在很多情况下,初始化内容涉及分配一些属性。可以自己创建这些属性abstract
并使派生类覆盖它们并返回一些值,而不是将值传递给要设置的基础构造函数。当然,这个想法是否适用取决于您特定班级的性质。无论如何,在构造函数中有那么多代码是臭的。
答案 4 :(得分:1)
乍一看,我建议将这种逻辑移到依赖于这种初始化的方法上。像
这样的东西public class Base
{
private void Initialize()
{
// do whatever necessary to initialize
}
public void UseMe()
{
if (!_initialized) Initialize();
// do work
}
}
答案 5 :(得分:1)
由于步骤1“抓取文件”,最好使用Initialize(IBaseFile)并跳过步骤1.这样消费者可以随意获取文件 - 因为它仍然是抽象的。你仍然可以提供一个'StepOneGetFile()'作为返回文件的抽象,因此他们可以选择实现它。
DerivedClass foo = DerivedClass();
foo.Initialize(StepOneGetFile('filepath'));
foo.DoWork();
答案 6 :(得分:1)
您可以使用以下技巧来确保以正确的顺序执行初始化。据推测,您在基类中实现了一些依赖于初始化的其他方法( DoActualWork )。
abstract class Base { private bool _initialized; protected abstract void InitilaizationStep1(); private void InitilaizationStep2() { return; } protected abstract void InitilaizationStep3(); protected Initialize() { // it is safe to call virtual methods here InitilaizationStep1(); InitilaizationStep2(); InitilaizationStep3(); // mark the object as initialized correctly _initialized = true; } public void DoActualWork() { if (!_initialized) Initialize(); Console.WriteLine("We are certainly initialized now"); } }
答案 7 :(得分:0)
我不会这样做。我通常发现在构造函数中做任何“真正的”工作最终会成为一个坏主意。
至少要有一个单独的方法来加载文件中的数据。您可以创建一个参数,使其更进一步,并有一个单独的对象负责从文件构建您的一个对象,分离“从磁盘加载”和对象的内存中操作的关注。