Почему плохая практика вызывать обработчик событий из кода? - PullRequest
72 голосов
/ 05 июня 2009

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

Ответы [ 9 ]

83 голосов
/ 06 июня 2009

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

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

15 голосов
/ 05 июня 2009

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

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

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

10 голосов
/ 06 июня 2009

Это расширенный ответ, как и было обещано. В 2000 году мы начали писать приложение с использованием Delphi. Это был один EXE и несколько DLL, содержащих логику. Это была киноиндустрия, так что были клиенты DLL, DLL бронирования, касса DLL и биллинг DLL. Когда пользователь захотел выставить счет, он открыл соответствующую форму, выбрал клиента из списка, затем логика OnSelectItem загрузила театры клиентов в следующее поле со списком, затем после выбора театра следующее событие OnSelectItem заполнило третье поле со списком информации о фильмах, которая не была выставлено еще. Последней частью процесса было нажатие кнопки «Сделать счет». Все было сделано как процедура мероприятия.

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

Через два года кто-то решил внедрить другую функцию - чтобы пользователю, работающему с данными о клиентах в другом модуле (модуле клиентов), была представлена ​​кнопка с названием «Выставить счет этому клиенту». Эта кнопка должна запустить форму счета и представить ее в таком состоянии, как если бы пользователь вручную выбирал все данные (пользователь должен был иметь возможность просмотреть, внести некоторые изменения и нажать волшебную кнопку «Сделать счет»). ). Так как данные клиента были одной DLL, а биллинг - другой, это был EXE, который передавал сообщения. Таким образом, очевидная идея заключалась в том, что разработчик данных клиента будет иметь одну подпрограмму с одним идентификатором в качестве параметра, и что вся эта логика будет внутри модуля биллинга.
Представь, что случилось. Поскольку вся логика была внутри обработчиков событий, мы потратили огромное количество времени, пытаясь на самом деле не реализовывать логику, а пытаться имитировать пользовательскую активность - например, выбирать элементы, приостанавливать Application.MessageBox внутри обработчиков событий с использованием переменных GLOBAL и т. Д. Представьте себе - если бы у нас были даже простые логические процедуры, вызываемые внутри обработчиков событий, мы бы смогли ввести логическую переменную DoShowMessageBoxInsideProc в сигнатуру процедуры. Такая процедура могла быть вызвана с параметром true, если вызван из обработчика события, и с параметрами FALSE, когда вызван из внешнего места.

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

8 голосов
/ 06 июня 2009

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

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

8 голосов
/ 05 июня 2009

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

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

8 голосов
/ 05 июня 2009

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

4 голосов
/ 05 июня 2009

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

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

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

2 голосов
/ 05 июня 2009

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

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

1 голос
/ 05 июня 2009

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

С другой стороны, если вы вызываете функцию, у вас меньше шансов изменить то, что она делает, поскольку вы (или следующий парень) знаете, что это будет иметь плохие последствия.

...