Статьи

Шаблоны проектирования JavaScript — Шаблон модуля раскрытия

Прошло несколько недель (хорошо, несколько месяцев) с момента моего последнего сообщения в блоге о шаблонах проектирования JavaScript. Я извиняюсь, но, честно говоря, вероятно, пройдет еще несколько недель, пока я снова не напишу в блоге на эту тему, так что, надеюсь, люди здесь не ожидают быстрой серии (ухмылка). Напомним, что идея этой серии заключается в создании реальных, практических примеров различных шаблонов проектирования JavaScript на основе книги «Изучение шаблонов проектирования JavaScript» Адди Османи. (См. Мой обзор здесь .) В этой записи блога я буду обсуждать шаблон модуля выявления.

Адди Османи описывает шаблон «Модуль раскрытия» как:


Шаблон «Модуль раскрытия» появился, когда [Кристиан] Хейлманн был разочарован тем, что ему пришлось повторять имя основного объекта, когда он хотел вызвать один открытый метод из другого или получить доступ к открытым переменным.
Он также не любил требование шаблона модуля о необходимости переключаться на буквальную запись объекта для вещей, которые он хотел обнародовать.

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

var revealModuleTest = (function() {
    
    function priv1() {
        return 1;   
    }
    
    return {
     
        doPriv:function() {
            return priv1();
        }, 
        pub1:function() {
            return 2;
        }, 
        testpub:function() {
            return pub1()*2;   
        }
        
    }
    
}());

//works just fine
console.log(revealModuleTest.doPriv());

//so will this
console.log(revealModuleTest.pub1());

//fail!
console.log(revealModuleTest.testpub());

У меня есть простой модуль, который называется showModuleTest. (И да, это не «пример из реальной жизни», но я хотел сначала продемонстрировать проблему простым блоком.) Мой модуль имеет один закрытый метод и три открытых метода. Первый, doPriv, просто оборачивает вызов приватного метода. Второй публичный метод, pub1, просто возвращает число 2. Наконец, testpub просто использует pub1 и умножает его на два.

Обратите внимание на три теста внизу. Как следует из комментариев, последний метод завершится ошибкой: Uncaught ReferenceError: pub1 не определен

Зачем? Когда результат модуля (все в этом возвращении) возвращается обратно к вызывающей стороне, область видоизменения изменяется таким образом, что сам по себе pub1 больше не адресуется из testpub. У вас не возникнет этой проблемы, если закрытый метод вызовет другой, но вы обязательно столкнетесь с этими методами, возвращенными вашим модулем. (Я не думаю, что я проделал большую работу, объясняя это — я могу вернуться и конкретизировать это.)

Исправление довольно тривиально — просто используйте тот же API, который вы использовали бы в своем коде, вызывающем модуль:

var revealModuleTest = (function() {
    
    function priv1() {
        return 1;   
    }
    
    return {
     
        doPriv:function() {
            return priv1();
        }, 
        pub1:function() {
            return 2;
        }, 
        testpub:function() {
            return revealModuleTest.pub1()*2;   
        }
        
    }
    
}());

//works just fine
console.log(revealModuleTest.doPriv());

//so will this
console.log(revealModuleTest.pub1());

//works now!
console.log(revealModuleTest.testpub());

Второе, о чем говорит Хайльманн, — это переключение на использование обозначений объектов для определения методов. Все в блоке возврата использует обозначение объекта. Лично это не кажется большой проблемой, но, учитывая все обстоятельства, я бы предпочел написать больше кода в синтаксисе функции x () по сравнению с x: function ().

Если это звучит так, будто модуль «Раскрытие» — это просто косметическое изменение, я думаю, это было бы справедливо. Но не просто отмахивайтесь от этого. Все, что делает вас более быстрым, эффективным и т. Д. В вашем развитии, вероятно, хорошо. Я также утверждаю, что проблема с «публичным публичным вызовом» — это то, с чем вы можете случайно столкнуться. Если шаблон «Модуль раскрытия» позволяет избежать этого, то это еще одна причина для его рассмотрения.

Давайте посмотрим на пример этого — снова — используя мой довольно глупый пример.

var revealModuleTest = (function() {
    
    function priv1() {
        return 1;   
    }
    
    function pub1() {
        return 2;
    }
    
    function testpub() {
        return pub1() * 2;
    }
    
    return {
        doPriv:priv1,
        pub1:pub1,
        testpub:testpub
    }
    
}());

//works just fine
console.log(revealModuleTest.doPriv());

//so will this
console.log(revealModuleTest.pub1());

//not a fail!
console.log(revealModuleTest.testpub());

Как видите, это не радикально измененная версия. Он имеет такой же запах, но просто структурирован немного по-другому. Обратите внимание, что вся настоящая логика находится в частных функциях. Блок возврата теперь намного проще — он просто предоставляет публичный API.

Хорошо, а как насчет реального примера? В своем предыдущем посте в блоге я создал дневниковое приложение, которое позволяло вам создавать и читать дневниковые записи, хранящиеся в WebSQL. Я определил свой дневник как модуль. Вы можете просмотреть этот источник здесь .

Чтобы изменить это в соответствии с шаблоном Revealing Module, я переместил все в приватные методы и создал гораздо более простой блок возврата.

var diaryModule = (function() {
  
	var db;
	
	function initDB(t) {
		t.executeSql('create table if not exists diary(id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT, body TEXT, image TEXT, published DATE)');
	}
	
	function dbErrorHandler(e) {
		console.log('DB Error');
		console.dir(e);
	}
	
	//Utility to convert record sets into array of obs
	function fixResults(res) {
		var result = [];
		for(var i=0, len=res.rows.length; i<len; i++) {
			var row = res.rows.item(i);
			result.push(row);
		}
		return result;
	}
	
	//I'm a lot like fixResults, but I'm only used in the context of expecting one row, so I return an ob, not an array
	function fixResult(res) {
		if(res.rows.length) {
			return res.rows.item(0);
		} else return {};
	}

    function setup(callback) {
        db = window.openDatabase("diary", 1, "diary", 1000000);
        db.transaction(initDB, dbErrorHandler, callback);        
    }
    
    function getEntries(start,callback) {
        console.log('Running getEntries');
        if(arguments.length === 1) callback = arguments[0];
    
        db.transaction(
            function(t) {
                t.executeSql('select id, title, body, image, published from diary order by published desc',[],
                    function(t,results) {
                        callback(fixResults(results));
                    },dbErrorHandler);
            }, dbErrorHandler);
    
    }
    
    function getEntry(id, callback) {
        db.transaction(
            function(t) {
                t.executeSql('select id, title, body, image, published from diary where id = ?', [id],
                    function(t, results) {
                        callback(fixResult(results));
                    }, dbErrorHandler);
                }, dbErrorHandler);
    
    }
    
    function saveEntry(data, callback) {
        db.transaction(
            function(t) {
                t.executeSql('insert into diary(title,body,published) values(?,?,?)', [data.title, data.body, new Date().getTime()],
                function() { 
                    callback();
                }, dbErrorHandler);
            }, dbErrorHandler);
    }
                
	return {
        setup:setup,
        getEntries:getEntries,
        getEntry:getEntry,
        saveEntry:saveEntry
	}
	
}());

Так что ты думаешь? Я должен быть честным. Когда я впервые прочитал об этом паттерне, я действительно не задумывался об этом. Как я уже сказал, это казалось просто косметическим. Но после просмотра изменений в моем дневнике, он просто … чувствует себя лучше.

Я не воспринимал это как «живую» демонстрацию, поскольку функциональность такая же, как и раньше. (И как напоминание, он работает только в браузерах WebKit, что плохо, и будет рассмотрено позже.) Если вы действительно хотите попробовать это, вы можете загрузить код из последней записи и вставить новый дневник. Поскольку он имеет точно такой же API, все будет просто работать!