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

69

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

  • 6
    Принято решение, поскольку я считаю, что все, кто интересуется программированием на Delphi, должны осознавать, что это плохая практика. До того, как я начал использовать Actions (как упомянул Роб Кеннеди в его пункте № 3), я cooked up довольно много приложений для spaghetti , которые просто кошмар, и очень жаль, так как приложения были довольно хорошими. Но я стал ненавидеть свое творение. Ответ Роба очень приятный и исчерпывающий, ИМО.
Теги:
vb6
coding-style
realbasic

9 ответов

83
Лучший ответ

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

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 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;

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

  • Один из них заключается в том, чтобы полностью избавиться от метода 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;
    

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

  • Компонент TAction, представленный в Delphi 4, разработан специально для описанной вами ситуации, где есть несколько путей пользовательского интерфейса к той же команде. (Другие языки и среды разработки предоставляют аналогичные концепции, они не уникальны для Delphi.) Поместите код обработки событий в обработчик событий TAction OnExecute, а затем назначьте это действие свойству Action как кнопки, так и пункт меню.

    procedure TJbForm.Action1Click(Sender: TObject);
    begin
      // Do something
      // (Depending on how closely this event 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, и любые связанные элементы управления будут автоматически отключены. Не нужно беспокоиться о том, что команда отключена с помощью панели инструментов, но все же включена через меню, например. Вы даже можете использовать событие action OnUpdate, чтобы действие могло обновляться на основе текущих условий, а не вам нужно знать, когда что-то происходит, что может потребовать сразу установить свойство Enabled.

  • 1
    Отличный ответ, спасибо. Я особенно впечатлен подходом TAction, о котором я не знал раньше, но который звучит как лучший способ приблизиться к этому. На самом деле Delphi, кажется, хорошо покрывает эту область, что позволяет использовать все подходы. Кстати, вы упоминаете, что TAction позволяет автоматически отключать связанные элементы управления. Одно из изменений в отношении к стилю, которое мне в последнее время нравится, это тенденция не отключать элементы управления, когда действие недоступно, а вместо этого позволить пользователю нажать на элемент управления и затем дать им сообщение, объясняющее, почему действие не происходит.
  • 0
    Я полагаю, что некоторые из преимуществ подхода TAction перед другими способами оказываются неактуальными, если использовать этот стиль.
Показать ещё 4 комментария
15

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

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

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

  • 0
    ИМО это не ответ на вопрос. Я спросил, почему вы не можете сделать A, а не B, и этот ответ просто говорит, потому что B лучше!
  • 0
    Кстати, я не имею в виду, что в грубом смысле это просто мое наблюдение, я думаю, что Джеральд ударил ногтем по голове своим ответом
Показать ещё 5 комментариев
10

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

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

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

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

  • 1
    Спасибо, что подняли это. Я думаю, что это ясно иллюстрирует мысль, которую вы высказали. Мне нравится идея, что логический параметр допускает различное поведение, когда событие действительно происходит, а не выполняется с помощью кода.
  • 1
    Разное поведение, которое вы можете иметь, если вы передадите ноль в качестве отправителя;)
Показать ещё 2 комментария
8

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

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

  • 0
    Я думаю, что это очень хороший момент
8

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

  • 1
    Я согласен с инкапсуляцией и разделением, но события click / dbclick на элементах управления vb6 никогда не являются частными. И если они не были сделаны частными, это потому, что кто-то считал, что вред будет минимальным.
  • 0
    Ни в Delphi / Lazarus они не опубликованы (RTTI'd)
Показать ещё 4 комментария
8

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

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

  • 1
    ОК, это логично для меня, когда вы удаляете кнопку, вам нечего сказать, что другие элементы управления ссылаются на нее. Есть ли другие причины?
  • 3
    Delphi может сделать то же самое. Назначьте действие элементу меню и кнопке - я делаю это все время для кнопок панели инструментов, которые отражают функциональность меню.
Показать ещё 1 комментарий
4

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

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

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

  • 0
    Звучит как прагматичная политика
2

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

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

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

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

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

  • 1
    Я не согласен с вашей логикой. Если у вас есть пункт меню и кнопка, чтобы сделать то же самое, они должны делать то же самое , а не работать по-другому. IOW, если у вас есть пункт меню, который позволяет вам редактировать текущую строку в базе данных, и кнопка, которая позволяет вам редактировать текущую строку в базе данных, оба должны делать то же самое; если нет, их не следует называть «Редактировать».
  • 0
    @Ken У меню и кнопки могут быть веские причины делать разные вещи. Например, в VB6, когда пользователь щелкает пункт меню, он не запускает событие потерянного фокуса в элементе управления с фокусом. Когда пользователь нажимает кнопку, он запускает события потерянного фокуса. Если вы полагаетесь на события потерянного фокуса (например, для проверки), вам может понадобиться специальный код в событии щелчка меню, чтобы вызвать потерянный фокус и прервать работу при обнаружении ошибок проверки. Вам не понадобится этот специальный код от нажатия кнопки.

Ещё вопросы

Сообщество Overcoder
Наверх
Меню