Pull to refresh

Паттерн «Обозреватель» и контекст вызова в Javascript

Reading time3 min
Views13K
Хотя про паттерн «Обозреватель (Наблюдатель, Observer)» сказано достаточно, в том числе и на Хабре, вкратце повторюсь. Суть паттерна в наблюдении за состоянием неких субъектов системы и соответствующей реакции наблюдателей на изменения этих состояний. За одним субъектом может следить несколько наблюдателей, причём сам он об этом не знает (слабое связывание), но исправно оповещает всех об изменении состояния.

Удобно использовать Обозревателя на сайтах и в веб-приложениях, поэтому логично реализовать его с использованием самого популярного языка для веб-среды — Javascript.

Observable = function() {
	this.observers = [];	
}

Observable.prototype.deliver = function(data) {
	for (var i in this.observers) {
		this.observers[i](data);
	}
}

Function.prototype.subscribe = function(observable) {
	observable.observers.push(this);
	return this;
}

Всё просто. Класс Observable и есть субъект. Массив observers — подписчики, наблюдатели. Метод deliver оповещает наблюдателей об изменении состояния. Subscribe же подписывает наблюдателя. На практике всё выглядит так:

myClass = function() {
	this.value = 0; //некое значение
	this.onChange = new Observable(); //наблюдаемое состояние
}

myClass.prototype.change = function(new_value) {
	this.value = new_value;
	this.onChange.deliver(this.value); //изменилось значение — сообщили наблюдателям
}

var c = new myClass(); 

var write_log = function(value) { 
	console.log(value);
}

write_log.subscribe(c.onChange);

Замечательно работающий пример: при каждом изменении c.value наблюдаемый onChange оповестит всех наблюдателей об изменении и сообщит им новое состояние, с которым наблюдатели распорядятся по-своему. Так, например, write_log() напечатает новое состояние в консоли. Но пример этот замечателен только до тех пор, пока не возникнет необходимости оперировать this'ом в исполняемой после рассылки функции.

Так, например, следующая конструкция работать как надо не будет:

myClass = function() {
	this.value = 0;
	this.onChange = new Observable();
}

myClass.prototype.change = function(new_value) {
	this.value = new_value;
	this.onChange.deliver(this.value);
}

Logger = function(logtype) {
	this.type = (!!logtype) ? logtype : "alert";
}

Logger.prototype.write = function(value) {
	if (this.type == "console") { console.log(value); return; }
	alert(value);
}

var c = new myClass();
var logger = new Logger("console");

logger.write.subscribe(c.onChange);

Проблема возникнет в обращении к this при исполнении logger.write. Напомню, что this — это контекст, а в данном случае контекстом будет не экземпляр класса Logger, а безымянная функция function(), вызванная при исполнении deliver.

Решить проблему помогает замечательная функция call, которая не только исполняет некий метод, но и позволяет указать контекст исполнения. Поэтому я немного переписал метод наблюдаемого — deliver и, соответственно, изменил механизм подписок.

Observable = function() { //без изменений
	this.observers = [];	
}

Observable.prototype.deliver =function(data) {
	for (var i in this.observers) {
		this.observers[i].func.call(this.observers[i].context, data); //функция теперь вызывается в нужном контексте
	}
}

Function.prototype.subscribe = function(observable, context) {
	var ctx = context || this; //если контекст вызова не задан, то контекстом считается this «по-умолчанию», то есть текущая функция
	var observer = { //теперь наблюдатель будет сообщать, в каком контексте нужно вызвать функцию
		context: ctx,
		func: this
	}
	observable.observers.push(observer);
	return this;
}

Таким образом, чтобы заработал крайний пример, нужно всего лишь указать в подписке контекст вызова — экземпляр Logger'а:

var logger = new Logger("console");
logger.write.subscribe(c.onChange, logger);

It works! А я надеюсь, этот маленький фокус кому-то пригодится.
Tags:
Hubs:
Total votes 45: ↑37 and ↓8+29
Comments24

Articles