73

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

4

9 回答 9

84

这是您的程序如何组织的问题。在您描述的场景中,菜单项的行为将根据按钮的定义:

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事件都是 type 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. TAction组件是在 Delphi 4 中引入的,专为您所描述的情况而设计,其中同一命令有多个 UI 路径。(其他语言和开发环境提供了类似的概念;它不是 Delphi 独有的。)将您的事件处理代码放在 的TAction事件OnExecute处理程序中,然后将该操作分配给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不仅仅是事件处理程序。它使您可以确保您的 UI 控件具有统一的属性设置,包括标题、提示、可见性、启用和图标。当命令当时无效时,相应地设置操作的Enabled属性,任何链接的控件将自动被禁用。例如,无需担心通过工具栏禁用命令,但仍通过菜单启用命令。您甚至可以使用操作的OnUpdate事件,以便操作可以根据当前条件进行自我更新,而不是您需要知道何时发生可能需要您立即设置Enabled属性的事情。

于 2009-06-05T20:33:59.780 回答
15

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

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

这是一个更优雅的解决方案,并且更容易维护。

于 2009-06-05T14:45:40.990 回答
10

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

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

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

因此,这就是告诉我不要将逻辑直接放在 GUI 事件处理程序中的原因,小型项目可能除外。

于 2009-06-05T21:32:35.793 回答
9

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

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

于 2009-06-05T14:51:16.753 回答
8

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

于 2009-06-05T14:47:28.663 回答
8

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

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

于 2009-06-06T10:35:02.107 回答
4

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

在 Delphi 中,我通常在严肃的应用程序中避免使用它,但我在小东西中调用事件处理程序。如果小东西不知何故变成了更大的东西,我会清理它,通常同时增加逻辑-UI 分离。

我确实知道这在 Lazarus/Delphi 中并不重要。其他语言可能有更多附加到事件处理程序的特殊行为。

于 2009-06-05T14:46:14.173 回答
2

为什么这是不好的做法?因为当代码没有嵌入到 UI 控件中时,它更容易重用代码。

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

于 2009-06-05T15:30:48.653 回答
1

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

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

于 2009-06-05T18:21:06.437 回答