此文除了是標題黨,沒有什麼其餘問題。前端
這篇文章推薦一個庫,https://github.com/jianfengye/collection。 這個庫是我在開發業務過程當中 Slice 的頻繁致使業務開發效率低,就產生了要作一個 Collection 包的想法。本文說說我開發這個庫的來龍去脈git
最近一個邏輯很是複雜的業務,我用 Golang 來開發。開發過程不斷在問一個問題,Golang 適不適合寫業務?github
業務說到底,是一大堆的邏輯,大量的邏輯都是在幾個環節:獲取數據,封裝數據,組織數據,過濾數據,排序結果。獲取/封裝數據,即從 DB 中根據查詢 SQL,獲取表中的數據,並封裝成數據結構。組織數據,例如,當我有兩份數據源,我須要將兩份數據源按照某個字段合併,那麼這種組織數據的能力也是很是須要的。過濾數據,我獲取的字段有10個,可是我只須要給前端返回3個就夠了;排序結果,返回的結構按照某種順序。這些都是咱們在寫業務中,每一個業務邏輯都會遇到的問題。一款適合作業務的語言必定是在這些環節上都提供足夠的便利性的。golang
我想,符合業務語義的語言纔有將來!!數組
什麼是業務語義呢?就是咱們開發人員和產品人員交流的語言。感覺一下,好比 「將這個名單中成績按照從大到小排列,而且成績大於60的最後一個學生找出來」 這麼一句話的需求,就是咱們經常和產品人員交流的語言。而咱們開發中使用到的語言/框架/庫,又是一種思惟和語言。當咱們接到上述的需求,若是咱們頭腦中浮現的邏輯是「我要使用快速排序,而後在快速排序循環中能直接找到成績大於60的,還要是最後一個,因此我可能須要有個 min 變量」。那麼我只能說,或許你的代碼運行效率足夠高,可是一旦業務複雜了,你的代碼開發效率必定很低。像上述的需求,咱們按照僞碼來講,最但願是有一門語言能支持:collection().sortDesc().Last(score > 60)
這樣符合業務語義的代碼。安全
如圖,若是說高級語言是拉近了機器語言和業務語義的距離,那麼開發Collection包的願景也是但願拉近 Golang 這門高級語言和 業務語言的距離。數據結構
Collection包目標是用於替換golang原生的Slice,使用場景是在大量不追求極致性能,追求業務開發效能的場景。框架
業務開發最核心的也就是對數組的處理,Collection封裝了多種數據數組類型。函數
Collection包目前支持的元素類型:int, int64, float32, float64, string, struct。除了struct數組使用了反射以外,其餘的數組並無使用反射機制,效率和易用性獲得必定的平衡。性能
使用下列幾個方法進行初始化Collection:
NewIntCollection(objs []int) *IntCollection NewInt64Collection(objs []int64) *Int64Collection NewFloat64Collection(objs []float64) *Float64Collection NewFloat32Collection(objs []float32) *Float32Collection NewStrCollection(objs []string) *StrCollection NewObjCollection(objs interface{}) *ObjCollection
全部的初始化函數都是很方便的將要初始化的slice傳遞進入,返回了一個實現了ICollection的具體對象。
下面作一些Collection中函數的展現。
首先業務是很須要進行代碼調試的,這裏封裝了一個 DD 方法,能按照友好的格式展現這個 Collection
a1 := Foo{A: "a1"} a2 := Foo{A: "a2"} objColl := NewObjCollection([]Foo{a1, a2}) objColl.DD() /* ObjCollection(2)(collection.Foo):{ 0: {A:a1} 1: {A:a2} } */ intColl := NewIntCollection([]int{1,2}) intColl.DD() /* IntCollection(2):{ 0: 1 1: 2 } */
在一個數組中查找對應的元素,這個是很是常見的功能
Search(item interface{}) int
查找Collection中第一個匹配查詢元素的下標,若是存在,返回下標;若是不存在,返回-1
注意 此函數要求設置compare方法,基礎元素數組(int, int64, float32, float64, string)可直接調用!
intColl := NewIntCollection([]int{1,2}) if intColl.Search(2) != 1 { t.Error("Search 錯誤") } intColl = NewIntCollection([]int{1,2, 3, 3, 2}) if intColl.Search(3) != 2 { t.Error("Search 重複錯誤") }
將Collection中重複的元素進行合併,返回惟一的一個數組。
intColl := NewIntCollection([]int{1,2, 3, 3, 2}) uniqColl := intColl.Unique() if uniqColl.Count() != 3 { t.Error("Unique 重複錯誤") } uniqColl.DD() /* IntCollection(3):{ 0: 1 1: 2 2: 3 } */
獲取該Collection中知足過濾的最後一個元素,若是沒有填寫過濾條件,默認返回最後一個元素
intColl := NewIntCollection([]int{1, 2, 3, 4, 3, 2}) last, err := intColl.Last().ToInt() if err != nil { t.Error("last get error") } if last != 2 { t.Error("last 獲取錯誤") } last, err = intColl.Last(func(item interface{}, key int) bool { i := item.(int) return i > 2 }).ToInt() if err != nil { t.Error("last get error") } if last != 3 { t.Error("last 獲取錯誤") }
Map(func(item interface{}, key int) interface{}) ICollection
對Collection中的每一個函數都進行一次函數調用,並將返回值組裝成ICollection
這個回調函數形如: func(item interface{}, key int) interface{}
若是但願在某此調用的時候停止,就在這次調用的時候設置Collection的Error,就能夠停止,且這次回調函數生成的結構不合併到最終生成的ICollection。
intColl := NewIntCollection([]int{1, 2, 3, 4}) newIntColl := intColl.Map(func(item interface{}, key int) interface{} { v := item.(int) return v * 2 }) newIntColl.DD() if newIntColl.Count() != 4 { t.Error("Map錯誤") } newIntColl2 := intColl.Map(func(item interface{}, key int) interface{} { v := item.(int) if key > 2 { intColl.SetErr(errors.New("break")) return nil } return v * 2 }) newIntColl2.DD() /* IntCollection(4):{ 0: 2 1: 4 2: 6 3: 8 } IntCollection(3):{ 0: 2 1: 4 2: 6 } */
Reduce(func(carry IMix, item IMix) IMix) IMix
對Collection中的全部元素進行聚合計算。
若是但願在某次調用的時候停止,在這次調用的時候設置Collection的Error,就能夠停止調用。
intColl := NewIntCollection([]int{1, 2, 3, 4}) sumMix := intColl.Reduce(func(carry IMix, item IMix) IMix { carryInt, _ := carry.ToInt() itemInt, _ := item.ToInt() return NewMix(carryInt + itemInt) }) sumMix.DD() sum, err := sumMix.ToInt() if err != nil { t.Error(err.Error()) } if sum != 10 { t.Error("Reduce計算錯誤") } /* IMix(int): 10 */
將Collection中的元素進行升序排列輸出
intColl := NewIntCollection([]int{2, 4, 3}) intColl2 := intColl.Sort() if intColl2.Err() != nil { t.Error(intColl2.Err()) } intColl2.DD() /* IntCollection(3):{ 0: 2 1: 3 2: 4 } */
Join(split string, format ...func(item interface{}) string) string
將Collection中的元素按照某種方式聚合成字符串。該函數接受一個或者兩個參數,第一個參數是聚合字符串的分隔符號,第二個參數是聚合時候每一個元素的格式化函數,若是沒有設置第二個參數,則使用fmt.Sprintf("%v")來該格式化
intColl := NewIntCollection([]int{2, 4, 3}) out := intColl.Join(",") if out != "2,4,3" { t.Error("join錯誤") } out = intColl.Join(",", func(item interface{}) string { return fmt.Sprintf("'%d'", item.(int)) }) if out != "'2','4','3'" { t.Error("join 錯誤") }
Collection 包的核心思想也就是繼承。可是在 Golang 中的繼承,特別是抽象類是沒有辦法實現的。我這裏使用了實現了自身接口的屬性Parent來實現的。
首先定義 ICollection 接口,在這個接口中定義好全部的方法。其次建立了 AbsCollection 這個 struct。首先它自身實現了 ICollection 方法,其次,它有個 Parent 屬性實現了 ICollection方法,這個 Parent 屬性是存放指向真正的實現類的方法,好比 IntCollection。最後,IntCollection/Float32Collection 等都是實現了 AbsCollection。這裏顯式寫實現了 AbsCollection 有幾個好處,一個是強制必須實現 ICollection的方法,其次,一些在具體實現類中不同的方法,能夠在實現類中重寫了。而且最後,爲每一個實現類實現了一個New方法。
固然,因爲是強類型語言,不少函數在定義的時候,返回值是沒法肯定類型的,固然這裏能夠簡單的使用一個interface來作,可是這樣易用性其實又下降了,每次函數調用就必須坐下類型判斷。再加上後續回說到的 error 處理的問題。因此我設計了一個 IMix 接口,由實現了這個接口的對象來進行類型轉換,ToString, ToInt64 等。固然我也爲 IMix 設計了 DD() 方便調試的方法。
上面說了繼承,AbsCollection 是我定位的抽象類,它的思想是一輩子二,二生萬物的思想。就是有一些原子方法(好比Insert方法)是根據不一樣的數組對象而不一樣的。這些方法在AbsCollection 層的實現就是調用 Parent 的具體實現方法。而其餘的 AbsCollection 中的通用方法則使用這些原子進行實現。
一共給具體的父實現類定義了6個方法,後續一旦有新的類型添加的需求,只須要保證他能實現了這6個方法便可使用其餘的方法了。
下面說說這個包設計的一些特點。
Collection 使用了大量的可選參數,好比 Collection.Slice方法。
Slice(...int) ICollection
獲取Collection中的片斷,能夠有兩個參數或者一個參數。
若是是兩個參數,第一個參數表明開始下標,第二個參數表明結束下標,當第二個參數爲-1時候,就表明到Collection結束。
若是是一個參數,則表明從這個開始下標一直獲取到Collection結束的片斷。
intColl := NewIntCollection([]int{1, 2, 3, 4, 5}) retColl := intColl.Slice(2) if retColl.Count() != 3 { t.Error("Slice 錯誤") } retColl.DD() retColl = intColl.Slice(2,2) if retColl.Count() != 2 { t.Error("Slice 兩個參數錯誤") } retColl.DD() retColl = intColl.Slice(2, -1) if retColl.Count() != 3 { t.Error("Slice第二個參數爲-1錯誤") } retColl.DD() /* IntCollection(3):{ 0: 3 1: 4 2: 5 } IntCollection(2):{ 0: 3 1: 4 } IntCollection(3):{ 0: 3 1: 4 2: 5 } */
是否使用可選方法我糾結了好久,由於這種可選參數畢竟仍是不夠美觀的。不事後來仍是想到了Collection這個包的設計宗旨是方便業務開發。那麼業務開發使用者使用的爽的程度纔是這個包應該關心的,因此也就大量使用了這種對使用者靈活友好,可是略不美觀的實現方式。
鏈式調用是我在實現這個包的時候一直堅持的。由於複雜的業務邏輯,鏈式調用的寫法閱讀性是很高的。因此在全部能返回數組的函數中,我都返回了 ICollection 接口。以方便於後續調用。
可是 Golang 中還有一個 error 的處理問題。每一個函數調用其實都是有可能有錯誤的,這個錯誤若是直接返回,那麼鏈式調用必然就不可行了。我採用的方式是火丁[文章]中說到的錯誤處理機制。當錯誤出現的時候,我把錯誤掛載在當前或者返回的 IColleciton,或者返回的 IMix 中。而且提供了 Error() 方法來讓外部用戶獲取確認這個鏈式調用是否有錯誤。
這樣的錯誤處理機制是我如今能想到的最好的處理機制了(在 Go 2.0 handle error沒有出來以前)。它一方面兼顧了鏈式調用,一方面能進行錯誤檢查。固然這種方式的錯誤檢查機制等於弱化,不是在每次調用函數的時候強制用戶檢查了,而是在鏈式調用以後,建議用戶檢查。可是回到 Collection 庫的願景,這樣的實現會讓使用者更爲溫馨。
數組固然有個compare函數,這個函數我設計做爲匿名函數放在 AbsCollection 中,具體的實如今每一個實現類的 New 函數中進行設置。我也將這個 compare 函數的設置權限做爲 SetCompare() 函數放給外部設置。主要考慮到擴展性,若是後續你的 Collection 是包的本身定義的一個複雜的 Object方法,那麼你徹底能夠按照某個字段進行排序。
對象數組是我最耗費精力的一個實現類。它大量使用了反射。可是這個是能夠擴展的。因爲接口中的方法的輸入輸出徹底是 ICollection 接口。好比在初期,你使用 Collection 自帶的 NewObjCollection 實例化了一個 ICollection, 或許你對使用了反射的 Insert,Pluck 方法的效率不是很是滿意,那麼,你只須要本身實現一個 ACollection, 而且本身實現上文說的6個方法,繼承AbsCollection,那麼,你就能夠很方便的使用 Colleciton的其餘方法,且沒有反射。
這個是我很後面加的,在 New 一個Collection的時候,Collection 中的數組元素,是選擇將參數中的數組指針複製到 Colleciton 中,仍是將參數中的整個數組複製到 Collection 中呢?後來我選擇了後者。主要是考慮到安全性,NewCollection 的時候我複製一份,後續若是有對這個數組進行修改的操做,不會影響原先傳入的參數Slice。爲了一些安全性,犧牲一些內存,我認爲仍是值得的。
這個 Collection 包我也先後利用業餘時間開發了挺久了。主要是實現的思想不斷在變化,從最初的我將 error 以直接panic的方式保持鏈式調用,到但願實現一個 IMap 數據結構,到使用的是數組,仍是指針等,包括名字我也從最初的IArray 改爲ICollection(我但願從使用這個包開始,Collection就成爲了這個包的關鍵字,全部接口和函數一旦設計到數組的概念的時候就使用Collection這個關鍵字)。
寫一個通用庫其實並非那麼容易的事情,最重要的是思想還有設計感。
這個庫我目前就在我本身的項目組進行推廣和使用。文中的PPT就是我在項目組推廣時使用的PPT。目前已經打了1.0.1的tag。後續會持續優化,而且作一些文檔補充。但願能成爲最適合業務開發的 Collection 包。
再次推廣下這個項目 https://github.com/jianfengye/collection ,歡迎使用和提PR。熟練使用以後,它必定會讓你的業務開發效率提高一個檔次的。