среда, 28 октября 2009 г.

Getter или функция?

Начало 2007 года. Я тогда работал в Харькове в аутсорсинговой фирме, делали внутренний веб-сайт для страховой компании. И заказчики захотели навороченную навигацию - чтобы из любой веб-формы ты возвращался в ту форму, которая её вызвала.

Допустим, есть такие возможные маршруты:


  • Все полисы - Только новые полисы - Список застрахованных - Сведения о застрахованном - Редактирование адреса

  • Сотрудники нашей компании - Сведения о сотруднике - Редактирование адреса



"Редактирование адреса" - это одна и та же форма, но она выполняется в разных контекстах. Когда пользователь нажимает "Сохранить" или кнопку "Назад" в браузере, то мы должны вернуться в предыдущее окно. Причем маршруты могут ветвиться. История, которую ведет браузер - это не совсем то, что нужно. Допустим, ты поднялся на уровень вверх. Если теперь нажать "Назад" в браузере, то снова опустишься вниз, а надо вверх!

Всё легко можно сделать в Windows-приложении, если каждая дочерняя форма - это модальное окно. А вот с вебом гораздо тяжелее. Мы потратили на навигацию раз в 10 больше времени, чем планировал заказчик. За прошедшее время я (hopefully) поумнел, поэтому сейчас я бы пошел одним из следующих путей:


  • Убедил бы заказчика, что это слишком сложно, и надо подкорректировать требования.

  • Воспользовался бы Workflow Foundation и Page Flow.

  • Переписал бы всё так, чтобы история перемещения по маршруту хранилась прямо в URL, вроде такой: /all-policies/new-policies-only/insured-person/insured-person-details/edit-address . Кроме того, очень бы помог ASP.NET MVC или другой MVC-фреймворк.



Но проект уже был выполнен больше чем наполовину, кардинально что-то менять было поздно. Кроме того, работающий Page Flow появился только через год, а ASP.NET MVC и того позже. Были, правда, Castle и другие варианты для MVC, но, как я уже говорил, менять что-то кардинально всё равно было поздно.

Один из ведущих программистов написал навороченный компонент, который пытался всё это отслеживать и перенаправлять пользователя, куда надо. Даже если тот нажал "Назад" или просто вручную набрал в браузере какой-то левый адрес. История хранилась в сессии. Как я сейчас понимаю, это была попытка создать MVC controller, но поверх WebForms.

Хотя парень очень толковый, и старался он долго, но задача была слишком сложная. Вроде как работало, но время от времени вылазили новые глюки. Однажды меня попросили помочь. Начинаю отлаживать test case - всё работает! Уже хотел радостно закрыть баг, но решаю проверить ещё раз, на тестовом сервере, не локально. Не работает. Хм. Запускаю ещё раз локально - тоже не работает. Отлаживаю - работает. Отлаживаю ещё раз - не работает. Что за чушь? Потратил полдня, пока понял...

Оказалось, парень создал класс, где хранил историю в виде стека. Последний (текущий) элемент он считывал не через функцию (вроде MyHistory.GetLastItem() ), а через свойство (назовем его MyHistory.LastItem ). В момент чтения элемент удалялся из стека. Код красиво смотрелся. Глюк не вылазил, если я просматривал MyHistory.LastItem в отладчике, тем самым нечаянно вытаскивая один (лишний) элемент из стека. Если же я просмотривал это свойство больше одного раза, то вытаскивал слишком много, и опять-таки вылазил глюк, но уже немного другой.

Мораль этой длинной истории: если getter меняет внутренее состояние объекта, то гораздо безопаснее использовать функцию.

6 комментариев:

Анонимный комментирует...

+1. Имхо такое должно ощущаться инстинктивно.

Igor Korkhov комментирует...

Когда пользователь нажимает "Сохранить" или кнопку "Назад" в браузере, то мы должны вернуться в предыдущее окно.

Очень интересно, как вы хотели запретить браузеру перейти на предыдущую показанную страницу при нажатии кнопки "Назад"? Браузер не знает про вашу иерерхию и не должен знать. И как бы помогла сессия на сервере?

Mikhail Krivoshein комментирует...

Это ж редкостный анти-паттерн, когда геттер меняет состояние объекта! Читайте книги по Java, должно помочь.

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

Valik комментирует...

Михаил меня опередил.

Именно так и работает PageFlow (кусок майкрософтовского блока WCSF). Ты можешь сказать, что хочешь делать, если юзер попал на неправильную страницу: выдать ошибку, перенаправить на правильную или на дефолтную.

Тут как бы не имеет особого значения, нажал ли юзер кнопки Вперед-Назад, или просто набрал адрес.

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

Oleksii Novikov комментирует...

Куда катится этот мир...

Это касается не только "геттеров" в тернинах дотнет и иже с ним, а любых функций с приставкой get. И обьявлены они должны быть с модификатором const во избежание.

За нарушение - пропускать автора через кернел-левел дебуггер

Igor Korkhov комментирует...

Михаил, да, я понимаю про наличие поля Cache-Control в заголовке. Но есть протокол, а есть реалии - не все браузеры и все кэширующие HTTP-proxy его соблюдают. В частности, RFC 2616 в секции 14.9.1 прямо говорит: "Most HTTP/1.0 caches will not recognize or obey this directive." Да, я понимаю, что речь у Валентина шла про интранет, где можно требовать определенных версий браузеров, ставить правильные прокси и т.п. Но все равно такой подход представляется мне неправильным, даже если не с точки зрения технической, то с точки зрения пользовательского интерфейса: пользователь всегда ожидает, что кнопка "Back" вернет его туда, где он только что был.

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

Ratings by outbrain