你和我都是熱衷於學習的工程師,我們經常閱讀各種書籍或文章,常常因為一本書寫的實在太好,於是又買了其中提到的其他書籍,我們深深的感受到知識帶來的精神上飽足感。
我們又同時熱愛分享,常常在自己的 Blog 上撰寫讀書心得,在社群網站上分享文章,在生活中又經常與好友推薦與分享自己所閱讀到的好書
而同時我們又是熱愛分享的工程師,我們經常撰寫 Blog,遊走於各種社交平台,到處分享所見所得,還常常舉辦 workshop 活動,我們不只是在獲取知識上得到滿足,我們還在分享的過程中得到了成就感。
由於我們在 Stone City 中的活躍,似乎工程圈的人都認識了我們,所以當 Stone City 市長打算建造一間城市中最大的圖書館時,也找到了我們。
市長打算打造一個城市中最大間的圖書館,取名叫 Stone Library,他希望這間圖書館在初期能夠有最基本的兩種書籍查詢功能,按照書名、分類來查找。
當然,這樣的目標對我們兩個來說還是蠻容易達成的,在接下這個挑戰之後,我們身上產生了一股熱流,連續通宵了七天之後就寫好了。
基本的書籍查詢功能
首先,我們建立一個 StoneLibrary.swift 並且定義了一本書最基本的兩個Property 分別是 name 以及 category,並且規定資料來源必須要實現 books 來獲取所有的書籍,以及通過 name 和 category 來進行 search 的方法。
StoneLibrary.swift
1 2 3 4 5 6 7 8 9 10 11 |
struct StoneBook { var name:String var category:String } protocol StoneLibraryDataSource { var books:[StoneBook] { get } func searchByName(name:String) -> [StoneBook] func searchByCategory(category:String) -> [StoneBook] } |
接著我們建立 StoneLibraryDB,所有的書籍放在 books 中,並且提供兩個方法用(searchByName, searchByCategory)來查詢書籍,你和我都很清楚,因為這兩個search方法的實現方式是一樣的,所以我們將其中的方法獨立出來,放在search 方法中。
接著,我們整理好了 ComputerScience 和 Fruit 兩類書籍,並且規定他們要遵循StoneLibraryDB 協議。
StoneLibraryDB.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
class StoneLibraryDB:StoneLibraryDataSource { var books = [StoneBook]() func searchByName(name:String) -> [StoneBook] { return search({ book -> Bool in return book.name.range(of: name) != nil }) } func searchByCategory(category:String) -> [StoneBook] { return search({ book -> Bool in return book.category.range(of: category) != nil }) } private func search(_ selector:((StoneBook) -> Bool)) -> [StoneBook] { var results = [StoneBook]() for book in books { if selector(book) { results.append(book) } } return results } } class ComputerScienceDB:StoneLibraryDB { override init() { super.init() books.append(StoneBook(name: "Code Complete", category: "computer science")) books.append(StoneBook(name: "Algorithm", category: "computer science")) } } class FruitDB:StoneLibraryDB { override init() { super.init() books.append(StoneBook(name: "apple", category: "fruit")) books.append(StoneBook(name: "orange", category: "fruit")) } } |
提供一個對外的查詢工具,工具中規範了兩種 SearchType,並且可以在初始化時加入書籍資料 StoneLibraryDB,可以通過books來獲取所有的書籍,通過 search方法來查找書籍。
StoneLibrarySearchTool.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
class StoneLibrarySearchTool { enum SearchType { case name case category } private var sources = [StoneLibraryDataSource]() var books:[StoneBook] { var results = [StoneBook]() for source in sources { results += source.books } return results } init(sources:[StoneLibraryDataSource]) { self.sources = sources } func search(text:String, type:SearchType) -> [StoneBook] { var results = [StoneBook]() for source in sources { results += type == .name ? source.searchByName(name: text) : source.searchByCategory(category: text) } return results } } |
在我們完成後拿去給 Stone City 的市長做測試:
我們調用了測試的 code:
1 2 3 4 5 6 7 8 9 10 11 |
let searchTool = StoneLibrarySearchTool(sources: [ComputerScienceDB(), FruitDB()]) print("== list ==") for book in searchTool.books { print(book.name) } print("== search ==") for book in searchTool.search(text: "computer science", type: .category) { print(book.name) } |
看到下面的運行結果,看來是成功了,市長表示非常高興,他已經看到了升遷的希望:D
1 2 3 4 5 6 7 8 9 10 |
== list == Code Complete Algorithm apple orange == search == Code Complete Algorithm Program ended with exit code: 0 |
不過 Stone City 的市長是非常有野心的,他想要打造一個圖書館平台,希望市民能夠通過這一個平台來搜尋所有圖書館的書籍,任何人想要借書都可以通過這個平台,當市民借閱一本書籍時,這本書就會搬運到距離這個市民最近的圖書館,聽起來超方便的對不對?!
然而每一間圖書館的系統都是不一樣的,要把他們都融入自己的系統中真的是一件不容易的事情,而且有一些舊系統,在還沒有全盤了解之前,都不知道是不是可以改動 source code….
整合第一間 Old Library
OldLibrary 是我們需要整合的第一間圖書館,他的歷史非常悠久,至今為止已經服務過幾代人了,剛和這間圖書館的 Devloper 聯繫,他們串了一段 Source Code過來。
並且在 email 中表示,目前他們只剩下一位負責維護的 Developer,如果需要整合,目前來看是沒有時間去修改 Code 的。
看到下面的 source code,目前來說,我們光是繼續維護開發 Stone Library 的系統就已經忙得不可開交,這下子該怎麼整合呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
class OldLibraryBook { fileprivate var name:String fileprivate var kind:String init(name:String, kind:String) { self.name = name self.kind = kind } func getName() -> String { return name } func getKind() -> String { return kind } } class OldLibraryDirectory { fileprivate var items:[String: OldLibraryBook] init() { items = ["gone girl": OldLibraryBook(name:"gone girl", kind:"novel"), "the martian" : OldLibraryBook(name: "the martian", kind: "novel") ] } func getItems() -> [String: OldLibraryBook] { return items } } |
適配器模式(Adapter Pattern)
當現有的系統需要繼承一個具有類似功能的新組件,而該組件沒有提供通用接口,並且無法修改改組件時,這時候就可以用上適配器了。
- 優點:將無法修改 source code 的組件集成到現有的項目中。在使用第三方框架或者利用另一個項目所輸出的數據時,通常會遇到組件之間的兼容問題。
- 何時應該避免使用:如果可以修改所要集成的組件的 SourceCode 或者可以直接將該組件提供的 Data 直接遷移到現有的應用中的話。(比如我們其實可以直接將 OldLibrary 中的資料 copy 到 StoneLibrary 中)
從長遠來看,數據遷移是個不錯的方式,但有時卻很難快速的完成,但現實生活中常常碰到的是找不到原有的開發工程師配合,又或者沒有足夠的人力投入,所以有時候直接使用適配器模式可以取得一些短期的效益。
適配器模式就很像上面這個轉接頭,我們通過製作一個轉接頭,就可以在不改造設備內部結構的情況下直接使用該設備了。
實現 OldLibraryAdapter
創建適配器的幾個步驟:
- 創建一個 OldLibraryAdapter,並且讓它遵從 LibraryDataSource 協議
- Adapter 向 OldLibrary 發送請求,OldLibrary 處理請求內容,將結果返回給Adapter
- Adapter 將 OldLibrary 返回的結果,轉換成 StoneLibrary 所需要的內容
我們創建了 OldLibraryAdapter.swift 並且讓他遵循 StoneLibrarydataSource 協議,其中我們在實現 books 的獲取,searchByName 以及 SearchByCategory 的過程,實際上是調用 OldLibrary 的一些方法,在獲得需要的資料以後將 Data 轉換成 StoneLibrary 所使用的格式(Book)在返回內容。
OldLibraryAdapter.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
class OldLibraryAdapter:StoneLibraryDataSource { fileprivate let directory:OldLibraryDirectory init() { directory = OldLibraryDirectory() } var books:[StoneBook] { return directory.getItems().values.map { item -> StoneBook in return StoneBook(name: item.getName(), category: item.getKind()) } } func searchByName(name:String) -> [StoneBook] { return createBooks(filter: {(oldBook:OldLibraryBook) -> Bool in return oldBook.getName().range(of: name) != nil }) } func searchByCategory(category:String) -> [StoneBook] { return createBooks(filter: {(oldBook:OldLibraryBook) -> Bool in return oldBook.getKind().range(of: category) != nil }) } // 查找出資料後,轉換成Book類型 func createBooks(filter filterClosure:((OldLibraryBook) -> Bool)) -> [StoneBook] { var results = [StoneBook]() for item in directory.getItems().values { if filterClosure(item) { let aBook = StoneBook(name: item.getName(), category: item.getKind()) results.append(aBook) } } return results } } |
最後的測試
這時候我們提供給 Stone City 的市長測試時,他可以不用了解 Old Library 的實現情況,而直接使用我們的 OldLibraryAdapter 就可以了!
1 2 3 4 5 6 7 8 9 10 11 |
let searchTool = StoneLibrarySearchTool(sources: [ComputerScienceDB(), FruitDB(), OldLibraryAdapter()]) print("== list ==") for book in searchTool.books { print(book.name) } print("== search ==") for book in searchTool.search(text: "computer science", type: .category) { print(book.name) } |
輸出結果
1 2 3 4 5 6 7 8 9 10 11 |
== list == Code Complete Algorithm apple orange gone girl the martian == search == Code Complete Algorithm Program ended with exit code: 0 |
市長放煙火啦~
最後,在欣賞完煙火以後,可以通過傳送門到 Github 上看看適配器模式的例子。