golang設計模式之迭代器模式

silsuer在掘金發表於2019-02-25

迭代器模式

定義

wiki: 在 物件導向程式設計裡,迭代器模式是一種設計模式,是一種最簡單也最常見的設計模式。它可以讓使用者透過特定的介面巡訪容器中的每一個元素而不用瞭解底層的實作。

簡單點說,為一個容器設定一個迭代函式,可以使用這個迭代函式來順序訪問其中的每一個元素,而外部無需知道底層實現。

如果再結合 訪問者模式,向其中傳入自定義的訪問者,那麼就可以讓訪問者訪問容器中的每個元素了。

類圖

golang設計模式之迭代器模式

(圖源網路)

角色

  • 抽象聚合類: 定義一個抽象的容器

  • 具體聚合類: 實現上面的抽象類,作為一個容器,用來存放元素,等待迭代

  • 抽象迭代器: 迭代器介面,每個容器下都有一個該迭代器介面的具體實現

  • 具體迭代器: 根據不同的容器,需要定義不同的具體迭代器,定義了遊標移動的具體實現

舉個例子

  1. 建立抽象容器結構體
 // 容器介面
 type IAggregate interface {
 	Iterator() IIterator
 }
複製程式碼
  1. 建立抽象迭代器
 // 迭代器介面
 type IIterator interface {
     HasNext() bool
     Current() int
     Next() bool
 }
複製程式碼

迭代器的基本需求,需要有判定是否迭代到最後的方法HasNext(),需要有獲得當前元素的方法Current(),需要有將遊標移動到下一個元素的方法 Next()

  1. 實現容器
  // 具體容器
  type Aggregate struct {
    container []int // 容器中裝載 int 型容器
  }
  // 建立一個迭代器,並讓迭代器中的容器指標指向當前物件
  func (a *Aggregate) Iterator() IIterator {
       i := new(Iterator)
       i.aggregate = a
       return i
   }
複製程式碼

為了簡便,這裡我們僅僅讓容器中存放 int型別的資料

  1. 實現迭代器
 type Iterator struct {
     cursor    int // 當前遊標
     aggregate *Aggregate // 對應的容器指標
 }
 
 // 判斷是否迭代到最後,如果沒有,則返回true
 func (i *Iterator) HasNext() bool {
     if i.cursor+1 < len(i.aggregate.container) {
         return true
     }
     return false
 }
 
 // 獲取當前迭代元素(從容器中取出當前遊標對應的元素)
 func (i *Iterator) Current() int {
     return i.aggregate.container[i.cursor]
 }
 
 // 將遊標指向下一個元素
 func (i *Iterator) Next() bool {
     if i.cursor < len(i.aggregate.container) {
         i.cursor++
         return true
     }
     return false
 }
複製程式碼
  1. 使用迭代器
 func main() {
     // 建立容器,並放入初始化資料
     c := &Aggregate{container: []int{1, 2, 3, 4}}
     // 獲取迭代器
     iterator := c.Iterator() 
     for {
     	// 列印當前資料
 	    fmt.Println(iterator.Current())
 	    // 如果有下一個元素,則將遊標移動到下一個元素
 	    // 否則跳出迴圈,迭代結束
 	    if iterator.HasNext() {
 		    iterator.Next()
 	    } else {
 		    break
 	    }
     }
 }
複製程式碼

上面的例子比較簡單,大家可以想象一下,如果容器類種的 container 不是一個切片或陣列,而是一個結構體陣列,我們卻只需要遍歷每個結構體中的某個欄位呢?結構體的某個欄位是陣列,又或者,這個結構體的需要迭代的欄位是私有的,在包外無法訪問,所以這時是用迭代器,就可以自定義一個迭代函式,對外僅僅暴露幾個方法,來獲取資料,而外部不會在意容器內究竟是結構體還是陣列。

實際栗子

我可能又雙叕要寫自己造的輪子了…

不過這裡我只說說我對於迭代器模式的一個實際應用, 這個輪子沒有開發完,只是個半半半成品

github.com/silsuer/bin…

這是我計劃寫的一個不借助 標準庫中的template 實現的模板引擎,後來工作太忙,就擱淺了,涉及到了編譯原理的一些知識,目前只寫到了詞法分析…

可能要等 Bingo 的所有模組都寫完之後再去實現了吧…

廢話有點多… 入正題…

模板引擎需要將模板中的字元,按照模板標記的左右定界符分割成詞法鏈,例如下面的模板:

 {{ for item in navigation }}
     <li>tag</li>
 {{ endfor }}
複製程式碼

這個模板的意思是遍歷 navigation,列印出對應數量的 li 標籤

將會生成如下的詞法鏈

  [for]-> [item]-> [in]-> [navigation]-> [<li>tag</li>]-> [endfor] 
複製程式碼

每個方括號代表一個詞法鏈上的節點,每個節點都會區分出是文字節點還是語法節點。

具體的實現方法這裡不說了,涉及到了詞法分析器的狀態轉換,有興趣的自己搜一搜就好,下面要實現的就是除錯時列印詞法鏈的過程,用到了迭代器模式。

詞法鏈的結構體如下:

 // 詞法分析鏈包含大量節點
 type LexicalChain struct {
 	Nodes       []*LexicalNode     
 	current     int                    // 當前指標
 	Params      map[string]interface{} // 變數名->變數值
 	TokenStream *TokenStream           // token流,這是通過節點解析出來的
 }
複製程式碼

對應的詞法節點的結構體如下:

 type LexicalNode struct {
 	T       int      // 型別(詞法節點還是文字節點)
 	Content []byte   // 內容,生成模版的時候要使用內容進行輸出
 	tokens  []*Token // token流
 	root    *Token   // 抽象語法樹跟節點
 	lineNum int      // 行數
 	stack   []int    // 符棧,用來記錄成對的操作符
 }
複製程式碼

每個節點的列印方法 Print():

 // 列印節點值
 func (n *LexicalNode) Print() {
 	switch n.T {
 	case textNode:
 		fmt.Println("[node type]: TEXT") // 文字節點
 	case lexicalNode:
 		fmt.Println("[node type]: LEXICAL") // 詞法節點
 	default:
 		fmt.Println("[node type]: UNKNOWN TYPE") // 未知型別
 		break
 	}
 	fmt.Println("[line number]: " + strconv.Itoa(n.lineNum))
 	fmt.Println("[content]: " + string(n.Content))
 }
複製程式碼

上面是列印一個節點的,當要列印整個詞法鏈時,需要迭代整個詞法鏈,對每個節點呼叫 Print() 方法:

  func (l *LexicalChain) Print() {
  	// 列印當前節點
  	l.Iterator(func(node *LexicalNode) {
  		fmt.Println("====================")
  		fmt.Println("[index]: " + strconv.Itoa(l.current))
  		node.Print()
  	})
  }    
複製程式碼

這裡的實現方法與第一個栗子的實現方式不同,可以看做這裡是迭代器模式與訪問者模式的結合使用,對迭代器方法Iterator(),傳入一個回撥函式作為訪問者,

對每個節點呼叫 Print() 方法來列印節點。

下面看看 Iterator() 方法的實現:

  func (l *LexicalChain) Iterator(call func(node *LexicalNode)) {
  	// 對於鏈中的每個節點,執行傳入的方法
  	call(l.Current()) // 呼叫傳入的方法,將當前節點作為引數傳入
  	for {
  		if l.Next() != -1 {  // 這裡和第一個栗子一樣,將遊標指向下一個元素,並繼續呼叫 傳入的回撥
  			call(l.Current())
  		} else {
  			break  // 如果迭代到了最後,則直接跳出迴圈,結束迭代
  		}
  	}
  }
複製程式碼

Next() 函式的實現和第一個栗子差不多,就不貼了,程式碼在這裡

這樣就可以對生成的詞法鏈進行列印了,方便了後續除錯開發…

總結

迭代器模式算是最簡單也最常用的行為型模式了,在JAVA中尤其常見,對於phper如果習慣了 laravel集合,可以去看看其中filter/map 等方法的實現,都是通過傳入一個回撥,然後方法內部迭代元素的方式實現過濾的。

優點:

  1. 這個不用說了,設計模式的最大優點——解耦,增加容器或者迭代器都無需修改原始碼,並且簡化了容器類,將迭代邏輯抽出來,放在了迭代器中
  2. 可以用不同的方式來遍歷同一個物件(這就是上面說的,通過傳入不同的回撥來進行不同的迭代)
    缺點:
  3. 迭代器模式算是將一個物件(容器)的儲存職責和遍歷職責分離了,就像第一個栗子中說的,新增一個容器類就要新增一個迭代器類,增加了系統的程式碼量和複雜性。

上述程式碼均放在 golang-design-patterns 這個倉庫中

打個廣告,推薦一下自己寫的 go web框架 bingo,求star,求PR ~

相關文章