воскресенье, 8 сентября 2013 г.

Автотесты и психология разработки

Человек должен получать обратную связь от своей работы - чем быстрее тем лучше.

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

Как разработчику понять, что то, что он сделал - будет работать?
Как узнать, хорошо он сегодня поработал, или плохо?


Работа бывает разной.

Front-end - разработчику легче, потому что он проверяет результаты своей работы непосредственно. Но даже он, при более сложных интерфейсах, может пропустить некоторые ситуации, например если интерфейс строится из виджетов, которые могут по-разному компоноваться. Даже простой верстальщик должен понимать, что его страничка будет просматриваться в другой среде - на смартфонах и других устройствах.

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

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

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

Как это можно решить?

Можно писать бэкенд одновременно с UI. Это даст возможность проверить результат, но не подходит для сложных систем. Если UI и бэкенд пишут два разных человека - куча времени может уйти на то, чтобы один разработчик указывал другому на его баги (сам был в такой ситуации).

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


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

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


Что ещё?

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

Автотесты также дают возможность разработчику посмотреть на свой код с другой точки зрения, как будто написанный модуль использует какая-то другая система. Я считаю, это очень важно, т.к. помогает уйти от "замыленного взгляда": лучше понять, насколько корректно и удобно организован API, увидеть пропущенные ранее недочёты в коде модуля.

И повторюсь: обратная связь - это основа любого обучения и личного роста.

воскресенье, 21 июля 2013 г.

Шаблонный метод для виджетов на JavaScript

Недавно написал приложение с интерфейсом на JavaScript и Backbone.js. Закончилось всё тем, что при построении интерфейса у меня получилась трёхэтажная иерархия классов, с порядка 10 конечными дочерними классами. Пришла пара идей, как лучше справляться с такими иерархиями, о чём и расскажу.

Навороченные интерфейсы на JS только недавно вошли в моду, в то время как принципы ООП уже совсем не новы. Веб-приложения становятся мало отличимыми от обычных приложений. Так что пришло время соединять моду с классикой.

Теорию интереснее всего рассматривать на примерах.. каким веб-приложением вам хотелось бы пользоваться?

Немного фантазии

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

Встал утром - и за 10 минут проверил всё, что требует моего внимания, на одной странице.

Пример, конечно, немного надуманный, т.к. подсоединять всевозможные сервисы к dashboard при отсутствии API сложно и нецелесообразно. Вместо такого dashboard'а я использую, в основном, почту и закладки в браузере.


Реализация dashboard

Наш dashboard будет просто «доской», заполненной виджетами. А в каждом виджете может быть что угодно:

Каждый виджет - это объект. Я использую backbone.js для построения интерфейсов, который мы рассматривать подробно не будем, зато мы рассмотрим простой ООП-паттерн на примере отображения объекта.. Backbone.js даёт нам базовый класс Backbone.View, от которого мы и будем наследовать каждый виджет. Метод, отображающий виджет, в Backbone принято называть render():
(Целиком, простейшее приложение на backbone.js можно посмотреть тут)

Считаем, что мы уже написали приложение, которое получает данные с сервера и отображает один виджет по этим данным:

Но мы хотим множество виджетов, с разным функционалом!

Что общего у виджетов?
  1. Они привязаны к соответствующим моделям Backbone, с отображаемыми данными.
  2. Их можно перемещать, менять размер, скрывать, удалять. Это значит, что в методе render() мы добавим общие для всех элементы управления.
То, что есть общий функционал, уже требует, чтобы мы создали базовый класс виджета:

App.views.Widget = Backbone.View.extend({
    initialize: function(){
        // перерисовка при изменении модели
        this.model.listenTo(this.model, 'change', this.render);
    },

    render: function(){
        // Отображаем виджет в DOM, используя данные модели:
        this.$el.html(this.template({model: this.model.attributes}));
        // Добавляем элементы управления:
        ...
    },
});

Чем отличаются наши виджеты друг от друга?
  1. Одна разновидность просто отображает данные: какой-нибудь график, или картинку погоды.
  2. Через другой тип виджетов мы можем быстро изменить какое-то значение. Например, поменять статус в социальных сетях. Удобнее всего, когда мы заполнили поле, и оно сохранилось автоматически. Полей может быть несколько - например мы можем захотеть менять «товар дня» в своём интернет-магазине, и назначать ему скидку. Всё это отобразится на сайте немедленно (это сделает серверная часть, которую мы не рассматриваем).
  3. Третья разновидность обновляется в реальном времени - например лента Твиттера, сообщения из разных соц. сетей, или мониторинг форумов. Совсем забыл: это примерно то же, что и первая разновидность. Но первая обновляется, скажем, раз в час. Это можно делать по таймеру. А для реального времени нам нужно держать постоянное соединение с сервером - используя websocket.


Пример с третьей разновидностью виджета говорит о том, что мы не всегда знаем, какой система будет завтра. Можно было бы сразу реализовать всё через websocket'ы, но предположим, что мы уже реализовали (1), а про вебсокеты вспомнили через месяц, когда уже накопилось с десяток виджетов типа (1) и (2), и они уже выстроились в некую иерархию... и добавление функционала потребует изменений иерархии. А именно, нам придётся подсоединиться к вебсокетам в методе initialize():


App.views.TweetsWidget = App.views.Widget.extend({
    initialize: function(){
        // Инициализация базового класса:
        App.views.Widget.prototype.initialize.apply(this, arguments);
        // Подписываемся на событие от вебсокета:
        App.websocket.on('graph-update', _.bind(function(){
           // обновляем Backbone-модель, содержащую данные виджета:
           this.fetch();
        }, this));
    },
});

А также мы можем захотеть добавлять новые элементы управления. Тогда нам придётся переопределить метод render():


App.views.TweetsWidget = App.views.Widget.extend({
    initialize: function(){
        ...
    },
    render: function(){
        // Отрисовка базового класса:
        App.views.Widget.prototype.render.apply(this, arguments);
        // Добавление новых элементов управления:
        ...
    },
});

Вернёмся от частности к общему. Мы хотим расширять приложение. При этом мы не знаем, какой функционал понадобится завтра. Раз уж мы пишем на backbone.js, в классе нового виджета нам нужно переопределить 2 метода - render() и initialize(), и изменять придётся очень по-разному.

Иерархия классов может стать слишком запутанной.
  • На 1-й итерации разработки мы реализуем базовый класс виджета, и несколько дочерних классов простых графиков.
  • На 2-й итерации - сделаем редактируемые виджеты, которые будут автоматически сохранять изменённые поля. Базовым классом для них будет App.views.EditableWidget.
  • На 3-й - добавим в некоторые графики функцию zoom, что потребует добавления контролов, котрые будут появляться по наведению мыши, а также создадим виджет реального времени TweetsWidget.

С каждым наследованием функции render() и initialize() оборачивается в соответствующие функции дочернего класса. Дополнительное неудобство в том, что если нужно скомпоновать или обработать данные перед отрисовкой, нам придётся как-то передавать их в render() базового класса. Т.е. придётся добавлять аргументы, что выглядит некрасиво:

render: function(customData) {
    if (customData === undefined) {
        customData = {};
    }
    var data = customData;
    data[model] = this.model.attributes;
    this.$el.html(this.template(data));
}

Получившиеся связи методов показаны синим и зелёным:
Мы слишком увлеклись. Мы ведь не обязаны переопределять методы render() и initialize() в каждом дочернем классе. Единственное что мы должны - это изменить их в дочернем классе.

Самое время вспомнить паттерн «шаблонный метод».

Оставим входные аргументы render() в покое. Не будем переопределять методы. Если нам нужно изменить данные - добавим хук для получения кастомных данных:

render: function() {
    var data = {}

    // Добавление кастомных данных от дочернего класса, если они есть:
    if (this._preRenderHook !== undefined) {
        _.extend(data, this._preRenderHook());
    }

    // Старая функциональность:
    data[model] = this.model.attributes;
    this.$el.html(this.template(data));
}

Если же нужно добавить новые элементы управления - добавим хук для создания этих элементов управления:

render: function() {
    var data = {}

    // Добавим данные дочернего класса, если они есть:
    if (this._preRenderHook !== undefined) {
        _.extend(data, this._preRenderHook());
    }

    // Старая функциональность:
    data[model] = this.model.attributes;
    this.$el.html(this.template(data));

    // Элементы управления:
    if (this._postRenderHook !== undefined) {
        this._postRenderHook();
    }
}

Получится иерархия без запутанных связей:
Конечно же, в случае сложной иерархии, мы можем захотеть переопределять хуки в дочерних классах, чтобы их расширить, или даже добавлять хуки в хуки. Но с этим лучше не слишком усердствовать, особенно если будущее приложения до конца не ясно. Преимущество в том, что сейчас мы можем сделать всё в виде плоской структуры.

Самое интересное: мы можем не думать о дочерних классах, когда создаём базовый. Просто создавая новый вид виджета, добавим нужные ему хуки в базовый класс. Какой бы ни была глубина иерархии, мы можем добавить хук со своим именем в базовый класс, специально для конкретного дочернего класса - тогда не придётся вообще ничего переопределять!

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

Полезности применения такого подхода:
  • Мы можем добавлять хуки быстро, на любой стадии разработки. При этом они не повлияют на уже реализованные классы.
  • Мы можем встраивать хуки в любое место в методе базового класса, а не только дополнять его в начале и в конце, как в случае с переопределением. Если хуков будет слишком много, это может запутать код метода - но зато мы можем легко контролировать очерёдность инициализации элементов виджета.
  • Структура кода - плоская. Методы не вкладываются друг в друга при наследовании, а просто добавляются в базовый класс. То есть код гораздо легче понять.
Для наглядности, пример потока выполнения для метода render():