#Программирование классами в веб-приложениях

Сегодня нечасто можно встретить программиста-одиночку или приложение-однодневку. Для достижения наилучших результатов мы объединяемся в команды, развиваемся, внедряем новые технологии. Мы много работаем, пишем много кода, еще чуть-чуть и наше приложение станет практически совершенным, но вдруг мы понимаем, что больше не можем управлять им. Оно становится громоздким. Куски неструктурированного кода накладываются друг на друга, образуя черную дыру, которая засасывает весь проект в пучину хаоса.

Если ты смотришь на код в течении 5 минут и не можешь понять, что он делает, то это плохой код.

Олег Лукавенко (один из моих работодателей)

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

###Как корабль назовешь, так он и поплывёт

Разрабатывая клиентское веб-приложение, необходимо заранее подумать о том, каким образом члены одной команды разработчиков будут взаимодействовать с проектом и, что более важно, необходимо заранее предвидеть как будет выглядеть код приложения через несколько лет. Плохо, когда архитектура приложения хранится в голове одного человека. Люди непостоянны и порой даже непредсказуемы. Хорошо, когда архитектура приложения понятна с первого взгляда (и задокументирована). На сегодняшний день в вопросе архитектуры веб-приложений на клиенте хорошо зарекомендовал себя Backbone. Но я бы не стал утверждать, что Backbone — это новаторское решение. Как раз наоборот. Разработчики Backbone обратили внимание мирового сообщества на то, что всегда было в JavaScript, а именно на возможность программирования классами.

JavaScript устроен таким образом, что все в нем является объектами. Строки, массивы, числа — это объекты JavaScript. По этой причине понятие классы в JavaScript никогда не применялось или применялось разве что в разговорной речи. В JavaScript более распространено такое понятие как функции-конструкторы, но для удобства я буду называть их классами.

Далее я постараюсь раскрыть тему статьи на примере 3-х вымышленных фронтенд-разработчиков, которые выполняют одну и ту же задачу, а именно: вычислить скидку для товара и вывести цену на HTML страницу.

###Junior, Middle and Senior developers

Посмотрим что же напишет «Junior developer»:

var discount = 0.10;
$('.products li').each(function() {
    var price = $(this).find('.price').html();
    var discountedPrice = price - (price * discount);
    $(this).find('.price').html(discountedPrice);
});

CodePen

С одной стороны задача выполнена, но тут мы имеем массу явных минусов, которые затянут проект в черную дыру:

  1. Данные берутся прямо из HTML страницы;
  2. Нет модели данных;
  3. Строковая математика;
  4. Код тяжелый для зрительного восприятия;
  5. Чтобы понять назначение отдельной части кода надо прочитать весь код целиком (затрата времени и умственных ресурсов);
  6. Код не расширяемый;
  7. Высокая зависимость от HTML шаблона.

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

var discountByTypes = {
    cheap: 0.20,
    expensive: 0.10
};
$('.products li').each(function() {
    var price = $(this).find('.price').html();
    var type = $(this).data('type');

    if (type === 'cheap') {
	    var discountedPrice = price - (price * discountByTypes.cheap);
    }

    if (type === 'expensive') {
	    var discountedPrice = price - (price * discountByTypes.expensive);
    }

    $(this).find('.price').html(discountedPrice);
});

CodePen

Код стал более запутанным и еще более сложным для восприятия. Уже на этом этапе можно сказать, что через 2 месяца активной жизни веб-приложения с кодом будет невозможно работать, а главное — неприятно. Ведь работа должна доставлять удовольствие. Только представьте, если строк такого кода будет 10 000. К тому же, как поведет себя скрипт, если мы изменим шаблон? Он перестанет работать, и нам придется рефакторить весь код.

Теперь посмотрим как справится с задачей «Middle developer».

var productDiscount = {

    discountByTypes: {
        cheap: 0.20,
        expensive: 0.10
    },

    getDiscoutedPrice: function(price, discount) {
	    return price - (price * discount);
    },

    getDiscountedByType: function(price, type) {
	    return this.getDiscoutedPrice(price, this.discountByTypes[type]);
    },

    setPrice: function(elem, price, type) {
	    var discountedPrice = this.getDiscountedByType(price, type);
	    elem.html(discountedPrice);
    },

    init: function() {
	    $('.products li').each(function() {
		    var price = $(this).find('.price').html(),
			    type = $(this).data('type');

		    productDiscount.setPrice($(this).find('.price'), price, type);
	    });
    }
}

productDiscount.init();

CodePen

Уже намного лучше. «Middle developer» создал объект productDiscount, каждый метод которого имеет свое уникальное имя. Также он сделал функцию-инициализатор объекта init(). Что изменилось по сравнению с вариантом джуниора?

  1. Код стал проще для восприятия;
  2. Разделение кода на блоки;
  3. Наименование методов говорят сами за себя. Теперь не нужно читать весь код чтобы понять (изменить) отдельную его часть.
  4. Код можно расширять новыми методами и свойствами.

Данный вариант значительно лучше. Хороший код всегда читается как открытая книга. Чем больше код похож на человеческую речь, тем лучше. Это и отличает языки высокого уровня от низкоуровневых языков. Но у нас по-прежнему нет модели данных! Мы оперируем данными с веб-страницы и используем строковую математику. Что нам ответит на это «Senior developer»? Он будет программировать классами, разделив при этом данные от представления. Вы конечно же знаете о таких конструкциях, как new Object(), new String() и т. д. Но, к великому сожалению, не все знают, что могут создавать самостоятельно подобные функции-конструкторы и работать с их экземплярами.

Когда мы создаем, например, new Object(), то получаем на вооружение не только сам объект, но и все его свойства и методы. Мы можем создать десятки тысяч экземпляров объекта, а методы и свойства у него всегда будут одинаковыми (далее я буду называть это модель). Если добавить новый метод в модель, у всех 10 000 экземпляров он появится тоже. Если нам потребуется добавить новое свойство, то экземпляры его унаследуют. Данный подход раскрывает перед нами поистине безграничные возможности. Модель — это сердце веб-приложения.

Теперь давайте посмотрим, как «Senior developer» решит поставленную задачу.

function Product(config) {
    var config = config || {};

    this.getDiscountedPrice = function() {
	    return config.price - (config.price * config.discount);
    }

    return this;
}

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

Тут же хочется напомнить об общепринятых в мировом сообществе правилах:

  1. Наименование функции-конструктора (класса) всегда начинаются с заглавной буквы;
  2. Экземпляры класса начинаются строчной буквой;
  3. Постарайтесь во всех классах использовать переменную «config» для конфигурирования класса

Создадим несколько экземпляров класса Product:

// Создаем экземпляры класса Product и передаем им конфиг

var firstProduct = new Product({
    title: 'Шапка ушанка',
    price: 1899,
    discount: 0.10,
    type: 'expensive'
});

var secondProduct = new Product({
    title: 'Валенки',
    price: 399,
    discount: 0.10,
    type: 'cheap'
});

var thirdProduct = new Product({
    title: 'Варежки',
    price: 199,
    discount: 0.10,
    type: 'cheap'
});

typeof firstProduct.getDiscountedPrice // function
typeof secondProduct.getDiscountedPrice // function
typeof thirdProduct.getDiscountedPrice // function

Как видно из примера, «Senior» создал 3 продукта через конструкцию new Product(). Имейте ввиду, что «Senior» не объявлял метод getDiscountedPrice для экземпляров, но, тем не менее, наши экземпляры содержат его. Теперь firstProduct, secondProduct и thirdProduct также содержат метод getDiscountedPrice.

Теперь немного расширим нашу модель.

function Product(config) {
    var config = config || {};

    this.template = '<li><h3><%=title%></h3><span><%=price%></span></li>'; // NEW

    this.appendTo = function(elem) { // NEW
        var parsedTpl = this.template
            .replace('<%=title%>', this.getTitle())
            .replace('<%=price%>', this.getDiscountedPrice());
    
	    elem.append(parsedTpl);
    }

    this.getTitle = function() { // NEW
	    return config.title;
    }

    this.getDiscountedPrice = function() {
	    return config.price - (config.price * config.discount);
    }

    return this;
}

На этом этапе достаточно целесообразно использовать клиентский шаблонизатор, чтобы не писать бесконечные «replace». Зачастую шаблоны (они же вьюшки) подгружаются аяксом (как и данные модели), но это немного выходит за рамки данной темы. Если вы знакомы с Backbone, то понимаете что я имею ввиду. В данной публикации я постараюсь акцентировать больше внимания на фундаментальных принципах программирования классами.

Отлично. Мы только что добавили новый метод getTitle в нашу модель, который будет возвращать название товара. Также мы добавили шаблон модели «template» и возможность добавить модель на HTML страницу при помощи метода appendTo. Обратите внимание на то, что мы добавили эти методы и свойство только в одном месте, когда писали функцию-конструктор Product, но, несмотря на это, у всех экземпляров класса эти методы тоже появятся. Не правда ли здорово?

Мы отделили модель данных от представления и сделали код расширяемым. Мы больше не зависим от шаблона, и теперь можем не бояться потерять данные, изменяя шаблон. Это принцип MVC, который мы используем на фронтенде.

Не могу не обратить внимание и на структурированность кода. Он настолько понятный, так легко читается, что разобраться в нем не составляет никакого труда. Код не режет глаза как это было с примером джуниора. С таким кодом приятно работать продолжительное время. Синтаксис кода приближен к человеческой речи. Если метод называется «getPrice», значит он возвращает цену, если называется «getDiscountedPrice», значит цену со скидкой. Проще не бывает! По этой причине мы можем пригласить в проект нового программиста и обучить его, затратив минимальное количество времени.

Старайтесь придумывать для методов и свойств короткие и понятные имена, говорящие сами за себя. Это сделает ваш код понятным.

CodePen

Этот процесс бесконечный. Мы можем расширять класс Product сколько угодно. Также мы можем, например, создать новый класс ProductCollection, который будем наполнять экземплярами Product через метод add.

function ProductCollection(config) {
    var config = config,
        storage = [],
        template = '<ul class="product"></ul>';

    this.add = function(product) {
	    storage.push(product);
    }

    this.appendTo = function(elem) {
	    var products = $(template);
	    storage.forEach(function(one) {
		    one.appendTo(products); // Тут one является экземпляром класса Product и, соответственно содержит все его методы
	    });
	    elem.append(products);
    }

    this.getProductsByType = function(type) {
	    // Напишите метод самостоятельно. Должен возвращать массив товаров с типом, переданным в type
    }

    return this;
}

var collection = new ProductCollection();
collection.add(firstProduct);
collection.add(secondProduct);
collection.add(thirdProduct);

Теперь в нашей коллекции содержится 3 товара. Мы можем написать методы для работы с целыми коллекциями данных. Попробуйте самостоятельно добавить в ProductCollection новый метод getProductsByType.

CodePen

###Способы хранения классов

Каждый класс обычно помещается в файл с наименованием класса. Например, product.class.js или productCollection.class.js. Для удобства файлы размещаются в папочку «model» или «core». Это позволяет использовать системный подход и не запутаться, когда количество классов достигает десятков или сотен.

Классы пишутся только один раз, приложение пишется в отдельном файле, например app.js. Структура нашего приложения будет выглядеть так:

Cтруктура приложения

В реальных условиях вам, вероятно, не захочется хранить фрагменты HTML кода в .js файлах, как мы делали это в свойстве template. И это хорошо. Тогда можно будет расширить дерево каталогов и добавить view, в который мы будем хранить вьюшки наших классов:

Структура приложения

Для программирования классами удобно использовать менеджер проектов на Node.js. Из наиболее известных можно выделить Gulp и Grunt. Данные инструменты позволят вам собирать классы в один файл для дальнейшего использования в проекте.

Удобство работы в команде. Каждый из разработчиков вашей команды может написать новый класс и разместить его в каталоге model без всякой путаницы, а вы и ваши коллеги потом будете использовать все методы и свойства этого класса.

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

В приложенном архиве есть исходники. Для наглядности я не стал собирать проект в один .js файл, но в боевых условиях, конечно, он должен собираться.

seniorDeveloper.zip

###Прототипы и наследование

На фоне всего вышесказанного хочется также вспомнить о прототипах в JavaScript. В примерах в данной публикации мы создавали классы Product и ProductCollection. С точки зрения JavaScript данные классы ничем не отличаются от нативных классов, таких как Number, String и т.п. Они имеют одинаковые права на существование. Разница только в том, что Number и String есть в JavaScript по умолчанию, а Product и ProductCollection мы создали самостоятельно. Из этого следует, что наши классы тоже имеют прототипы.

Говоря простым языком, прототип — это возможность обратиться напрямую к функции-конструктору для того, чтобы расширить ее новыми методами и свойствами извне самой функции. Чтобы лучше понять принцип работы прототипов в рамках данной темы, предлагаю вновь вернуться к классу Product. Допустим, в праздничные дни наши продажи вырастают в 2 раза, и мы хотим увеличить цену товаров на 10%. Для этого нам понадобится новый метод, который мы можем добавить двумя способами:

  1. Написать метод напрямую в функции-конструкторе;
  2. Добавить метод через прототип.

Происходит это следующим образом:

var product = new Product({
    price: 199,
    discount: 0.20,
    type: 'cheap'
});
 
Product.prototype.increasePrice = function(percent) {
    var price = this.getPrice();
    return price + (price * percent);
}

product.increasePrice(0.10); // 218.9

Я нарочно добавил новый метод после создания экземпляра product. Несмотря на то, что экземпляр создавался выше по коду, он все равно получит метод increasePrice. Объяснение очень простое — экземпляры всегда наследуют методы класса, даже если мы добавили их в другом месте. Таким образом, при помощи прототипов мы можем расширять классы динамически. С таким же успехом мы могли бы добавить метод increasePrice во время создания класса:

...
    this.increasePrice = function(percent) {
        var price = this.getPrice();
        return price + (price * percent);
    }
...

Попробуйте добавить метод increasePrice сначала внутри функции конструктора, затем до создания экземпляра и в конце после него. Результат будет всегда один и тот же — класс Product обретет этот метод.

CodePen

###Расширение нативных классов

Как я уже говорил выше, пользовательские классы имеют одинаковые права с нативными. Из это следует только одно — их мы тоже можем расширить. Опять же, как мы можем расширить класс?

  1. Написать метод напрямую в функции-конструкторе;
  2. Добавить метод через прототип.

Попробуем расширить нативный String новым методом через его прототип. Допустим, мы хотим иметь такой метод, который смог бы удалять лишние пробелы из строки. Нам нужно выполнить trim и заменить 2 и более пробела на 1.

// Добавляем в String новый метод. Обратите внимание, что название классов всегда пишутся с заглавной буквы.

String.prototype.clearing = function() {
    return this.replace(/\s+/g, ' ').trim();
}

var string = '    Клара    у Карла украла      кораллы   ';

string; //    Клара    у Карла украла      кораллы   
string.clearing(); // Клара у Карла украла кораллы

Оператор this внутри метода всегда ссылается на текущий экземпляр. В данном случае:

this === ' Клара у Карла украла кораллы '.

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

CodePen

JavaScript нас ни в чем не ограничивает. Мы вообще можем удалить или заменить нативные методы классов. Например, по умолчанию String имеет метод split. Давайте попробуем его удалить.

console.info('Slpit works'.split(' ')); // ["Slpit", "works"]

String.prototype.split = undefined;

try {
    "Where is our method split?".split();
} catch(e) {
    console.info('Ugh, it was deleted!');
    console.error(e);
}

Если мы попытаемся выполнить такой код, то сразу поймаем «exception»:

TypeError: "Where is our method split?".split is not a function

Да, мы поступили очень плохо и нет нам никакого оправдания. Пожалуйста, не нажимайте эту ссылку:

Сделать плохо на CodePen

Добавляйте методы для извлечения емайлов и телефонов из String, получение квадрата от Number, уникальные переборы Array — никаких ограничений для полета вашей творческой мысли. Вместо того, чтобы писать функции, гораздо более интересно расширять классы новыми методами и использовать их в веб приложении, получая от процесса огромное удовольствие.

###Подведем итоги

  1. Это нативный JavaScript и его фундаментальные принципы;
  2. Мы получаем модель данных и возможность управлять ей;
  3. Данные не зависят от шаблона;
  4. Мы можем изменять шаблон (HTML) и не бояться потерять данные;
  5. Нам больше не требуется строковая математика;
  6. Классы можно импортировать в другие проекты;
  7. Код легкий для восприятия;
  8. Код легко читается благодаря синтаксису, приближенному к человеческой речи;
  9. Код расширяется до бесконечности без риска попасть в черную дыру;
  10. Код легко документируется. Достаточно лишь описать класс и его методы;
  11. Низкий порог входа в проект других разработчиков