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

假设你有一个菜单项和一个button来完成相同的任务。 为什么不好的做法是将任务的代码放到一个控件的action事件中,然后从另一个控件中调用该事件? delphi允许这与vb6一样,但realbasic不,并说你应该把代码放入一个方法,然后调用的菜单和button

这是你的程序如何组织的问题。 在你描述的场景中,菜单项的行为将按照button的方式定义:

 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; 

这三种实现中的任何一种都可以工作,但是为什么菜单项要依赖于button呢? 有什么特别的button,它应该定义菜单项? 如果一个新的UIdevise消除了button,菜单会发生什么? 更好的方法是将事件处理程序的操作分解出来,使其独立于所连接的控件。 有几种方法可以做到这一点:

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

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

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

    通过这种方式,真正实现代码的代码不会直接绑定到任何一个组件上,而是让您可以更轻松地更改这些控件 ,例如通过重命名或用不同的控件replace它们。 将代码从组件中分离出来,导致我们转向第三种方式:

  3. 在Delphi 4中引入的TAction组件专为您所描述的情况而devise,其中有多个到相同命令的UIpath。 (其他语言和开发环境提供了类似的概念;这不是Delphi独有的)将事件处理代码放入TActionOnExecute事件处理程序,然后将该操作分配给button和菜单项的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; 

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

    TAction不仅仅是事件处理程序。 它可以让你确保你的UI控件具有统一的属性设置 ,包括标题,提示,可视性,启用性和图标。 当一个命令无效时,相应地设置动作的Enabled属性,任何链接的控件将自动被禁用。 例如,无需担心通过工具栏禁用命令,但仍然通过菜单启用该命令。 您甚至可以使用该操作的OnUpdate事件,以便该操作可以根据当前条件自行更新,而不必在发生可能需要立即设置Enabled属性的情况时才需要知道。

因为你应该分开内部逻辑到其他function,并调用这个function…

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

这是一个更优雅的解决scheme,更容易维护。

按照承诺,这是一个扩展答案。 在2000年,我们开始用Delphi编写一个应用程序。 这是一个EXE和很less的DLL包含逻辑。 这是电影业,所以有客户DLL,预订DLL,票房DLL和计费DLL。 当用户想要做结算时,他打开适当的表格,从列表中select客户,然后OnSelectItem逻辑加载客户剧院到下一个combobox,然后select剧院下一个OnSelectItem事件填充第三个combobox与关于电影的信息,那还没有帐单了。 这个过程的最后一部分是按下“Do Invoice”button。 一切都是按照事件程序完成的。

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

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

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

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

假设在某一时刻,你决定菜单项不再有意义,并且你想摆脱菜单项。 如果你只有一个控件指向菜单项的事件处理程序,这可能不是一个大问题,你可以将代码复制到button的事件处理程序中。 但是如果你有几种不同的方式可以调用代码,你将不得不做很多改变。

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

另一个重要的原因是可测性。 当事件处理代码被隐藏在用户界面中时,唯一的方法就是通过手动testing或者与用户界面密切相关的自动化testing。 (例如打开菜单A,点击buttonB)。 UI中的任何改变自然可以打破几十个testing。

如果代码被重构成专门处理它需要执行的工作的模块,那么testing变得非常容易。

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

在delphi,我通常不会在严重的应用程序中,但我把事件处理程序的小东西。 如果小东西莫名其妙地变成更大的东西,我把它清理干净,通常同时增加逻辑UI分离。

我知道,在拉撒路/delphi中,这并不重要。 其他语言可能有更多的特殊行为附加到事件处理程序。

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

为什么你不能在REALbasic中做到这一点? 我怀疑是否有任何技术原因。 这可能只是他们做出的一个devise决定。 它当然会强化更好的编码实践。

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

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