- PVSM.RU - https://www.pvsm.ru -
В JavaScript существуют разные способы создания объектов. В частности, речь идёт о конструкциях, использующих ключевое слово class
и о так называемых фабричных функциях (Factory Function). Автор материала, перевод которого мы публикуем сегодня, исследует и сравнивает эти две концепции в поисках ответа на вопрос о плюсах и минусах каждой из них.
Ключевое слово class
появилось в ECMAScript 2015 (ES6), в результате теперь у нас есть два конкурирующих паттерна создания объектов. Для того чтобы их сравнить, я опишу один и тот же объект (TodoModel
), пользуясь синтаксисом классов, и применив фабричную функцию.
Вот как выглядит описание [2] TodoModel
с использованием ключевого слова class
:
class TodoModel {
constructor(){
this.todos = [];
this.lastChange = null;
}
addToPrivateList(){
console.log("addToPrivateList");
}
add() { console.log("add"); }
reload(){}
}
Вот — описание [3] того же самого объекта, выполненное средствами фабричной функции:
function TodoModel(){
var todos = [];
var lastChange = null;
function addToPrivateList(){
console.log("addToPrivateList");
}
function add() { console.log("add"); }
function reload(){}
return Object.freeze({
add,
reload
});
}
Рассмотрим особенности этих двух подходов к созданию классов.
Первая особенность, которую можно заметить, сравнивая классы и фабричные функции, заключается в том, что все члены, поля и методы объектов, создаваемых с помощью ключевого слова class
, общедоступны.
var todoModel = new TodoModel();
console.log(todoModel.todos); //[]
console.log(todoModel.lastChange) //null
todoModel.addToPrivateList(); //addToPrivateList
При использовании фабричных функций общедоступно только то, что мы сознательно открываем, всё остальное скрыто внутри полученного объекта.
var todoModel = TodoModel();
console.log(todoModel.todos); //undefined
console.log(todoModel.lastChange) //undefined
todoModel.addToPrivateList(); //taskModel.addToPrivateList
is not a function
После того, как объект создан, я ожидаю, что его API не будет меняться, то есть, жду от него иммутабельности. Однако мы можем легко изменить реализацию общедоступных методов объектов, созданных с помощью ключевого слова class
.
todoModel.reload = function() { console.log("a new reload"); }
todoModel.reload(); //a new reload
Эту проблему можно решить [4], вызывая Object.freeze(TodoModel.prototype)
после объявления класса, или используя декоратор для «заморозки» классов, когда он будет поддерживаться.
С другой стороны, API объекта, созданного с помощью фабричной функции, иммутабельно. Обратите внимание на использование команды Object.freeze()
для обработки возвращаемого объекта, который содержит лишь общедоступные методы нового объекта. Закрытые данные этого объекта могут быть модифицированы, но сделать это можно только посредством этих общедоступных методов.
todoModel.reload = function() { console.log("a new reload"); }
todoModel.reload(); //reload
Объекты, создаваемые с помощью ключевого слова class
, подвержены давней проблеме потери контекста this
. Например, this
теряет контекст во вложенных функциях. Это не только усложняет процесс программирования, подобное поведение ещё и является постоянным источником ошибок.
class TodoModel {
constructor(){
this.todos = [];
}
reload(){
setTimeout(function log() {
console.log(this.todos); //undefined
}, 0);
}
}
todoModel.reload(); //undefined
А вот как this
теряет контекст при использовании соответствующего метода в событии DOM:
$("#btn").click(todoModel.reload); //undefined
Объекты, созданные с помощью фабричных функций, от подобной проблемы не страдают, так как тут ключевое слово this
не используется.
function TodoModel(){
var todos = [];
function reload(){
setTimeout(function log() {
console.log(todos); //[]
}, 0);
}
}
todoModel.reload(); //[]
$("#btn").click(todoModel.reload); //[]
Стрелочные функции частично решают проблемы, связанные с потерей контекста this
при использовании классов, но, в то же время, они создают новую проблему. А именно, при использовании стрелочных функций в классах ключевое слово this
больше не теряет контекст во вложенных функциях. Однако this
теряет контекст при работе с событиями DOM.
Я переработал [4] класс TodoModel
с использованием стрелочных функций. Стоит отметить, что в процессе рефакторинга, при замене обычных функций на стрелочные, мы теряем кое-что важное для читаемости кода: имена функций. Взгляните на следующий пример.
//имя указывает на цель использования функции
setTimeout(function renderTodosForReview() {
/* code */
}, 0);
//код менее понятен при использовании стрелочной функции
setTimeout(() => {
/* code */
}, 0);
При использовании стрелочных функций мне приходится читать текст функции для того, чтобы понять, что именно она делает. Мне же хотелось бы прочесть имя функции и понять её суть, а не читать весь её код. Конечно, можно обеспечить хорошую читабельность кода и при использовании стрелочных функций. Например, можно завести привычку использовать стрелочные функции так:
var renderTodosForReview = () => {
/* code */
};
setTimeout(renderTodosForReview, 0);
При создании объектов на основе классов нужно использовать оператор new
. А при создании объектов с помощью фабричных функций new
не требуется. Однако если использование new
улучшит читаемость кода, данный оператор можно использовать и с фабричными функциями, вреда от этого не будет.
var todoModel= new TodoModel();
При использовании [5] new
с фабричной функцией функция просто вернёт созданный ей объект.
Предположим, что приложение использует объект User
для работы с механизмами авторизации. Я создал пару таких объектов, используя оба описываемых здесь подхода.
Вот описание [6] объекта User
с использованием класса:
class User {
constructor(){
this.authorized = false;
}
isAuthorized(){
return this.authorized;
}
}
const user = new User();
Вот как выглядит тот же объект, описанный [7] средствами фабричной функции:
function User() {
var authorized = false;
function isAuthorized(){
return authorized;
}
return Object.freeze({
isAuthorized
});
}
const user = User();
Объекты, создаваемые с использованием ключевого слова class
, уязвимы к атакам в том случае, если у злоумышленника имеется ссылка на объект. Так как все свойства всех объектов общедоступны, атакующий может использовать другие объекты для получения доступа к тому объекту, в котором он заинтересован.
Например, получить соответствующие права можно прямо из консоли разработчика, если переменная user
является глобальной. Для того чтобы в этом убедиться, откройте код примера [8] и модифицируйте переменную user
из консоли.
Этот пример подготовлен с помощью ресурса Plunker [9]. Для того, чтобы получить доступ к глобальным переменным, измените контекст в закладке консоли с top
на plunkerPreviewTarget(run.plnkr.co/)
.
user.authorized = true; //доступ к закрытому свойству
user.isAuthorized = function() { return true; } //переопределение API
console.log(user.isAuthorized()); //true
Модификация объекта с помощью консоли разработчика
Объект, созданный с помощью фабричной функции, нельзя изменить извне.
Классы поддерживают и наследование, и композицию объектов.
Я создал пример [10] наследования, в котором класс SpecialService
является наследником класса Service
.
class Service {
log(){}
}
class SpecialService extends Service {
logSomething(){ console.log("logSomething"); }
}
var specialService = new SpecialService();
specialService.log();
specialService.logSomething();
При использовании фабричных функций наследование не поддерживается, тут можно пользоваться лишь композицией. Как вариант, можно использовать команду Object.assign()
для копирования всех свойств из существующих объектов. Например [11], предположим, что нам надо повторно использовать все члены объекта Service
в объекте SpecialService
.
function Service() {
function log(){}
return Object.freeze({
log
});
}
function SpecialService(args){
var standardService = args.standardService;
function logSomething(){
console.log("logSomething");
}
return Object.freeze(Object.assign({}, standardService, {
logSomething
}));
}
var specialService = SpecialService({
standardService : Service()
});
specialService.log();
specialService.logSomething();
Фабричные функции содействуют использованию композиции вместо наследования, что даёт разработчику более высокий уровень гибкости в плане проектирования приложений.
При использовании классов тоже можно предпочесть композицию наследованию, на самом деле, это всего лишь архитектурные решения, касающиеся повторного использования существующего поведения.
Использование классов способствует экономии памяти, так как они реализованы на базе системы прототипов. Все методы создаются лишь один раз, в прототипе, ими пользуются все экземпляры класса.
Дополнительные затраты памяти, которая потребляется объектами, создаваемыми с помощью фабричных функций, заметны лишь при создании тысяч схожих объектов.
Вот страница [12], использованная для выяснения затрат памяти, характерных для использования фабричных функций. Вот результаты, полученные в Chrome для различного количества объектов с 10 и 20 методами.
Затраты памяти (в Chrome)
Прежде чем продолжать анализ затрат памяти, следует разграничить два вида объектов:
Объекты предоставляют поведение и скрывают данные.
Структуры данных предоставляют данные, но не обладают сколько-нибудь значительным поведением.
Роберт Мартин, «Чистый код [13]».
Взглянем на уже знакомый вам пример объекта TodoModel
для того, чтобы разъяснить разницу между объектами и структурами данных.
function TodoModel(){
var todos = [];
function add() { }
function reload(){ }
return Object.freeze({
add,
reload
});
}
Объект TodoModel
ответственен за хранение списка объектов todo
и за управление ими. TodoModel
— это ООП-объект, тот самый, который предоставляет поведение и скрывает данные. В приложении будет лишь один его экземпляр, поэтому при его создании с использованием фабричной функции дополнительных затрат памяти не потребуется.
Объекты, хранящиеся в массиве todos
— это структуры данных. В программе может быть множество таких объектов, но это — обычные JavaScript-объекты. Мы не заинтересованы в том, чтобы делать их методы закрытыми. Скорее мы стремимся к тому, чтобы все их свойства и методы были бы общедоступными. В результате все эти объекты будут построены с использованием прототипной системы, благодаря чему нам удастся сэкономить память. Их можно создавать с помощью обычного объектного литерала или командой Object.create()
.
В приложениях могут быть сотни или тысячи экземпляров компонентов пользовательского интерфейса. Это — та ситуация, в которой нужно найти компромисс между инкапсуляцией и экономией памяти.
Компоненты будут создаваться в соответствии с методами, принятыми в используемом фреймворке. Например, в Vue используются объектные литералы, в React — классы. Каждый член объекта-компонента будет общедоступным, но, благодаря использованию прототипной системы, применение таких объектов позволит экономить память.
В более широком смысле, классы и фабричные функции демонстрируют битву двух противоположных парадигм объектно-ориентированного программирования.
ООП, основанное на классах, в применении к JavaScript, означает следующее:
ООП без использования классов сводится к следующему:
instanceof
. Все объекты создают с помощью объектных литералов, некоторые из них — с общедоступными методами (ООП-объекты), некоторые — с общедоступными свойствами (структуры данных).Object.assign()
.
Сильная сторона классов заключается в том, что они хорошо знакомы программистам, пришедшим в JS из языков, разработка на которых основана на классах. Классы в JS представляют собой «синтаксический сахар» для прототипной системы. Однако, проблемы с безопасностью и использование this
, ведущее к постоянным ошибкам из-за потери контекста, ставят классы на второе место [14] в сравнении с фабричными функциями. В порядке исключения к классам прибегают в тех случаях, когда они применяются в используемом фреймворке, например — в React.
Фабричные функции — это не только инструмент для создания защищённых, инкапсулированных и гибких ООП-объектов. Этот подход к созданию классов, кроме того, открывает дорогу для новой, уникальной для JavaScript, парадигмы программирования.
Позволю себе в заключение этого материала процитировать Дугласа Крокфорда [15]: «Я думаю, что ООП без классов — это подарок человечеству от JavaScript».
Уважаемые читатели! Что и почему вам ближе: классы или фабричные функции?
Автор: ru_vds
Источник [16]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/javascript/276368
Ссылки в тексте:
[1] Image: https://habrahabr.ru/company/ruvds/blog/352198/
[2] описание: https://jsfiddle.net/cristi_salcescu/m9dhpzfx/
[3] описание: https://jsfiddle.net/cristi_salcescu/bcta6yyv/
[4] решить: https://jsfiddle.net/cristi_salcescu/y0k18og2/
[5] использовании: https://jsfiddle.net/cristi_salcescu/4j8wpfhx/
[6] описание: https://jsfiddle.net/cristi_salcescu/9s9t5qyr/
[7] описанный: https://jsfiddle.net/cristi_salcescu/2jbd2go8/
[8] примера: https://plnkr.co/edit/8LTmGKHuuKm5iroomDyz
[9] Plunker: https://plnkr.co/edit/8LTmGKHuuKm5iroomDyz?p=info
[10] пример: https://jsfiddle.net/cristi_salcescu/1xo96yt8/
[11] Например: https://jsfiddle.net/cristi_salcescu/jgbhkb4o/
[12] страница: https://plnkr.co/edit/4cxGfN?p=info
[13] Чистый код: https://www.amazon.com/Clean-Code-Handbook-Software-Craftsmanship/dp/0132350882
[14] второе место: https://www.youtube.com/watch?v=Tllw4EPhLiQ
[15] Дугласа Крокфорда: https://www.youtube.com/watch?v=DxnYQRuLX7Q&feature=youtu.be&t=45m41s
[16] Источник: https://habrahabr.ru/post/352198/?utm_campaign=352198
Нажмите здесь для печати.