封装
上一篇文章距離有出不多一個多月了,現在好不容易有了好心情,繼續看書,寫點感悟.
第三章講述的是js封裝,像java,可以通過private關鍵字來聲明一個方法使得只有該對象內部的代碼才能執行它,在js中沒有這樣的關鍵字,但是可以使用閉包來創建只允許從對象內部訪問的方法和屬性.相比于private,閉包走了一條彎路卻達到同樣的效果.
接口在js封裝中的作用
上一篇文章講到接口模式是許多其他js設計模式的基礎,它定義了兩個對象間的關系,接口不變那么關系的雙方可以被替換,不一定非得使用像第二章那樣嚴格的接口,而且應該避免公開定義于接口中的方法,否則其他對象可能會對那些并不屬于接口的方法產生依賴,不安全,因為這些方法隨時都可能改變或者被刪除.一個理想軟件系統應該未所有類定義接口,這些類只向外界提供他們實現的接口中規定的方法,任何別的方法都留作自用.其所有屬性都是私有的,外界只能通過接口中定義的存取方法進行操作.
創建對象的基本模式
有3種,門戶大開型只能提供公用成員;第二種,使用下劃線來表示方法或者屬性的私有性;第三種,通過閉包來創建真正私有的成員,這些成員只能通過特權方法訪問.以Book 類為例 -- 一個用來存儲關于一本書的數據的類,并為其實現一個以 HTML 形式顯示這些數據的方法.現在只需創建這個類, 下面是其他人在創建并使用實例:
//Book(isbn, title, author) var jsDesignPatterns = new Book('978-7-115-19128-1', 'JavaScript 設計模式', 'Harmes, R.'); jsDesignPatterns.display(); //Outputs the data by creatingand populating an HTML element.門戶大開型對象
實現 Book 類最簡單的做法是按傳統方式創建一個類,用一個函數來做其構造器.他的所有屬性和方法都公開可訪問,這些公用屬性需要使用 this 關鍵字來創建:
var Book = function(isbn, title, author) {if (isbn == undefined) {throw new Error('Book constructor requires an isbn.');this.isbn = isbn;this.title = title || 'No title specified';this.author = author || 'No author specified';}Book.prototype.display = function () {...}; }在構造器中,isbn 必選, 因為 display 方法要求 book 對象都有一個準確的 isbn,否則就不能找到相應的圖片,也不能生成一個用于購書的鏈接.title 和 author 參數都是可選的,要有默認值以防它們未被提供.
有個最大的問題,沒有辦法檢查 isbn 數據的完整性,錯誤的 isbn 數據可能導致 display 方法失效.下面的版本強化了對 isbn 的檢查:
checkIsbn 方法可以保證 ISBN 是一個具有正確位數和校驗和的字符串.因為Book 類現在有兩個方法,所以 Book.prototype 被設置為一個對象字面量,這樣在定義多個方法的時候就不用在每個方法前面都加上Book.prototype.
現在保證了 display 方法可以正常工作,出現了另一個問題,假設一本書可能會有多個版本,每個版本都有自己的 ISBN,在實例化book對象之后直接修改 isbn 屬性:
所以即使在構造器中對數據完整性進行檢驗,還是無法阻止其他人給 isbn賦值,為了保護內部數據,為每個屬性提供 accessor 取值器和 mutator 賦值器方法.通過使用賦值器,你可以在把一個新值真正賦給屬性之前進行各種檢驗.下面是加入了取值器合賦值器之后的新版 Book 對象:
var Publication = new Interface ('Publication', ['getIsbn', 'getTitle', 'setTitle', 'getAuthor', 'setAuthor', 'display']);var Book = function (isbn, title, author) { // implements Publicationthis.setIsbn(isbn);this.setTitle(title);this.setAuthor(author); }Book.prototype = {checkIsbn: function (isbn) {...},getIsbn: function () {return this.isbn;},setIsbn: function (isbn) {if (!this.checkIsbn(isbn)) {throw new Error('Book: Invalid ISBN.');}this.isbn = isbn;},getTitle: function () {return this.title;},setTitle: function (title) {this.title = title || 'No title specified';},getAuthor: function () {return this.author;},setAuthor: function (author) {this.author = author || 'No author specified';},diplay: function () {...} };上述代碼定義了一個接口,只需要使用這個接口中定義的方法與對象來打交道.還有一些對數據有保護作用的取值器,賦值器方法,以及一些檢驗方法.
但是依然有缺陷:提供了賦值器方法,但那些屬性仍然是公開的,可以被直接設置.而且無法保護內部數據,取值器和賦值器方法也引入了較多代碼(js 文件大小較為重要).
用命名規范區別私用成員
依然是上面那個 snippet,只不過 setAttributes 的時候所有要設置的屬性和方法都加上了 下劃線_ 前綴,表示它是私用屬性和方法(js 中可以使用下劃線和字母開頭命名出有效變量).
下劃線的這種用法表明一個屬性或者方法僅供對象內部使用,直接訪問它或者設置它可能會導致意想不到的后果,這有助于防止程序員對它的無意使用,卻不能防止有意使用使用.
這個規范只有在得到遵守時才有效果,并不是真正可以用來隱藏對象內部數據的解決方案,主要適用于非敏感性的內部方法和屬性.
作用域,嵌套函數和閉包
在討論這種真正的私用性方法和屬性的實現技術之前,我們先鞏固一下相關基礎.在 js 中,只有函數具有作用域,在一個函數內部聲明的變量在函數外部無法訪問,私用屬性也是希望無法在對象外部訪問的變量,所以作用域相關性明顯.定義在一個函數中的變量在該函數中的內嵌函數是可以訪問的.
function foo() {var a = 10;function bar() {a *= 2;}bar();return a; }a定義在函數foo中,但函數 bar 可以訪問它,因為 bar 也定義在 foo 中,bar 內部對 a 賦新值,當 bar 在 foo 中被調用時它能夠訪問 a,這可以理解.如果 bar 在 foo 外部被調用呢
function foo() {var a = 10;function bar () {a *= 2;return a;}return bar; } var baz = foo(); // baz is now a reference to functionbar. baz(); // return 20; baz(); // return 40; baz(); // return 80;var blat = foo(); // blat is another reference to bar. blat(); // return 20, because a new copy of a is being used.在上述代碼中, 所返回的對 bar 函數的引用被賦給變量 baz.這個函數現在是在 foo 外部被調用,但他依然能夠訪問 a.這是因為 js 中的作用域是詞法性的,函數是運行在定義他們的作用域中(本例中是 foo 內部的作用域),而不是運行在調用他們的作用域中,只要 bar 被定義在 foo 中,他就能訪問在 foo 中定義的所有變量, 即時 foo 的執行已經結束.
在 foo 返回后,它的作用域被保存下來,但是只有他返回的那個函數能夠訪問這個作用域.baz和 blat 各有這個作用域及 a 的一個副本,而且只有他們自己能對其進行修改.返回一個內嵌函數是創建閉包最常用的手段.
用閉包實現私用成員
現在有了對閉包的理解之后再來討論一下我們剛剛要做的事: 需要創建一個只能在對象內部訪問的變量.借助于閉包你可以創建只允許特定函數訪問的變量,而且這些變量在這些函數的各次調用之間依然存在.為了創建私用屬性,你需要在構造器函數的作用域中定義相關變量,這些變量可以被定義于該作用域中的所有函數訪問,包括哪些特權方法:
var Book = function(newIsbn, newTitle, newAuthor){ // implements Punlication// Private attribute.var isbn, title, author;// Private method.function checkIsbn(isbn) {...}// Privilleged methods.this.getIsbn = function (){return isbn;};this.setIsbn =function (newIsbn) {if (!checkIsbn(newIsbn) {throw new Error('Book: Invalid ISBN.');}isbn =newIsbn;};this.getTitle = function () {return title;};this.setTitle = function (newTitle) {title = newTitle || 'No title specified';};this.getAuthor = function () {return author;};this.setAuthor = function (newAuthor) {author = newAuthor || 'No author specified';};// Constructor code.this.setIsbn(newIsbn);this.setTitle(newTitle);this.setAuthor(newAuthor); };// Public, non-privileged methods. Book.prototype = {display: function () {...} };以上代碼和之前的創建對象模式有什么不同呢,其他情況下我們在創建和引用對象的屬性時總要使用 this 關鍵字.但是這個地方我們用 var 聲明這些屬性變量,它們只存在于Book 構造器中,checkIsbn 函數是私有方法.
需要訪問這些變量和函數的方法只需聲明在 Book 中,被稱作特權方法(privileged method),他們是公有方法,之所以有 特權方法前面都用 this 關鍵詞來幫助聲明, 是為了在對象外部能夠被訪問.這些方法定義于 Book 構造器的作用域中,所以它們能夠訪問到私有屬性,引用這些屬性時并沒有使用 this關鍵詞,因為他們沒有公開.所有取值器和賦值器方法都被改為不加 this 的直接引用這些屬性.
任何不需要直接訪問私用屬性的方法都可以像原來那樣在 Book.prototype中聲明.display 就是這類方法中的一個,他可以通過調用 getIsbn或者 getTitle等等特權方法來間接訪問任何私用屬性.只有那些需要直接訪問私用成員的方法才是特權方法.需要注意的是,特權方法太多會占用較多內存.每個對象實例都包含了所有特權方法的新副本.
用閉包方式創建的對象可以具有真正的私有屬性,其他程序員不可能直接訪問它們創建的Book 實例的任何內部數據.
但是,,,這樣使用閉包還是有缺點的.之前的門戶大開型對象創建模式中,所有方法都創建在原型對象中,因此不管生成多少對象實例,這些方法在內存中只存在一份.而在本節中每生成一個新的對象實例都將 copy 每一個私有方法和特權方法,,,這樣就會帶來更多內存消耗,所以只適合用在真正私有成員的場合.這種對象創建模式也不利于派生子類,因為所派生出的子類不能訪問超類的任何私有屬性或者方法.
所以在 js 中用閉包實現私有成員導致的派生問題被稱作**"繼承破壞封裝(inheritance breaks encapsulation)",如果你創建的類以后可能會需要派生出子類, 那么最好還是采用前兩種對象創建模式.
高級的對象創建模式
靜態方法和屬性
剛才講的作用域和閉包可用于創建靜態成員(公有和私有),大多數方法和屬性所關聯的是類的實例,但是靜態成員所關聯的是類本身.也就是說,靜態成員是在類的層次上操作,不是實例層次上.每個靜態成員都只有一份,是通過類對象方法的.
下面是添加了靜態屬性和方法和 Book 類:
與前一節創建的類大題相似,但是有重要區別.這里的私有成員和特權方法仍然聲明在構造器中(分別使用 var 和 this 關鍵字),但是那個構造器卻從原來的普通函數辦成了一個內嵌函數,并且被作為包含它的函數的返回值賦給變量 Book.這里創建了一個閉包,里面聲明了靜態的私有成員.位于外層函數聲明之后的一對空括號很重要 -- 代碼一載入就立即執行這個函數(而不是在調用 Book 構造函數時),這個函數的返回值是另一個函數,被賦值給 Book 變量 -- 一個構造函數,在實例化 Book 時,所調用的是這個內層函數,外層函數只是用于創建一個可以用來存放靜態私有成員的閉包.
私有的靜態成員可以從構造器內部訪問,這意味著所有私有函數和特權函數都能訪問它們.與其他方法相比,他們在內存中只會存放一份.
因為它們在構造器之外,所以不是特權方法,不能訪問任何定義在構造器中的私有屬性.定義在構造器中的私有方法能夠調用那些私有靜態方法.
要判斷一個私有方法是否應該被設計成靜態方法,主要看它是否需要訪問任何實例數據,如果不需要那么設計成靜態方法更省內存.
常量
在 js 中,可以通過創建只有取值器而沒有賦值器的私有變量來模仿常量,而且不因對象實例的不同而變化,所以將其作為私有靜態屬性來設計是合乎情理的.
假設 Class 對象有一個 UPPER_BOUND的常量,那么為了獲取這個常量而進行的方法調用
Class.getUPPER_BOUND();
為了實現這個取值器,需要使用特權靜態方法:
如果需要許多常量,可以創建一個通用的取值器方法,這樣就不必為每個常量都創建取值器方法:
var Class = (function () {// Private static attributes.var constants = {UPPER_BOUND: 100,LOWER_BOUND: -100};// Constructor.var ctor = function(constructorArgument) {...};// Privileged static method.ctor.getConstant = function(name) {return constants[name];}...// Return the constructor.return ctor;})();單體和對象工廠
這兩個模式就是使用閉包來創建受保護的變量空間.后面會慢慢涉及,在此先簡要介紹一下,單體模式使用一個外層函數返回的對象字面量來公開特權成員,而私用成員則被保護性地封裝在外層函數的作用域中,主要原理是:外層函數在定義后立即執行,其結果被賦給一個變量.前面的例子中外層函數返回的都是一個函數,而單體模式中外層函數返回的是一個對象字面量.
對象工廠可以使用閉包來創建具有私有成員的對象.最簡形式是一個類構造器.
封裝的好處
保護了內部數據的完整性,通過講數據的訪問途徑限制為取值器和賦值器這兩個方法,可以獲得對取值和賦值的完全控制.這樣可以減少其他函數所需要的錯誤檢查代碼數量,并且確保數據不會處于無效狀態.另外,對象的重構可以變得更輕松.因為用戶不知道對象的內部細節,所以可以隨心所欲的修改對象內部使用的數據結構和算法.
通過只公開那些在接口中規定的方法,可以弱化模塊間的耦合.盡可能的提高對象的獨立性可以帶來許多好處:提高對象的可重用性,使其在必要的時候可以被替換.使用私有變量可以避免空間沖突,如果一個變量在代碼中其他地方都不能被訪問,就不用擔心它是否與程序中其他地方的對象或者函數重名,大幅改動對象的內部細節也不會影響其他代碼.
壞處
封裝也存在一定的缺憾.比如,私有方法很難進行單元測試.因為他們還有其內部變量都是私有的,在對象外部沒法訪問.
要么通過使用公有方法來提供訪問途徑(這樣就市區許多私有方法的好處),要么設法在對象內部定義并執行所有單元測試.最好的解決辦法是只對公有辦法進行單測.可以覆蓋到所有私有方法,但是卻是間接的.這種問題不是 js 特有的,只對公有方法進行單測較易接受作用域鏈復雜的話可能會使錯誤調試更加困難,有時候很難區分來自不同作用域的許多同名變量.此問題不是經過封裝的對象所特有的,但是實現私有方法和屬性所有的閉包會讓它變得更加復雜.
過度封裝可能會損害類的靈活性,不利于和小伙伴之間的合作,他可能對你的類的需求了解的并不透徹.
-最大的問題在于 js 實現封裝較為困難.不利于新手使用.js 與大多數面向對象語言不同,封裝涉及的調用鏈和定義后立即執行的匿名函數等概念加大了學習難度.
小結
本文討論了信息隱藏的概念以及如何使用封裝這種手段來實現它,js 沒有對封裝提供內置的支持,所以需要依賴其他東西.
如果可以確信其他小伙伴只會使用接口中規定的方法,或者并非迫切需要保持內部數據的完整性,那,那么可以使用門戶大開型對象.
命名規范用來告知小伙伴哪些方法是不宜直接方法的內部方法.如果需要真正的私有成員,那么只能使用閉包.通過創建一個受保護的變量空間,可以實現公有,私有和特權成員,靜態成員,常量.理解 js 作用域的特點,可以模仿出各種面向對象技術.
----------劇終----------
總結
- 上一篇: uestc 1073 秋实大哥与线段树
- 下一篇: 服务器双网卡绑定