Почему это плохая практика, чтобы вызвать eventhandler из кода?


скажем, у вас есть пункт меню и кнопка, которые выполняют ту же задачу. Почему не рекомендуется помещать код задачи в событие действия одного элемента управления, а затем вызывать это событие из другого элемента управления? Delphi позволяет это, как и vb6, но realbasic этого не делает и говорит, что вы должны поместить код в метод, который затем вызывается как меню, так и кнопкой

9 72

9 ответов:

это вопрос того, как организована ваша программа. В описанном сценарии поведение элемента меню будет определяться в терминах кнопки:

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;

любой из этих трех реализаций будет работать, но почему пункт меню должен быть так зависим от кнопки? что такого особенного в кнопке, что она должна определять пункт меню? Если новый дизайн пользовательского интерфейса покончил с кнопками, что произойдет с меню? Лучший способ вынесем действия обработчика событий, поэтому он не зависит от элементов управления, к которым он прикреплен. Есть несколько способов сделать это:

  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. The TAction компонент, представленный в Delphi 4, разработан специально для ситуации, которую вы описали, где есть несколько путей пользовательского интерфейса к одной и той же команде. (Остальные языки и среды разработки предоставляют аналогичные концепции; это не уникально для Delphi.) Поместите код обработки событий в TAction ' s 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;
    

    хочу добавить еще один элемент пользовательского интерфейса, который действует как кнопка? Не проблема. Добавьте его, установите его Action собственность, и вы закончите. Нет необходимости писать больше кода, чтобы новый элемент управления выглядел и действовал как старый. Ты уже написал этот код один раз.

    TAction выходит за рамки только обработчики событий. это позволяет гарантировать, что элементы управления пользовательского интерфейса имеют единые настройки собственность, включая подписи, подсказки, видимость, доступность и значки. Если команда не действительна в то время, установите действие Enabled свойство соответственно, и любые связанные элементы управления будут автоматически отключены. Не нужно беспокоиться о том, что команда отключена через Панель инструментов, но все еще включена через меню, например. Вы даже можете использовать действие OnUpdate событие, чтобы действие могло обновляться на основе текущих условий, вместо того, чтобы вам нужно было знать, когда что-то происходит, что может потребовать от вас установить Enabled собственность сразу.

потому что вы должны отделить внутреннюю логику от какой-то другой функции и вызвать эту функцию...

  1. из обоих обработчиков событий
  2. отдельно от кода, если вам нужно

Это более элегантное решение и гораздо проще в обслуживании.

Это ответ на расширение, как и было обещано. В 2000 году мы начали писать приложения с использованием Delphi. Это был один EXE и несколько DLL, содержащих логику. Это было кино, так что не было клиентов ДЛЛ, бронирование dll файлы, кассовые DLL и dll файлы биллинга. Когда пользователь хотел сделать биллинг, он открыл соответствующую форму, выбрал клиента из списка, затем onselectitem logic загрузил театры клиентов в следующее поле со списком, а затем после выбора theater next onselectitem event заполнил третье поле со списком с информацией о фильмах, которые еще не выставлено. Последней частью процесса было нажатие кнопки "сделать счет". Все было сделано как процедура события.

потом кто-то решил, что мы должны иметь расширенную поддержку клавиатуры. Мы добавили вызывающие обработчики событий из других даже обработчиков.. Рабочий процесс обработчиков событий начал усложняться.

через два года кто-то решил реализовать еще одну функцию - чтобы пользователь работал с данными клиентов в другой модуль (модуль customers) должен быть представлен кнопкой под названием "выставить счет этому клиенту". Эта кнопка должна запустить форму счета и представить ее в таком состоянии, как будто это был пользователь, который вручную выбирал все данные (пользователь должен был иметь возможность смотреть, вносить некоторые корректировки и нажимать волшебную кнопку "Сделать счет"). Поскольку данные клиента были одной DLL, а биллинг-другой, это был EXE, который передавал сообщения. Таким образом, очевидная идея заключалась в том, что разработчик данных о клиентах будет иметь один рутина с одного ID в качестве параметра, и что вся эта логика будет внутри модуля биллинга.
Представь, что случилось. Поскольку вся логика была внутри обработчиков событий, мы потратили огромное количество времени, пытаясь фактически не реализовать логику, но пытаясь имитировать активность пользователя – например, выбирая элементы, приостанавливая приложение.MessageBox внутри обработчиков событий с использованием глобальных переменных, и так один. Представьте себе – если бы у нас были даже простые логические процедуры, вызванные внутри обработчиков событий, мы бы смогли введите логическую переменную DoShowMessageBoxInsideProc в сигнатуру процедуры. Такая процедура могла быть вызвана с параметром true, если вызывается из обработчика событий, и с параметрами FALSE, если вызывается из внешнего места.

Так вот что научило меня не помещать логику непосредственно в обработчики событий GUI, за возможным исключением небольших проектов.

разделение проблем. частное событие для класса должно быть инкапсулировано внутри этого класса и не вызываться из внешних классов. Это делает ваш проект легче изменить в будущем, если у вас есть сильные интерфейсы между объектами и минимизировать появление нескольких точек входа.

предположим, в какой-то момент Вы решите, что пункт меню больше не имеет смысла, и вы хотите избавиться от пункта меню. Если у вас есть только один другой элемент управления, указывающий на обработчик событий элемента меню, это может быть не большой проблемой, вы можете просто скопировать код в обработчик событий кнопки. Но если у вас есть несколько различных способов вызова кода, вам придется сделать много изменений.

лично мне нравится, как Qt обрабатывает это. Существует класс QAction с его собственным обработчик событий, который можно подключить, а затем QAction связан с любыми элементами пользовательского интерфейса, которые должны выполнить эту задачу.

еще одна большая причина для тестируемости. Когда код обработки событий скрыт в пользовательском интерфейсе, единственный способ проверить это - либо ручное тестирование, либо автоматическое тестирование, которое сильно привязано к пользовательскому интерфейсу. (например, откройте меню A, Нажмите кнопку B). Любое изменение в пользовательском интерфейсе, естественно, может разбить десятки тестов.

Если код рефакторинг в модуль, который имеет дело исключительно с работой, которую он должен выполнить, то тестирование становится намного проще.

Это аккуратнее, очевидно. Но простота использования и производительность, конечно, также всегда важны.

в Delphi я обычно воздерживаюсь от этого в серьезных приложениях, но я называю eventhandlers в мелочах. Если мелкие вещи каким-то образом превращаются во что-то большее, я очищаю его, и обычно в то же время увеличиваю логическое разделение пользовательского интерфейса.

Я знаю, что это не будет иметь значения в Lazarus/Delphi. Другие языки могут иметь более особое поведение, связанное с обработчиков событий.

Почему это плохая практика? Потому что гораздо проще повторно использовать код, когда он не встроен в элементы управления пользовательского интерфейса.

Почему вы не можете сделать это в REALbasic? Я сомневаюсь, что есть какая-то техническая причина; это, вероятно, просто дизайнерское решение, которое они приняли. Это, безусловно, обеспечивает лучшую практику кодирования.

предположим, что в какой-то момент Вы решили, что меню должно сделать что-то немного по-другому. Возможно, это новое изменение происходит только при определенных обстоятельствах. Вы забыли о кнопке, но теперь вы также изменили ее поведение.