作者簡介 joey 螞蟻金服·資料體驗技術團隊
繼前文Typescript玩轉設計模式 之 結構型模式(上)之後,本週帶來的是系列文章之三,講解的是3種結構性模式:
- 外觀
- 享元
- 代理
Facade(外觀)
定義
為子系統中的一組介面提供一個一致的介面,Facade模式定義一個高層介面,這個介面使得這個子系統更加容易使用。
結構
外觀模式包含以下角色:
- Facade(外觀角色):在客戶端可以呼叫它的方法,在外觀角色中可以知道相關的(一個或者多個)子系統的功能和責任;在正常情況下,它將所有從客戶端發來的請求委派到相應的子系統去,傳遞給相應的子系統物件處理。
- SubSystem(子系統角色):在軟體系統中可以有一個或者多個子系統角色,每一個子系統可以不是一個單獨的類,而是一個類的集合,它實現子系統的功能;每一個子系統都可以被客戶端直接呼叫,或者被外觀角色呼叫,它處理由外觀類傳過來的請求;子系統並不知道外觀的存在,對於子系統而言,外觀角色僅僅是另外一個客戶端而已。
示例
案例:領導提出要實現一個產品功能,但又不想了解其中的細節。
// 主意
class Idea {};
// 需求
class Requirement {};
// 開發包
class Development {};
// 釋出包
class Release {};
// 產品經理
class PD {
analyze(idea: Idea) {
console.log('PD 開始需求');
return new Requirement();
}
}
// 開發者
class Developer {
develop(requirement: Requirement) {
console.log('程式設計師開始開發');
return new Development();
}
}
// 測試者
class Tester {
test(develop: Development) {
return new Release();
}
}
// 外觀方法,領導不需要關注具體的開發流程,只要說出自己的想法即可
// 而不用外觀方法的話,也可以訪問到子系統,只是需要了解其中的細節
function addNewFunction(idea: Idea) {
const pd = new PD();
const developer = new Developer();
const tester = new Tester();
const requirement = pd.analyze(idea);
const development = developer.develop(requirement);
const release = tester.test(development);
console.log('釋出');
}
// 領導
class Leader {
haveAGoodIdea() {
const idea = new Idea();
addNewFunction(idea);
}
}
function facadeDemo() {
const leader = new Leader();
leader.haveAGoodIdea();
}
facadeDemo();
複製程式碼
適用場景
- 當你要為一個複雜子系統提供一個簡單介面時。子系統往往由於不斷演化而變得越來越複雜。大多數模式使用時都會產生更多更小的類。這使得子系統更具可重用性,也更容易對子系統進行定製,但這也給那些不需要定製子系統的使用者帶來一些使用上的困難。外觀可以提供一個簡單的預設介面,這一介面對於大多數使用者來說已經足夠,而那些需要更多的可定製性的使用者可以越過外觀層。
- 客戶程式與抽象類的實現部分之間存在著很大的依賴性。外觀模式分離子系統,提高子系統的獨立性和可移植性。
- 當你需要構建一個層次結構的子系統時,使用外觀模式定義子系統的入口點。讓子系統間通過外觀進行通訊,簡化互相之間的依賴關係。
優點
- 對客戶程式遮蔽了子系統元件。通過引入外觀模式,客戶端程式碼將變得很簡單,與之關聯的物件也很少;
- 實現了子系統和客戶程式的鬆耦合關係;
- 一個子系統的修改對其他子系統沒有任何影響,而且子系統內部變化也不會影響到外觀物件;
缺點
- 不能很好地限制客戶端直接使用子系統類,如果對客戶端訪問子系統類做太多的限制則減少了可變性和靈活性;
- 如果設計不當,增加新的子系統可能需要修改外觀類的原始碼,違背了開閉原則;
相關模式
- 抽象工廠模式可以與外觀模式一起使用以提供一個介面,這一介面可用來以一種子系統獨立的方式建立子系統物件。抽象工廠也可以代替外觀模式隱藏哪些與平臺相關的類。
- 中介者模式與外觀模式的相似之處是,他抽象了一些已有的類的功能。然而,中介者的目的是對同事之間的任意通訊進行抽象,通常集中部署域任何單個物件的功能。中介者的同事物件知道中介者並與他通訊,而不是直接與其他同類物件通訊。相對而言,外觀模式僅對子系統物件的介面進行抽象,從而使他們更容易使用,他並不定義新功能,子系統也不知道外觀的存在。
- 外觀物件常常屬於單例模式。
Flyweight(享元)
定義
運用共享技術有效地支援大量細粒度的物件。
結構
享元模式包含以下角色:
- Flyweight(抽象享元類):通常是一個介面或抽象類,在抽象享元類中宣告瞭具體享元類公共的方法,這些方法可以向外界提供享元物件的內部資料(內部狀態),同時也可以通過這些方法來設定外部資料(外部狀態)。
- ConcreteFlyweight(具體享元類):它實現了抽象享元類,其例項稱為享元物件;在具體享元類中為內部狀態提供了儲存空間。通常我們可以結合單例模式來設計具體享元類,為每一個具體享元類提供唯一的享元物件。
- UnsharedConcreteFlyweight(非共享具體享元類):並不是所有的抽象享元類的子類都需要被共享,不能被共享的子類可設計為非共享具體享元類;當需要一個非共享具體享元類的物件時可以直接通過例項化建立。
- FlyweightFactory(享元工廠類):享元工廠類用於建立並管理享元物件,它針對抽象享元類程式設計,將各種型別的具體享元物件儲存在一個享元池中,享元池一般設計為一個儲存“鍵值對”的集合(也可以是其他型別的集合),可以結合工廠模式進行設計;當使用者請求一個具體享元物件時,享元工廠提供一個儲存在享元池中已建立的例項或者建立一個新的例項(如果不存在的話),返回新建立的例項並將其儲存在享元池中。
示例
// 書籍類,書的基本資訊和借閱資訊都是屬性
// 但同一本書可以被多次借出,對借閱記錄來說,同一本書的多次借閱記錄裡儲存的書的資訊是冗餘的
class OriginBookRecord {
// 書的基本資訊
ISBN: string;
title: string;
// 借閱資訊
id: string;
time: string;
constructor(ISBN: string, title: string, id: string, time: string) {
this.ISBN = ISBN;
this.title = title;
this.id = id;
this.time = time;
}
checkout(time: string) {
this.time = time;
}
}
// 書籍管理者
class OriginBookRecordManager {
books: Map<string, OriginBookRecord>;
add(ISBN: string, id: string, title: string, time: string) {
const book = new OriginBookRecord(ISBN, title, id, time);
this.books.set(id, book);
}
checkout(id: string, time: string): void {
const book = this.books.get(id);
if (book) {
book.checkout(time);
}
}
}
// 享元模式,分離內部狀態和外部狀態,將能共享的部分分離出來
// 本案例中,書的基本資訊和借閱資訊分離開來,同一本書可以有多條借閱記錄
class LibraryBook {
ISBN: string;
title: string;
constructor(ISBN: string, title: string) {
this.ISBN = ISBN;
this.title = title;
}
}
// 享元工廠
class LibraryBookFactory {
books: Map<string, LibraryBook>;
createBook(ISBN: string, title: string): LibraryBook {
let book = this.books.get(ISBN);
if (!book) {
book = new LibraryBook(ISBN, title);
this.books.set(ISBN, book);
}
return book;
}
}
// 將享元工廠實現為單例
const libraryBookFactory = new LibraryBookFactory();
// 借閱記錄,此時記錄物件不需要儲存書的屬性,只需要儲存一個書的引用,減少了儲存空間
class BookRecord {
book: LibraryBook;
id: string;
time: string;
constructor(id: string, book: LibraryBook, time: string) {
this.book = book;
this.time = time;
this.id = id;
}
checkout(time: string) {
this.time = time;
}
}
class BookRecordManager {
bookRecords: Map<string, BookRecord>;
add(id: string, ISBN: string, title: string, time: string): void {
const book = libraryBookFactory.createBook(ISBN, title);
const bookRecord = new BookRecord(id, book, time);
this.bookRecords.set(id, bookRecord);
}
checkout(id: string, time: string) {
const bookRecord = this.bookRecords.get(id);
if (bookRecord) {
bookRecord.checkout(time);
}
}
}
複製程式碼
適用場景
使用享元模式需要符合以下條件:
- 一個應用需要使用大量物件;
- 完全由於使用大量的物件,造成很大的儲存開銷;
- 物件的大多數狀態都可變為外部狀態;
- 如果刪除物件的外部狀態,那麼可以用相對較少的共享物件取代很多組物件;
優點
- 可以極大減少記憶體中物件的數量,使得相同或相似物件在記憶體中只儲存一份,從而可以節約系統資源,提高系統效能;
- 享元模式的外部狀態相對獨立,而且不會影響其內部狀態,從而使得享元物件可以在不同的環境中被共享;
缺點
- 享元模式使得系統變得複雜,需要分離出內部狀態和外部狀態,這使得程式的邏輯複雜化;
注意點
- 刪除外部狀態。該模式的可用性很大程度上取決於是否容易識別外部狀態並將它從共享物件中刪除。如果不同種類的外部狀態和共享前物件的書目相同的話,刪除外部狀態不會降低儲存消耗。
- 管理共享物件。因為物件是共享的,使用者不能直接對他進行例項化。需要有享元工廠幫助使用者查詢某個特定的享元物件。共享還意味著可以方便地進行引用計數和垃圾回收,當享元物件書目固定而且很小的時候,可以永久儲存。
相關模式
- 享元模式通常和組合模式結合,用共享葉節點的有向無環圖實現一個邏輯上的層次結構。
- 最好用享元實現狀態和策略物件。
Proxy(代理)
定義
為其他物件提供一種代理以控制對這個物件的訪問。
結構
代理模式包含以下角色:
- Subject(抽象主題角色):它宣告瞭真實主題和代理主題的共同介面,這樣一來在任何使用真實主題的地方都可以使用代理主題,客戶端通常需要針對抽象主題角色進行程式設計。
- Proxy(代理主題角色):它包含了對真實主題的引用,從而可以在任何時候操作真實主題物件;在代理主題角色中提供一個與真實主題角色相同的介面,以便在任何時候都可以替代真實主題;代理主題角色還可以控制對真實主題的使用,負責在需要的時候建立和刪除真實主題物件,並對真實主題物件的使用加以約束。通常,在代理主題角色中,客戶端在呼叫所引用的真實主題操作之前或之後還需要執行其他操作,而不僅僅是單純呼叫真實主題物件中的操作。
- RealSubject(真實主題角色):它定義了代理角色所代表的真實物件,在真實主題角色中實現了真實的業務操作,客戶端可以通過代理主題角色間接呼叫真實主題角色中定義的操作。
示例
遠端代理
為一個物件在不同的地址空間提供區域性代表,延遲獲取遠端物件。
class RemoteResource {
getContent(): string {
return '讀取遠端檔案內容';
}
}
class RemoteRecourceProxy {
getContent() {
const resource = this.request();
return resource.getContent();
}
request(): RemoteResource {
console.log('千辛萬苦從遠端拿到了檔案')
return new RemoteResource();
}
}
function remoteProxyDemo() {
const resource = new RemoteRecourceProxy();
const content = resource.getContent();
console.log(content);
}
remoteProxyDemo();
複製程式碼
虛代理
如果需要建立一個資源消耗較大的物件,先建立一個消耗相對較小的物件,真實物件只在需要時才會被真正建立。
// 大圖片,繪製會消耗較多資源
class BigImage {
private name: string;
constructor(name: string) {
this.name = name;
this.draw();
}
// 繪製
draw(): void {
console.log('繪製 ${this.name},需要消耗大量資源');
}
// 預覽
preview(): void {
console.log(`展示 ${this.name} 的預覽效果`);
}
getName(): string {
return this.name;
}
}
class VirutalBigImageProxy {
private image: BigImage;
private name: string;
// 虛代理先建立一個大圖片的代理,而不真正建立實際物件
constructor(name: string) {
this.name = name;
}
// 只有在要預覽時,才真正繪製影像
preview(): void {
if (!this.image) {
this.image = new BigImage(this.name);
}
this.image.preview();
}
getName(): string {
if (!this.image) {
console.log('返回虛代理裡儲存的名稱');
return this.name;
}
console.log('實際圖片已經被建立,返回實際圖片的名稱');
return this.image.getName();
}
}
function virutalProxyDemo() {
const image1 = new VirutalBigImageProxy('圖1');
const image2 = new VirutalBigImageProxy('圖2');
// 讀取圖1的名稱,此時不需要真正繪製大圖片,只需要返回虛代理裡儲存的資料即可,減小開銷
console.log(image1.getName());
// 只有在真正需要使用大圖片時,才建立大圖片物件
image2.preview();
}
virutalProxyDemo();
複製程式碼
保護代理
控制對原始物件的訪問,保護代理使用者物件應該有不同的訪問許可權的時候。
class SecretDoc {
read(): string {
return '機密檔案內容';
}
}
class ProtectionSecretDocProxy {
private name: string;
private doc: SecretDoc;
constructor(name: string) {
this.name = name;
this.doc = new SecretDoc();
}
// 提供相同的方法名,但是加了許可權控制的程式碼
read(): string {
if (this.name === '遠峰') {
const content = this.doc.read();
return content;
}
return '';
}
}
function protectionProxyDemo() {
const doc1 = new ProtectionSecretDocProxy('遠峰');
console.log(`遠峰讀出了: ${doc1.read()}`);
const doc2 = new ProtectionSecretDocProxy('其他人');
console.log(`其他人讀出了: ${doc2.read()}`);
}
protectionProxyDemo();
複製程式碼
智慧代理
在訪問物件時執行一些附加的操作。
class Resource {
content: string;
constructor(content: string) {
this.content = content;
}
read(): string {
return this.content;
}
write(content: string): Promise<null> {
return new Promise(resolve => {
setTimeout(() => {
this.content = content;
resolve();
}, 1000);
})
}
}
// 智慧代理,多了一個是否上鎖的屬性,以及相關對鎖的操作
class SmartResourceProxy {
lock: boolean;
resource: Resource;
constructor() {
this.resource = new Resource('檔案內容');
}
read(): string|Error {
if (this.lock) { return new Error('別人正在寫'); }
console.log('正在讀');
return this.resource.read();
}
write(content: string) {
console.log('正在寫')
this.lock = true;
this.resource.write(content)
.then(() => {
this.lock = false;
});
}
}
function smartProxyDemo() {
const resource = new SmartResourceProxy();
// 能讀到內容
console.log(resource.read());
resource.write('新的檔案內容');
// 由於別人正在寫,讀不到內容
try {
resource.read();
} catch (e) {
console.error(e);
}
}
smartProxyDemo();
複製程式碼
適用場景
- 遠端代理。為一個物件在不同的地址空間提供區域性代表。
- 虛代理。根據需要建立開銷很大的物件。
- 保護代理。控制對原始物件的訪問,保護代理用於物件應該有不同的訪問許可權的時候。
- 智慧指引。在訪問物件時執行一些附加操作,如:
1)對指向實際物件的引用計數,這樣當該物件沒有引用時,可以自動釋放他;
- 當第一次引用一個持久物件時,將它裝入記憶體;
- 在訪問一個實際物件前,檢查是否已經鎖定了他,以確保其他物件不能改變他;
優點
代理模式公有優點:
- 能夠協調呼叫者和被呼叫者,在一定程度上降低了系統的耦合度;
- 客戶端可以針對抽象主題角色進行程式設計,增加和更換代理類無須修改原始碼,符合開閉原則,系統具有較好的靈活性和可擴充套件性;
不同代理模式有各自的優點:
- 遠端代理為位於兩個不同地址空間物件的訪問提供了一種實現機制,可以將一些消耗資源較多的物件和操作移至效能更好的計算機上,提高系統的整體執行效率;
- 虛代理通過一個消耗資源較少的物件來代表一個消耗資源較多的物件,可以在一定程度上節省系統的執行開銷;
- 保護代理可以控制對一個物件的訪問許可權,為不同使用者提供不同級別的使用許可權;
缺點
- 實現代理模式需要額外的工作,而且有些代理模式的實現過程較為複雜,例如遠端代理;
相關模式
- 介面卡模式為他所適配的物件提供了一個不同的介面,相反,代理提供了與它的實體相同的介面。然而,用於訪問保護的代理可能會拒絕執行實體會執行的操作,因此,它的介面實際上可能只是實體介面的一個子集。
- 儘管裝飾器的實現部分與代理相似,但裝飾器的目的不一樣,裝飾器為物件新增一個或多個功能,而代理則控制對物件的訪問。保護代理實現可能與裝飾器差不多,遠端代理不包含對實體的直接引用,而只是一個間接引用,如“主機ID,主機上的區域性地址”。虛代理開始的時候使用一個間接引用,最終將獲取並使用一個直接引用。
參考文件
本文介紹了前4種結構型模式,對後續模式感興趣的同學可以關注專欄或者傳送簡歷至'tao.qit####alibaba-inc.com'.replace('####', '@'),歡迎有志之士加入~