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


73

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


6
我对此表示支持,因为我相信对Delphi编程感兴趣的每个人都应该意识到这是一种不良做法。在开始使用Actions之前(Rob Kennedy在他的观点3中提到过),我有cooked up很多spaghetti应用程序,这些都是维护工作的噩梦,但是由于这些应用程序相当不错,所以很遗憾。但是我开始讨厌自己的创造。IMO,Rob的回答确实非常详尽。
PeterPerháč09年

Answers:


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事件都是类型的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. TActionDelphi 4中引入的组件是专门为您所描述的情况而设计的,在该情况下,同一命令有多个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属性。


1
好答案,谢谢。我以前从未听说过的TAction方法给我留下特别深刻的印象,但这听起来是实现此目标的最佳方法。实际上,Delphi似乎已经很好地覆盖了这一领域,允许所有方法。顺便说一句,您提到TAction允许自动禁用关联的控件。我最近喜欢的一种样式更改态度是,趋势是在某项操作不可用时不禁用控件,而是允许用户单击该控件,然后向他们提供一条消息,解释为什么未进行该操作。
jjb

我认为,如果使用这种方式,则TAction方法相对于其他方法的某些优势将变得无关紧要。
jjb

3
@jjb:即使不能使用控件,也不能禁用控件ATM会使用户界面非常混乱。但是,由于禁用的控件的确确实使UI变得不那么容易被发现,因此应该在某种程度上指出原因,例如,当鼠标悬停在禁用的控件上时,会显示工具提示或状态栏帮助消息。我更喜欢这种方法太多了,让没有它的状态的指示的UI。
mghie

@mghie是的,听起来像两全其美。
jjb

1
<叹> 用TAction做什么不是重点。关键是,它可以确保一切工作方式相同。
罗布·肯尼迪

15

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

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

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


IMO这不是问题的答案。我问为什么你不能做A而不是B,而这个答案只是说,因为B更好!
jjb

顺便说一句,我并不是说粗鲁的看法只是我的观察,我认为杰拉尔德的回答打在了头上
jjb

2
B是更优雅的解决方案并且更易于维护的答案来自于我个人的经验。自己的个人经历实际上并不是您可以使用硬数据证明的想法,这是体验某些东西与科学证明它之间的区别。当谈论优雅..您无法定义它时,您只能感受到它。...最终参考Steve McConnell的“代码完成”,他对此类问题有很好的报道。
smok1 2009年

公平的说法,但我想说,以个人经验为论据,需要举一些例子,以减轻负担。
jjb

好的,我将搜索代码档案并以一些代码为例。
smok1 2009年

10

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

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

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

因此,这使我学会了不要将逻辑直接放在GUI事件处理程序中,除了小型项目以外。


1
感谢您提出来。我认为它清楚地说明了您的观点。我喜欢boolean参数的想法,以便在事件实际发生时允许不同的行为,而不是通过代码来完成。
jjb

1
如果您通过nil作为发件人,则可能会有不同的行为;)
inzKulozik 2009年

@jjb:我认为这是更广泛的主题,即在两个不同的过程中具有相似的逻辑。遇到这种情况时,最好提供带有实际逻辑的第三个过程,并将这两个相似的过程转换为包含proc的新逻辑的包装。行为上的差异可以通过控制参数来完成。许多具有两个或多个重载方法的组件(例如Open)。这些打开方法通常是某种专用InternalOpen过程的包装器,带有布尔参数的一些一些小调整。
smok1 2009年

@inzKulozik:是的,使用UI逻辑操纵逻辑,实际上使用niled Sender作为布尔控制变量...我认为它甚至比声明var a,b,c,d,e,f,g甚至更好:案例;)
smok1,2009年

8

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


1
我同意封装和分离,但是vb6控件上的click / dbclick事件从来都不是私有的。而且,如果不将它们设为私有,那是因为有人认为危害最小。
jpinto3912 2009年

它们都没有在Delphi / Lazarus中公开(RTTI发布)
Marco van de Voort,2009年

@ jpinto3912-实际上,VB6事件处理程序默认情况下是私有的。
MarkJ 2009年

这不是事件,而是事件接收器。甚至不是接收器本身,而是编译器调用的逻辑会生成接收器。根据该线程中的大多数逻辑,VB6事件处理程序除了调用另一个(冗余)过程外,根本不会包含任何代码!坦白说,我不买它,反正这种情况应该很少见。如果一个人偏执,则可以将实现该逻辑的处理程序与调用该逻辑的处理程序分组,并放置详细的注释以指导将来的维护者。
鲍勃

@ jpinto3912:事件是公开的,但处理程序是私有的。事件实际上是(隐藏的但公共的)事件接收器接口上的方法。(私有)事件处理程序方法是(公共)事件接收器接口上方法的实现。与使用Implements关键字实现接口的Private方法类似,默认情况下类似于为实现创建方法的方法,不同之处在于事件和事件处理程序经过特殊处理(即,不必为类公开的所有事件实现处理程序,编译器都会插入空事件处理程序)。
Mike Spross,2009年

8

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

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


1
好的,这对我来说是合乎逻辑的,当您删除按钮时,您什么也没有告诉您其他控件都在引用它。还有其他原因吗?
jjb

3
Delphi可以做同样的事情。为菜单项和按钮分配一个动作-我一直在为反映菜单功能的工具栏按钮执行此操作。
TheArtTrooper

1
另一个原因是,当您选择一个菜单项而选择按钮时不适用时,您可能想进行某种类型的用户界面更新。在大多数情况下,做您所说的事情本质上没有什么坏处,但这只是一个有问题的设计决定,限制了灵活性。
杰拉尔德

8

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

如果将代码重构为专门处理其需要执行的工作的模块,则测试将变得更加容易。


4

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

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

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


听起来像是一个务实的政策
jjb 2009年

2

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

您为什么不能在REALbasic中做到这一点?我怀疑有任何技术原因;这可能只是他们做出的设计决定。当然,它确实可以实施更好的编码实践。


这是除事件中的调用外不允许任何内容的论点。如果您首先必须查看事件以查找代码所在的方法的名称,那么查找代码总是会花费额外的时间。同样,要想出无穷无尽的方法名称也变得很乏味。
jjb

1
不,它是不尝试重用事件中代码的参数。如果代码仅适用于事件,那么我会将其放入事件中。但是,如果需要从其他任何地方调用它,则可以将其重构为自己的方法。
Paul Lefebvre,2009年

是的,这种方法似乎很有意义。谢谢
jjb

1

假设您决定某个时间菜单应该做些不同的事情。也许仅在某些特定情况下才会发生此新更改。您忘记了该按钮,但是现在您也更改了它的行为。

另一方面,如果调用一个函数,则更改它的功能的可能性较小,因为您(或下一个家伙)知道这将带来严重的后果。


1
我不同意你的逻辑。如果您有一个菜单项和一个按钮来执行相同的操作,则它们应该执行相同的操作,而不是不同的功能。IOW,如果您有一个菜单项可以编辑数据库中的当前行,还有一个按钮可以编辑数据库中的当前行,那么两者都应做同样的事情。如果不是,则不应同时将它们都称为“编辑”。
肯·怀特2009年

@Ken菜单和按钮执行不同操作可能有充分的理由。例如在VB6中,当用户单击菜单项时,它不会在具有焦点的控件上触发丢失焦点的事件。当用户单击按钮时,它会触发丢失焦点事件。如果您依赖丢失的焦点事件(例如进行验证),则可能在菜单click事件中需要特殊的代码来触发丢失的焦点,如果发现验证错误,则中止。单击按钮就不需要此特殊代码。
MarkJ
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.