iT邦幫忙

2019 iT 邦幫忙鐵人賽

DAY 11
0
Modern Web

JS Design Pattern 系列 第 11

JS Design Pattern Day11-輕量模式 Flyweight(下)

繼續上篇的問題,
1.一開始就new出兩個model物件,但在有些系統之中並不是一開始就需要有所有的共享物件。
2.給model物件手動設置了clothes外部狀態,在較複雜的系統中這不是一個最好的方式,因外部狀態可能會相當複雜,他們與共享物件的聯繫會變得困難。

針對兩個問題我們使用下面範例做解釋,這邊舉的例子是作者之前寫的檔案上傳功能,可以同時選擇2000個檔案,
每個檔案都會對應一個JS物件,所以在同時2000物件存在狀況下效能會相當低落。

簡化的例子當中,使用者可以選擇外掛程式和flash兩種上傳種類(uploadType),當使用者選完檔案之後,都會通知呼叫windows下的一個全域JS函數,名字叫做startUpload,使用者選擇的files被組合成一個陣列檔案塞進該函數的參數列表裡:

var id = 0;
window.startUpload = function(uploadType, files) {
	files.forEach(function(file, index) {
		var uploadObj = new Upload(uploadType, file.fileName, file.fileSize);
		uploadObj.init(id++); //給upload物件設置一個唯一的id
	});
};

var Upload = function(uploadType, fileName, fileSize) {
	this.uploadType = uploadType;
	this.fileName = fileName;
	this.fileSize = fileSize;
	this.$dom = null;
};

Upload.prototype.init = function(id) {
	var self = this;
	this.id = id;
	this.$dom = createUploadFileDiv(this.fileName, this.fileSize);

	function createUploadFileDiv(fileName, fileSize) {
		var $textSpan = $('<span>').text('檔案名稱:' + fileName + ',檔案大小:' + fileSize);
		var $delBtn = $('<button>').text('刪除').click(onDelete);
		return $('<div>').addClass('.delFile').append($textSpan).append($delBtn).appendTo('body');
	}

	function onDelete() {
		return self.delFile();
	}
};

/*為了簡化範例只保留刪除功能,其中一項業務邏輯是當檔案大小小於3000時會直接刪除檔案,否則回有提示視窗*/
Upload.prototype.delFile = function() {
	if (this.fileSize < 3000) {
		return this.$dom.remove();
	}
	if (window.confirm('確定要刪除嗎?')) {
		return this.$dom.remove();
	}
};


startUpload('plugin', [{
	fileName: 'A.txt',
	fileSize: 1000
}, {
	fileName: 'B.txt',
	fileSize: 3000
}, {
	fileName: 'C.txt',
	fileSize: 5000
}]);

startUpload('flash', [{
	fileName: 'D.txt',
	fileSize: 1000
}, {
	fileName: 'E.txt',
	fileSize: 3000
}, {
	fileName: 'F.txt',
	fileSize: 5000
}]);

上述這樣就會有多少上傳檔案就做多少upload物件出來,接下來我們要用輕量模式簡化它:
首先要先區分出內部狀態與外部狀態,實務上upload物件必須依賴uploadType,因為必須在前期就要知道uploadType而對應到不同動作(這部分範例沒有寫出來明確的code),另外我個人覺得有一種比較簡單的區分方法,就是用會有最少變動的屬性來當作內部狀態,因為輕量模式本來就是盡可能減少存在物件數,且讓這些物件共用的模式,以這例子來說,上傳種類、檔名跟檔案大小就只有上傳種類是較固定且變化小的屬性。

確定uploadType為內部狀態之後,我們就開始重構:
首先Upload函數只保留uploadType

var Upload = function(uploadType) {
	this.uploadType = uploadType;
};

我們在實作delFile的時候必須拿外部狀態(檔案大小)做判斷,upload物件本身是沒有這個狀態的所以我們之後會做一個uploadManager管理器裡面存放狀態,透過管理器裡的setExternalState方法獲得這些狀態,在這邊我們先寫上去之後做管理器的時候再來實作內部細節。

Upload.prototype.delFile = function(id) {
	uploadManager.setExternalState(id, this);
	if (this.fileSize < 3000) {
		return this.$dom.remove();
	}
	if (window.confirm('確定要刪除嗎?')) {
		return this.$dom.remove();
	}
};

接下來我們建立一個工廠來將物件實例化

var UpladFactory = (function() {
	var createFlyweightObj = {};
	return {
		create: function(uploadType) {
			if (createFlyweightObj[uploadType]) {
				return createFlyweightObj[createFlyweightObj];
			}
			return createFlyweightObj[createFlyweightObj] = new Upload(uploadType);
		}
	}
})();

我們要做一個管理器,它負責向工廠提交建立物件的請求,並且會用一個物件(uploadDatabase)存放upload物件的外部狀態,存放這個狀態的目的是因為在upload物件它一些運算過程中(例如delFile)需要用到這些外部狀態,但是物件本身是不存在這些狀態的所以要另外拿

var uploadManager = (function() {
	var uploadDatabase = {};
	return {
		add: function(id, uploadType, fileName, fileSize) {
			var flyweightObj = UpladFactory.create(uploadType);
			var $dom = createUploadFileDiv(fileName, fileSize);
			uploadDatabase[id] = {
				fileName: fileName,
				fileSize: fileSize,
				$dom: $dom
			};
			return flyweightObj;

			function createUploadFileDiv(fileName, fileSize) {
				var $textSpan = $('<span>').text('檔案名稱:' + fileName + ',檔案大小:' + fileSize);
				var $delBtn = $('<button>').text('刪除').click(onDelete);
				return $('<div>').addClass('.delFile').append($textSpan).append($delBtn).appendTo('body');
			}

			function onDelete() {
				return flyweightObj.delFile(id);
			}
		},
		setExternalState: function(id, externalObj) {
			var uploadData = uploadDatabase[id];
			for (var key in uploadData) {
				externalObj[key] = uploadData[key];
			}
		}
	}
})();

最後就是模擬觸發上傳的時候,內容框架跟舊版差不多,差異就在會用管理器加上工廠來實例化

var id = 0;
window.startUpload = function(uploadType, files) {
	files.forEach(function(file, index) {
		uploadManager.add(++id, uploadType, file.fileName, file.fileSize);
	});
};

我們來實際上傳一下,運行結果會跟舊版一樣

startUpload('plugin', [{
	fileName: 'A.txt',
	fileSize: 1000
}, {
	fileName: 'B.txt',
	fileSize: 3000
}, {
	fileName: 'C.txt',
	fileSize: 5000
}]);

startUpload('flash', [{
	fileName: 'D.txt',
	fileSize: 1000
}, {
	fileName: 'E.txt',
	fileSize: 3000
}, {
	fileName: 'F.txt',
	fileSize: 5000
}]);

現在傳再多檔案,最多就只會2種(依照你上傳的種類數量)。

雖然減少了物件數量,輕量模式會帶來一些複雜性的問題,就例子來說還需要多維護factory與manager物件,如果你的物件數量其實不需要使用到輕量模式的化這些開銷是可省下來的。

那麼何時可以使用呢?
1.系統中有大量相似物件。
2.由於大量物件導致的效能降低。
3.物件大多數狀態可以變成外部狀態。
4.剝離外部狀態之後,可以讓物件變成共享取代大量物件。

另外有一個叫做'物件池'概念實做起來會跟輕量模式有點像,我們來看一下怎麼做的:
在使用地圖的時候常常會出現標示地點的圖案,書上叫他小氣泡,所以我們也一起叫他小氣泡。當我們在地圖上出現兩個小氣泡分別標是兩個地點的時候,我們再稍微滑動地圖的化可能就會出現更多地標,這時候小氣泡就會增加,在這裡我們假設總共是6個小氣泡。從2個變成6個的時候並不會把2個刪掉再產生6個,而是會先會收2個進物件池,之後就只需要再產生4個就好。

那我們可以來實作一下:

var toolTipFactory = (function() {
	var toolTipPool = [];
	return {
		create: function() {
			if (toolTipPool.length === 0) {
				return createDiv();
			} else {
				return toolTipPool.shift();
			}
		},
		recover: function($element) {
			return toolTipPool.push($element);
		}
	}
})();

function createDiv() {
	return $('<div>').appendTo('body');
}


//模擬一下產生2個氣泡的樣子,ary用來紀錄已生成的的小氣泡
var ary = [];
for (var i = 0, str; str = ['A', 'B'][i++];) {
	var $toolTip = toolTipFactory.create();
	$toolTip.text(str);
	ary.push($toolTip);
}

//回收
ary.forEach(function($element) {
	$element.attr('old', 'isOld'); //第一次回收的時候偷偷動手腳,可以與之後長出來的做區別
	toolTipFactory.recover($element);
});

//再產生6個

for (var i = 0, str; str = ['A', 'B', 'C', 'D', 'E', 'F'][i++];) {
	var $toolTip = toolTipFactory.create();
	$toolTip.text(str);
	ary.push($toolTip);
}

這樣就是一個物件池的概念。
最後再次重構,我們把建立物件的過程封裝起來,可以做一個通用的物件池工廠

var objectPoolFactory = function(createObjFn) {
	var objPool = [];
	return {
		create: function() {
			var obj = objPool.length === 0 ?
				createObjFn.apply(this, arguments) : objPool.shift();
			return obj;
		},
		recover: function(obj) {
			objPool.push(obj);
		}
	};
};
//現在利用objectPoolFactory建立一個
var iframeFactory = objectPoolFactory(function() {
	var $iframe = createIframe();
	$iframe.load(function() {
		$iframe.unbind('load'); //防止iframe被重複載入
		iframeFactory.recover($iframe); //iframe載入完成之後回收節點
	});
	return $iframe;

	function createIframe() {
		return $('<iframe>').appendTo('body');
	}
});
var $iframe1 = iframeFactory.create();
$iframe1.attr('src', 'https://www.google.com.tw/');
var $iframe2 = iframeFactory.create();
$iframe2.attr('src', 'https://tw.yahoo.com/');

setTimeout(function() {
	var $iframe3 = iframeFactory.create();
	$iframe3.attr('src', 'https://ithelp.ithome.com.tw/');
}, 3000);

物件池是另一種效能最佳化的方式,根輕量模式有點像都是共用物件的道理。差別在於輕量模式會分離內外部屬性而物件池不會。

ok 以上就是輕量模式


上一篇
JS Design Pattern Day10-輕量模式 Flyweight(上)
下一篇
JS Design Pattern Day12-職責鏈模式 Chain of Responsibility(上)
系列文
JS Design Pattern 30

尚未有邦友留言

立即登入留言