为啥从代码中调用事件处理程序是不好的做法?

Posted

技术标签:

【中文标题】为啥从代码中调用事件处理程序是不好的做法?【英文标题】:Why is it bad practice to call an eventhandler from code?为什么从代码中调用事件处理程序是不好的做法? 【发布时间】:2010-10-31 16:08:28 【问题描述】:

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

【问题讨论】:

赞成,因为我相信每个对 Delphi 编程感兴趣的人都应该意识到这是一种不好的做法。在我开始使用 Actions 之前(正如 Rob Kennedy 在他的第 3 点中所提到的),我有 cooked up 相当多的 spaghetti 应用程序,这些应用程序维护起来简直就是一场噩梦,这很遗憾,因为这些应用程序非常好。但我越来越讨厌我自己的创作。国际海事组织,Rob 的回答非常好且详尽。 【参考方案1】:

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

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 设计取消了按钮,菜单会发生什么变化?更好的方法是将事件处理程序的操作分解出来,使其独立于它所附加的控件。有几种方法可以做到这一点:

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

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

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

    这样,真正起作用的代码不会直接绑定到任何一个组件,并且让您可以更轻松地更改这些控件,例如通过重命名或替换它们具有不同的控件。将代码与组件分离导致我们采用第三种方式:

    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 属性的事情。

【讨论】:

很好的答案,谢谢。 TAction 方法给我留下了特别深刻的印象,这是我以前没有意识到的,但听起来是解决这个问题的最佳方法。实际上,Delphi 似乎很好地涵盖了这个领域,允许所有方法。顺便说一句,您提到 TAction 允许自动禁用关联控件。最近我喜欢的风格态度的一个变化是,当操作不可用时,不禁用控件,而是允许用户单击控件,然后给他们一条消息,解释为什么该操作没有发生。 我认为如果使用这种风格,TAction 方法相对于其他方法的一些优势将变得无关紧要。 @jjb:即使控件的操作不可用也不禁用控件 ATM 会导致用户界面非常混乱恕我直言。但是由于禁用的控件确实使 UI 不易被发现,因此应该有一些原因的指示,例如当鼠标悬停在禁用的控件上时的工具提示或状态栏帮助消息。我更喜欢这种方法,而不是没有指示其所处状态的 UI。 @mghie 是的,这听起来是两全其美。 。你用 TAction 做什么并不是重点。关键是它可以让您确保一切都以相同的方式工作。【参考方案2】:

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

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

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

【讨论】:

IMO 这不是问题的答案。我问为什么你不能做 A 而不是 B,这个答案只是说因为 B 更好! 顺便说一句,我并不是说粗鲁地说这只是我的观察,我认为杰拉德的回答一针见血 B是更优雅的解决方案并且更易于维护的答案来自我自己的个人经验。自己的亲身经历其实并不是想用硬数据就能证明的,这就是亲身经历和科学证明的区别。而谈到优雅时..你无法定义它,你只能感受它......最终参考Steve McConnell的“Code Complete”,他对这些问题有很好的覆盖。 公平点,但我想说,如果要承载重量,使用个人经验作为论据需要示例。 好的,我会搜索我的代码档案并放一些代码作为示例。【参考方案3】:

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

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

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

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

【讨论】:

感谢您提出这个问题。我认为它清楚地说明了你的观点。我喜欢布尔参数的想法,以便在事件实际发生时允许不同的行为,而不是通过代码完成。 如果您将 nil 作为发件人传递,您可能会有不同的行为 ;) @jjb:我认为这是一个更广泛的主题,即在两个不同的过程中具有相似的逻辑。当您遇到这种情况时,最好为第三个过程提供实际逻辑并将这两个相似的过程转换为包含 proc 的新逻辑的包装器。行为的差异可以通过控制参数来完成。许多组件具有两个或多个重载方法,例如 Open。这些开放方法通常是某种私有 InternalOpen 过程的包装器,带有用于一些小调整的布尔参数。 @inzKulozik:是的,使用 UI 逻辑的转向逻辑,实际上使用 niled Sender 作为布尔控制变量...我认为它甚至比声明 var a,b,c,d,e 更好,f,g : 整数以防万一;)【参考方案4】:

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

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

【讨论】:

好的,这对我来说是合乎逻辑的,当您删除按钮时,您没有什么可以告诉您其他控件正在引用它。还有其他原因吗? Delphi 也可以这样做。为菜单项和按钮分配一个操作 - 我一直为反映菜单功能的工具栏按钮执行此操作。 另一个原因是,您可能希望在选择菜单项时进行某种用户界面更新,而在选择按钮时不适用。在大多数情况下,按照您所说的去做本质上并没有什么坏处,但这只是一个有问题的设计决策,限制了灵活性。【参考方案5】:

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

【讨论】:

我同意封装和分离,但是 vb6 控件上的 click/dbclick 事件永远不会是私有的。如果不将它们设为私有,那是因为有人认为伤害很小。 在 Delphi/Lazarus 中都没有发布它们(RTTI'd) @jpinto3912 - 实际上 VB6 事件处理程序默认是私有的。 这不是一个事件,它是一个事件接收器。甚至不是真正的接收器本身,而是编译器生成的接收器调用的逻辑。根据这个线程中看到的大多数逻辑,VB6 事件处理程序除了调用另一个(冗余)过程之外,根本不会有任何代码!坦率地说,我不买它,无论如何,这种情况应该很少见。如果有人偏执,则可以将实现逻辑的处理程序与调用它的处理程序分组,并精心布置 cmets 以指导未来的维护人员。 @jpinto3912:事件是公开的,但处理程序是私有的。事件实际上是(隐藏但公共的)事件接收器接口上的方法。 (私有)事件处理程序方法是(公共)事件接收器接口上的方法的实现。与使用Implements 关键字实现接口的方式类似,默认情况下会为实现创建Private 方法,除了事件和事件处理程序被特殊处理(即您不必为类公开的所有事件实现处理程序) ,编译器在编译时插入空事件处理程序)。【参考方案6】:

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

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

【讨论】:

【参考方案7】:

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

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

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

【讨论】:

听起来是个务实的政策【参考方案8】:

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

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

【讨论】:

这是一个不允许除了事件调用之外的任何东西的论点。如果您首先必须在事件中查找代码所在的方法的名称,则总是需要额外的查找代码。此外,为无穷无尽的方法想出有意义的名称也变得非常乏味。 不,这是不尝试重用事件中的代码的论据。如果代码仅适用于事件,那么我会将其放入事件中。但如果我需要从其他任何地方调用它,我会将其重构为自己的方法。 是的,这种方法似乎很有意义。谢谢【参考方案9】:

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

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

【讨论】:

我不同意你的逻辑。如果你有一个菜单项和一个按钮来做同样的事情,他们应该做同样的事情,而不是不同的功能。 IOW,如果您有一个允许您编辑数据库中的当前行的菜单项和一个允许您编辑数据库中的当前行的按钮,那么两者都应该做同样的事情;如果不是,它们不应该都被称为“编辑”。 @Ken 菜单和按钮可能有充分的理由做不同的事情。例如,在 VB6 中,当用户单击菜单项时,它不会在具有焦点的控件上触发失去焦点事件。当用户单击按钮时,它会触发失去焦点事件。如果您依赖失去焦点事件(例如进行验证),您可能需要在菜单单击事件中使用特殊代码来触发失去焦点并在发现验证错误时中止。您不需要通过单击按钮获得此特殊代码。

以上是关于为啥从代码中调用事件处理程序是不好的做法?的主要内容,如果未能解决你的问题,请参考以下文章

如果我每次都从不同的线程调用事件,为啥会从同一个线程触发事件处理程序的多次执行?

为啥从独立函数生成的 pyplot 窗口与从事件处理程序调用的函数生成时的行为不同?

为啥 bind() 在 Vue 模板事件处理程序中的工作方式如此不一致?

为啥在 dispatchEvent 上不调用 React 事件处理程序?

为啥我不能使用 jQuery 从卸载事件处理程序触发 AJAX 请求?

从事件处理程序回调调用的函数中“this”的值?