我目前正在编写一个小型C#库,以简化实施小型物理模拟/实验。
主要组件是SimulationForm
,它在内部运行定时器循环并隐藏用户的样板代码。实验本身只能通过三种方法来定义:
Init()
(初始化所有内容)Render(Graphics g)
(渲染当前模拟状态)Move(double dt)
(移动实验dt
秒)我只是想知道让用户实现这些功能的更好选择是什么:
1)由继承表格覆盖的虚拟方法
protected virtual void Init() {}
...
或
2)活动
public event EventHandler<MoveSimulationEventArgs> Move = ...
...
修改:请注意,方法不应该是 abstract 。实际上,还有更多, 没有实现。由于许多模拟不需要它们,因此将它们放在外面通常很方便。
关于这是一个“正常形式”的很酷的事情是你可以写
partial class frmMyExperiment : SimulationForm {
}
并且您完全能够与设计器以及所有继承的控件和设置/属性进行交互。我不想通过采用完全不同的方法来失去这些功能。
答案 0 :(得分:17)
在这种情况下,我更喜欢使用虚拟方法。
此外,如果方法必需才能运作,您可以将班级设为abstract class。这在可用性方面是最好的,因为它会导致编译器强制使用。如果用户试图在不实现方法的情况下使用您的类/表单,编译器会抱怨。
事件在这里有点不合适,因为这不是你想要的不仅仅是单个实现(事件允许多个订阅者)的东西,从概念上讲,它更像是对象的功能,而不是由对象引起的通知。
答案 1 :(得分:15)
有趣的是,我不得不在最近工作的设计中做出类似的选择。一种方法并不严格优于另一种方法。
在你的例子中,没有额外的信息,我可能会选择虚拟方法 - 特别是因为我的直觉让我相信你会使用继承来模拟不同类型的实验,而你不需要拥有多个订阅者的能力(事件允许)。
以下是关于在这些模式之间进行选择的一些一般性观察:
活动很棒:
事件的最大问题是管理订阅者的生命周期可能会变得棘手,并且当订阅者订阅时间超过必要时,您可能会引入泄漏甚至功能缺陷。第二个最大的问题是,允许多个订阅者可以创建一个混乱的实现,其中各个订阅者互相踩踏 - 或者表现出顺序依赖性。
虚拟方法运作良好:
虚拟方法的最大问题是您可以轻松地将脆弱的基类问题引入到您的实现中。虚方法本质上是与派生类的契约,您必须清楚地记录它们,以便继承者可以提供有意义的实现。
虚拟方法的第二大问题是它可以引入深度或广泛的继承树,以便定制在特定情况下如何自定义类的行为。这可能没问题,但如果问题域中没有明确的is-a
关系,我通常会尝试避免继承。
您可以考虑使用另一种解决方案:strategy pattern。让您的类支持对象(或委托)的分配,以定义渲染,初始化,移动等应该如何表现。您的基类可以提供默认实现,但允许外部使用者更改行为。虽然与事件类似,但优点是您可以将值返回给调用代码,并且您只能强制执行一个订阅者。
答案 2 :(得分:5)
决定一点帮助:
是否要通知其他(多个)对象?然后使用事件。
您想要实现不同的实现/使用多态吗?然后使用虚拟/抽象方法。
在某些情况下,即使是两种方式组合也是一个很好的解决方案。
答案 3 :(得分:3)
还有另一个选项,从函数式编程中借用:将扩展点公开为Func或Action类型的属性,并让实验为代表提供代理。
例如,在您的模拟跑步者中,您将拥有:
public class SimulationForm
{
public Action Init { get; set; }
public Action<Graphics> Render { get; set; }
public Action<double> Move { get; set; }
//...
}
在你的模拟中你会有:
public class Simulation1
{
private SimulationForm form;
public void Simulation1()
{
form = new SimulationForm();
form.Init = Init;
form.Render = Render;
form.Move = Move;
}
private void Init()
{
// Do Init code here
}
private void Render(Graphics g)
{
// Do Rendering code here
}
private void Move(double dt)
{
// Do Move code here
}
}
编辑:有关我在某些私有代码中使用的这种技术的非常愚蠢的例子(对于私人游戏项目),请查看my delegate-powered-collection blog post。
答案 4 :(得分:2)
两者都不是。
你应该使用一个实现定义所需函数的接口的类。
答案 5 :(得分:1)
为什么界面不适合这个?
答案 6 :(得分:0)
使用虚方法允许基类的特化 - 内部细节。
从概念上讲,除了返回方法之外,您还可以使用事件来表示类的“输出”。
答案 7 :(得分:0)
我喜欢LBuskin的总结。关于脆弱的基类问题,有一种情况我认为受保护事件可能是最佳实践的竞争者:
这避免了使用“派生类应调用基本方法”来记录虚拟受保护方法的需要,并希望您或其他派生类的人始终阅读文档并遵循它。它还避免了不确定性,想知道是否应该在重写方法的开头或中间或结尾调用对base方法的调用。
注意:如果您认为附加到其自身事件的基类是愚蠢的,那么就没有什么能阻止您在基类中使用非虚方法。
在OP的情况下,我认为Init和可能的Move事件可能符合上述标准,具体取决于具体情况。通常情况下,它不太可能有多个Render方法,所以一个抽象方法(或空虚拟方法,如果这个类允许你运行模拟而不绘制任何东西)不需要调用任何基本Render方法的方法似乎很可能在这里。如果外部组件可能知道如何将类绘制到图形目标,可能具有替代视觉样式(即2D与3D),则Render的策略(公共委托)将是有用的。对于Render方法似乎不太可能,但也许可能适用于其他未提及的函数,尤其是应用算法的方法(趋势线平均方法等)。