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

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

7457 次浏览

因为您应该将内部逻辑分离到其他函数中,并调用这个函数..。

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

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

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

在 Delphi 中,我通常在严肃的应用程序中避免使用它,但在小事情中我称之为 eventhandlers。如果小东西以某种方式演变成更大的东西,我清理它,并通常在同一时间增加逻辑用户界面分离。

但我知道在 Lazarus/Delphi 语言中这并不重要,因为其他语言可能有更特殊的附加行为。

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

假设在某个时刻,您认为菜单项不再有意义,并且您想要删除该菜单项。如果只有一个其他控件指向菜单项的事件处理程序,这可能不是一个大问题,您只需将代码复制到按钮的事件处理程序中即可。但是,如果有几种不同的方式可以调用代码,那么就必须进行大量更改。

就我个人而言,我喜欢 QT 处理这件事的方式。有一个带有自己的事件处理程序的 QAction 类可以被钩住,然后 QAction 与需要执行该任务的任何 UI 元素相关联。

为什么是坏习惯?因为当代码不嵌入 UI 控件时,它更容易重用。

为什么不能在 REALbasic 中做呢?我怀疑有任何技术上的原因,这可能只是他们的设计决定。它确实强制执行了更好的编码实践。

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

另一方面,如果你调用一个函数,你不太可能改变它的功能,因为你(或者下一个人)知道这会带来不好的结果。

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

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 所独有。)将事件处理代码放在 TActionOnExecute事件处理程序中,然后将该操作分配给按钮和菜单项的 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属性的情况。

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

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

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

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

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

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