封裝是為了實作在分析層面分離關注點的行為,目的是為了達到資訊隐藏。一、封裝是分離關注點的一種實作方式
對于一個複雜的系統來說,經過抽象之後,可以清楚的看到一個層次結構。對于每一層的抽象,都是由多個子產品組成的,這些子產品之間互相通信以便于為了上一層的抽象提供服務。一般來說,我們對于研究的每一層抽象,都是先在這個問題空間中找到與某個子產品相關的所有特性開始的,對于如何将這些特性展現出是這個子產品的?就是通過封裝來實作。
比如如下需求:
某學校的圖書館要引進一批新書,這些新書有索引編号、書名和作者,這些書是存放在圖書館中的。圖書館館的功能就是存放這些書,并且能夠找到一本書被讀者去借。
很顯然,對于這麼一個簡單的圖書管理系統來說,這個系統的層析結構并不是特别複雜,一個圖書管理系統由圖書館、圖書這幾個子產品/類組成。從中抽象出來的子產品結構如下:
圖書:對于每一本圖書都有索引編号(isbn)、名字(name)、作者名(author)組成,其中索引編号要滿足一定的格式格式必須為12位的數字為唯一辨別不能為空,名字和作者名由于存在某些原因可以為空,不存在唯一性。圖書給圖書館這個子產品提供一些資訊:比如對于任意一本圖書可以知道它的索引編号、名字、作者名或者同時顯示。
圖書館:可以存放很多本書,可以存放書查找書和被借書。
class Book class library
------------------------- -------------------------
isbn;必須有值 books;預設為[]
name; 預設值為null
author;預設值為null
------------------------ ------------------------
checkIsbn(); putBook();
getIsbn(); hasBook();
getName(); borrowBook();
getAuthor();
setIsbn();
setName();
setAuthor();
display();
------------------------- -------------------------
這樣就把圖書和圖書館這兩個子產品從紛雜的需求描述中分離出來了。對于如何去設計實作它呢接着往下看。二、封裝的目的是資訊隐藏
封裝的目的是為了資訊隐藏。如何去做到資訊隐藏呢?就是對于通過分析得到的特性中那些屬性私有化,然後對于方法來說有一部分方法隻有内部這個子產品所使用的,那也私有化,隻有那些與處于同一抽象層的另一個子產品互動的方法公有化,這些方法一般會通過接口的方式進行表達。
資訊隐藏通過屬性私有化部分方法私有化公共方法接口化的實作方式,其目的就是實作子產品的高度獨立性和靈活性,并且不依賴于其他子產品,進而實作較低的耦合度。進而使得對于單元測試等檢測方式變得容易,提高了編寫代碼的效率。在接下來分别介紹如何實作的獨立性靈活性和解耦。
2.1屬性私有化
一個子產品的屬性是這個子產品/類的核心,因為就是這個子產品中的方法基于這些屬性與其他的子產品進行互動。
比如說,一個類中的屬性就相當于雞蛋中的蛋黃,蛋黃承載着孵化小雞的關鍵任務是非常重要的,方法相當于蛋白,蛋白的作用就是為了蛋黃能孵化成小雞提供充足的營養,如果一個雞蛋的蛋黃流出那麼這個雞蛋肯定是不能孵化小雞的。一個類的屬性就和蛋黃一樣如此的重要。其實任何一個軟體系統都是對資料進行操作的。
如果屬性公有的話,可能有一個子產品對這個屬性進行了修改,但是其他的子產品并不知道,還去使用這個屬性,那麼必然會導緻錯誤。對于其他子產品隻能增加對這個屬性值判斷的代碼,無疑增加了代碼的複雜度和重複程度。
是以說屬性私有化是保證資料完整性的一個很好的方式。對于這個擁有私有屬性的子產品來說,增加特權方法去操作這些屬性,確定屬性不會處于無效的狀态,同時也友善了其他子產品的使用。減少錯誤的産生。
所謂的資料完整性就是指資料在指派的時候要進行檢測是否符合指派要求,在取值的時候不允許對他修改,是以對于每一個屬性(isbn、name、author)都有自己的特權方法(getIsbn、putIsbn……)
是以對于上述圖書和圖書館的描述将變成如下形式:
class Book class library
------------------------- -------------------------
private isbn;必須有值 private books;預設為[]
private name; 預設值為null
private author;預設值為null
------------------------ ------------------------
checkIsbn(); putBook();
getIsbn(); hasBook();
getName(); borrowBook();
getAuthor();
setIsbn();
setName();
setAuthor();
display();
------------------------- -------------------------
屬性私有化保證了一個類的資料的完整性,進而使用這個屬性的其他類們可以放心的去使用沒有必要去添加一些檢測這些屬性的代碼,進而使得當這些屬性完整性條件發生改變的時候可以僅僅改變自己這個類其他類不用修改提高了類的獨立性降低了耦合度。
2.2部分方法私有化
一個子產品的所有行為無外乎兩種,一種就是為其他的子產品提供服務,一種就是為自身服務。
我們可以将那些為自身服務的方法私有化,避免了外部子產品對這個子產品的幹擾,減少了外部子產品對該子產品的依賴,進而降低耦合度。
class Book class library
------------------------- -------------------------
private isbn;必須有值 private books;預設為[]
private name; 預設值為null
private author;預設值為null
------------------------ ------------------------
private checkIsbn(); putBook();
getIsbn(); hasBook();
getName(); borrowBook();
getAuthor();
setIsbn();
setName();
setAuthor();
display();
------------------------- -------------------------
2.3公共方法接口化
其他的方法為接口的形式,因為這些方法要和其他子產品打交道,是以就要提供一種契約後或者說一種标準。當然也可以不選擇接口的形式,僅僅是将這個方法公有就好了,但是問題是我這個子產品可能會對這個共有的方法進行修改,特别是方法名和參數,如果使用這些公共方法的子產品沒有修改勢必産生沒有找到的錯誤,是以說為了統一标準,要使用接口的形式,當然接口僅僅是一種設計理念實作方式完全可以是最簡單的注釋的形式。
是以,對于方法來說,一個子產品僅僅是對外公布那些接口的方法,如果一個子產品改變了實作方式或者算法,隻要實作了接口,其他的子產品是感覺不到這個改變的。
class Book
-------------------------------
private isbn;必須有值
private name; 預設值為null
private author;預設值為null
-------------------------------
private checkIsbn();
public getIsbn();
public getName();
public getAuthor();
public setIsbn();
public setName();
public setAuthor();
public display();
-------------------------------
interface publicActionOfBook
-------------------------------
getIsbn();
getName();
getAuthor();
setIsbn();
setName();
setAuthor();
display();
------------------------------
class library
-------------------------
private books;預設為null
-------------------------
private putBook();
public hasBook();
public borrowBook();
-------------------------
方法私有化和公共方法接口化可以使得類的獨立性更高并且更加靈活,比如當修改類中某些方法的算法的時候或者甚至在滿足接口的情況下改變資料結構都是可以得,因為外部的類僅僅是通過接口和這個類打交道,是以實作兩個類或者多個類之間的解耦。
三、javascript中實作封裝的兩種方式
3.1使用命名規範來實作封裝
// 對于接口來說有幾種實作方式我們在這裡采用注釋的形式 // 當然可以使用interface類的方式定義一個接口正如上一篇部落格定義的接口方法 /* interface publicActionOfBook{ getIsbn(); getName(); getAuthor(); setIsbn(); setName(); setAuthor(); display(); } */ function Book(){ this._isbn; this._name; this._author; } Book.prototype._checkIsbn = function(){ //在這裡檢測Book對象的isbn屬性的值是否滿足一定的條件 //也就是說是否是隻有12個數字 } Book.prototype.setIsbn= function(isbn){ if(this._checkIsbn(isbn)){/*必須有唯一的值*/ this._isbn = isbn; }else{ //抛出異常 } } Book.prototype.getIsbn= function(){ return this._isbn; } Book.prototype.setName = function(name){ this._name = name || "this book no have name";/*如果沒有值則為預設值*/ }; Book.prototype.getName = function(){return this._name;}; ...... Book.prototype.display = function(){return this._isbn+this._name+this._author;}; function Library(){ this._books = []; } Library.prototype.putBook = function (book){ //必須确認book實作了接口publicActionOfBook this._books.push(book.display()); } Library.prototype.hasBook = function(){}; ....
通過這種方式封裝一個類,對于私有的屬性和方法在屬性名前用下劃線辨別,除非程式員遵循這一标準非常自覺,否則也是會污染屬性的完整性,也就是說對于私有化并沒有完全的實作私有。
這個方式如下好處(一)沒有使用javascript閉包作用域這些特征,是的運作起來比較高效,記憶體占用良好。(二)對于通過繼承而來的子類來說,對于私有的方法也是可以內建的,碎玉接下來介紹的這種方法并沒有實作這種功能。
3.2使用js一些文法特性來實作封裝
var Book = (function(){ // 類屬性和類方法 //這是一個常量類屬性,這個屬性限制建立Book對象的個數 var BOOKNUM = 100; var Book = function(newIsbn,newName,newAuthor){ var isbn,name,author; function checkIsbn = function(){ //在這裡檢測Book對象的isbn屬性的值是否滿足一定的條件 //也就是說是否是隻有12個數字 }; Book.prototype.setIsbn= function(isbn){ if(checkIsbn(isbn)){/*必須有唯一的值*/ isbn = isbn; }else{ //抛出異常 } }; Book.prototype.getIsbn= function(){ return isbn; }; Book.prototype.setName = function(name){ name = name || "this book no have name";/*如果沒有值則為預設值*/ }; Book.prototype.getName = function(){return this._name;}; ...... Book.prototype.display = function(){return isbn+name+author;}; // 初始化 if(!checkBookNum()){ BOOKNUM--; this.setIsbn(newIsbn); this.setName(newName); this.setAuthor(newAuthor); } } //類方法,檢測是否建立了多餘100個book類 function checkBookNum(){ if(BOOKNUM<=0){ throw new Error("最多建立100個Book對象"); } return true; } return Book; })(); var Library = (function(){ var Library = function (){ var books = []; Library.prototype.putBook = function (book){ //必須确認book實作了接口publicActionOfBook books.push(book.display()); } Library.prototype.hasBook = function(){}; .... } return Library; })(); var b1 = new Book("12345678912","javascript","BOOCH"); var l1 = new Library(); l1.putBook(b1);
這種方式實作了真正意義上的類的封裝,實作了屬性和某些方法的私有化,同時某些公共方法的接口化,濕的即使改變book内部的資料結構形式,隻要滿足相應的接口格式的話,那麼Library是不會去修改的。實作了Book類的獨立性和子產品化,減少了Book和Library的依賴,以至于實作解耦。
但是這種實作方式存在一個緻命的問題就是性能,因為這種獨立性的實作是依托于javascript閉包和作用域鍊這種文法格式的,這種建立閉包的形式本身是很消耗性能和記憶體的。
一般是使用第一種方式通過程式設計人員的自覺性實作執行個體屬性和方法的私有化,通過第二種方式實作類屬性和類方法的私有化,或者實作常量的形式。
四、最終補充說明的事情
4.1實作某些功能性的函數比如toString、valueOf等。
對于一般的對象在進行加減法或者比較運算的時候,會将操作數進行轉化,可能轉化成字元串類型或者是數字類型。以便于能夠濟甯相應的操作。
一般情況下,對于操作對象是對象類型,如果這個位置需要一個數字的話,那麼就先調用valueOf如果這個結果通過轉化成數字後還不是一個數字類型的話,那麼在調用toString最終傳回這個值,如果這個位置需要一個字元串的話,則先調用toString在調用valueof,如果不告訴什麼類型比如+,那麼會預設按照數字的形式進行處理。
是以說主動聲明tostring和valueof方法很重要。
4.2方法借用
因為使用接口過來規範類的接口,是以會存在相同類似的類或者是不同類型的但是實作了這個接口的類中對這個接口定義的類的實作可能會和我将要定義的類的該接口實作相同那麼我就可以去借用了。
如下是最終實作版本:
// 對于接口來說有幾種實作方式我們在這裡采用注釋的形式 // 當然可以使用interface類的方式定義一個接口正如上一篇部落格定義的接口方法 /* interface publicActionOfBook{ getIsbn(); getName(); getAuthor(); setIsbn(); setName(); setAuthor(); display(); } */ var Book = (function(){ //這是一個常量類屬性,這個屬性限制建立Book對象的個數 var BOOKNUM = 100; //類方法,檢測是否建立了多餘100個book類 function checkBookNum(){ if(BOOKNUM<=0){ throw new Error("最多建立100個Book對象"); } return true; } function Book(newIsbn,newName,newAuthor){ //實作初始化 if(checkBookNum()){ this._isbn; this._name; this._author; this.setIsbn(newIsbn); this.setName(newName); this.setAuthor(newAuthor); } }; Book.prototype._checkIsbn = function(){ //在這裡檢測Book對象的isbn屬性的值是否滿足一定的條件 //也就是說是否是隻有12個數字 }; Book.prototype.setIsbn= function(isbn){ if(this._checkIsbn(isbn)){/*必須有唯一的值*/ this._isbn = isbn; }else{ //抛出異常 } }; Book.prototype.getIsbn= function(){ return this._isbn||; }; Book.prototype.setName = function(name){ this._name = name || "this book no have name";/*如果沒有值則為預設值*/ }; Book.prototype.getName = function(){ return this._name; }; ...... Book.prototype.display = function(){ return this._isbn+this._name+this._author; }; Book.prototype.display.toString = Book.prototype.display; Book.prototype.display.valueof = function(){ //方法借用 return Object.prototype.valueof.call(); } return Book; })(); var Library = (function(){ var Library = function(){ this._books = []; } Library.prototype.putBook = function (book){ //必須确認book實作了接口publicActionOfBook 一定要保證接口的實作,切記,否則可能耦合度變高 this._books.push(book.display()); } Library.prototype.hasBook = function(){}; Library.prototype.toString = function(){ return Array.prototype.join.call(this._books,","); }; Library.prototype.valueOf = Library.prototype.toString; .... return Library; })();