为什么从代码中调用eventhandler是不好的做法?

时间:2009-06-05 14:43:20

标签: delphi vb6 coding-style realbasic

假设您有一个菜单项和一个执行相同任务的按钮。 为什么将任务的代码放入一个控件的动作事件然后从另一个控件调用该事件是不好的做法? Delphi允许这和vb6一样,但是realbasic没有,并且说你应该将代码放入一个方法,然后由菜单和按钮调用

9 个答案:

答案 0 :(得分:83)

这是一个关于你的计划如何组织的问题。在您描述的场景中,菜单项的行为将根据按钮的定义:

procedure TJbForm.MenuItem1Click(Sender: TObject);
begin
  // Three different ways to write this, with subtly different
  // ways to interpret it:

  Button1Click(Sender);
  // 1. "Call some other function. The name suggests it's the
  //    function that also handles button clicks."

  Button1.OnClick(Sender);
  // 2. "Call whatever method we call when the button gets clicked."
  //    (And hope the property isn't nil!)

  Button1.Click;
  // 3. "Pretend the button was clicked."
end;

这三种实现中的任何一种都可以使用,但是为什么菜单项应该如此依赖于按钮?它应该定义菜单项的按钮有什么特别之处?如果新的UI设计取消了按钮,菜单会发生什么?更好的方法是分解事件处理程序的操作,使其独立于它附加的控件。有几种方法可以做到这一点:

  1. 一种是完全摆脱MenuItem1Click方法,并将Button1Click方法分配给MenuItem1.OnClick事件属性。将按钮分配给菜单项的事件命名方法令人困惑,因此您需要重命名事件处理程序,但这没关系,因为与VB不同,Delphi的方法名称不能定义它们是什么事件处理。只要签名匹配,您就可以将任何方法分配给任何事件处理程序。两个组件的OnClick事件都是TNotifyEvent类型,因此它们可以共享一个实现。 为他们所做的事情命名方法,而不是他们属于什么。

  2. 另一种方法是将按钮的事件处理程序代码移动到单独的方法中,然后从两个组件的事件处理程序中调用该方法:

    procedure HandleClick;
    begin
      // Do something.
    end;
    
    procedure TJbForm.Button1Click(Sender: TObject);
    begin
      HandleClick;
    end;
    
    procedure TJbForm.MenuItem1Click(Sender: TObject);
    begin
      HandleClick;
    end;
    

    这样,真正做事的代码并不直接与任何一个组件绑定,而可让您更轻松地更改这些控件,例如通过重命名或替换它们有不同的控制。将代码与组件分开会将我们引向第三种方式:

  3. Delphi 4中引入的TAction组件专门针对您所描述的情况而设计,其中有多个UI路径指向同一命令。 (其他语言和开发环境提供类似的概念;它不是Delphi独有的。)将事件处理代码放在TAction的{​​{1}}事件处理程序中,然后将该操作分配给{{1按钮和菜单项的属性。

    OnExecute

    想要添加另一个像按钮一样的UI元素吗?没问题。添加它,设置其Action属性,然后就完成了。无需编写更多代码来使新控件看起来像旧的一样。你已经编写了一次代码。

    procedure TJbForm.Action1Click(Sender: TObject); begin // Do something // (Depending on how closely this event's behavior is tied to // manipulating the rest of the UI controls, it might make // sense to keep the HandleClick function I mentioned above.) end; 不仅仅是事件处理程序。 它可以确保您的UI控件具有统一的属性设置,包括字幕,提示,可见性,启用和图标。当命令当时无效时,相应地设置操作的Action属性,任何链接的控件将自动被禁用。例如,无需担心通过工具栏禁用命令,但仍然可以通过菜单启用该命令。您甚至可以使用操作的TAction事件,以便操作可以根据当前条件自行更新,而不是每当发生可能需要您立即设置Enabled属性的事件时都需要知道。 / p>

答案 1 :(得分:15)

因为您应该将内部逻辑与其他函数分开并调用此函数...

    来自两个事件处理程序的
  1. 如果您需要
  2. ,请与代码分开

    这是一种更优雅的解决方案,更易于维护。

答案 2 :(得分:10)

这是承诺的扩展答案。 2000年,我们开始使用Delphi编写应用程序。这是一个EXE,很少有DLL包含逻辑。这是电影业,所以有客户DLL,预订DLL,票房DLL和计费DLL。当用户想要开票时,他打开适当的表格,从列表中选择客户,然后OnSelectItem逻辑将客户影院加载到下一个组合框,然后在选择影院后,OnSelectItem事件填充第三个组合框,其中包含有关电影的信息,尚未收费了。该过程的最后一部分是按下“Do Invoice”按钮。一切都是作为一个事件程序完成的。

然后有人决定我们应该有广泛的键盘支持。我们添加了来自另一个偶数处理程序的调用事件处理程序。事件处理程序的工作流程开始变得复杂。

两年后,有人决定实施另一项功能 - 以便在另一个模块(客户模块)中处理客户数据的用户应该看到一个标题为“向此客户发票”的按钮。此按钮应触发发票表单并将其显示在这样的状态,就像是手动选择所有数据的用户(用户可以查看,进行一些调整,然后按魔术“Do Invoice”按钮)。由于客户数据是一个DLL而计费是另一个,因此传递消息的是EXE。因此,显而易见的想法是,客户数据开发人员将使用单个ID作为参数的单个例程,并且所有这些逻辑都将在计费模块内。 想象一下发生了什么。由于所有逻辑都在事件处理程序中,我们花费了大量时间,尝试实际上没有实现逻辑,而是尝试模仿用户活动 - 比如选择项目,使用GLOBAL变量挂起事件处理程序中的Application.MessageBox等等。想象一下 - 如果我们甚至在事件处理程序中调用了简单的逻辑过程,我们就能够将DoShowMessageBoxInsideProc布尔变量引入过程签名。如果从事件处理程序调用,则可以使用true参数调用此过程,并在从外部位置调用时使用FALSE参数调用此过程。

所以这就是教我不要将逻辑直接放在GUI事件处理程序中,可能的例外是小项目。

答案 3 :(得分:8)

关注点分离。类的私有事件应该封装在该类中,而不是从外部类调用。如果您在对象之间具有强大的接口并最大限度地减少多个入口点的出现,这将使您的项目更容易改变。

答案 4 :(得分:8)

假设在某些时候您决定菜单项不再有意义,并且您想要摆脱菜单项。如果您只有另一个控件指向菜单项的事件处理程序,这可能不是一个大问题,您只需将代码复制到按钮的事件处理程序中即可。但是,如果您可以通过几种不同的方式调用代码,则必须进行大量更改。

我个人喜欢Qt处理这个问题的方式。有一个QAction类,它有自己的事件处理程序,可以挂钩,然后QAction与任何需要执行该任务的UI元素相关联。

答案 5 :(得分:8)

另一个重要原因是可测试性。当事件处理代码隐藏在UI中时,测试它的唯一方法是通过手动测试或与UI密切相关的自动化测试。 (例如,打开菜单A,单击按钮B)。 UI的任何变化自然会破坏数十个测试。

如果代码被重构为一个专门处理它需要执行的作业的模块,那么测试变得更加容易。

答案 6 :(得分:4)

显然更整洁。但是易用性和生产力当然也很重要。

在Delphi中,我通常会在严肃的应用程序中避免使用它,但我会将事件处理程序称为小东西。如果小东西以某种方式变成更大的东西,我会清理它,并且通常同时增加逻辑UI分离。

我知道在Lazarus / Delphi中无关紧要。其他语言可能会在事件处理程序中附加更多特殊行为。

答案 7 :(得分:2)

为什么这是不好的做法?因为当代码未嵌入到UI控件中时,重用代码要容易得多。

为什么不能在REALbasic中执行此操作?我怀疑有任何技术原因;它可能只是他们做出的设计决定。它肯定会强制执行更好的编码实践。

答案 8 :(得分:1)

假设在某个时候您决定菜单应该做一些略微不同的事情。也许这种新变化只发生在某些特定情况下。你忘记了按钮,但现在你也改变了它的行为。

另一方面,如果你调用一个函数,你不太可能改变它的作用,因为你(或下一个人)知道这会产生不良后果。

相关问题