天天看點

實踐GoF的設計模式:疊代器模式

摘要:疊代器模式主要用在通路對象集合的場景,能夠向用戶端隐藏集合的實作細節。

本文分享自華為雲社群《【Go實作】實踐GoF的23種設計模式:疊代器模式》,作者:元閏子。

簡介

有時會遇到這樣的需求,開發一個子產品,用于儲存對象;不能用簡單的數組、清單,得是紅黑樹、跳表等較為複雜的資料結構;有時為了提升存儲效率或持久化,還得将對象序列化;但必須給用戶端提供一個易用的 API,允許友善地、多種方式地周遊對象,絲毫不察覺背後的資料結構有多複雜。

實踐GoF的設計模式:疊代器模式

對這樣的 API,很适合使用 疊代器模式(Iterator Pattern)實作。

GoF 對 疊代器模式 的定義如下:

Provide a way to access the elements of an aggregate object sequentially without exposing its underlying representation.

從描述可知,疊代器模式主要用在通路對象集合的場景,能夠向用戶端隐藏集合的實作細節。

Java 的 Collection 家族、C++ 的 STL 标準庫,都是使用疊代器模式的典範,它們為用戶端提供了簡單易用的 API,并且能夠根據業務需要實作自己的疊代器,具備很好的可擴充性。

UML 結構

實踐GoF的設計模式:疊代器模式

場景上下文

在簡單的分布式應用系統(示例代碼工程)中,db 子產品用來存儲服務注冊和監控資訊,它的主要接口如下:

// demo/db/db.go
package db
// Db 資料庫抽象接口
type Db interface {
 CreateTable(t *Table) error
 CreateTableIfNotExist(t *Table) error
 DeleteTable(tableName string) error
 Query(tableName string, primaryKey interface{}, result interface{}) error
 Insert(tableName string, primaryKey interface{}, record interface{}) error
 Update(tableName string, primaryKey interface{}, record interface{}) error
 Delete(tableName string, primaryKey interface{}) error
 ...
}      

從增删查改接口可以看出,它是一個 key-value 資料庫,另外,為了提供類似關系型資料庫的按列查詢能力,我們又抽象出 Table 對象:

// demo/db/table.go
package db
// Table 資料表定義
type Table struct {
    name            string
 recordType reflect.Type
    records         map[interface{}]record
}      

其中,Table 底層用 map 存儲對象資料,但并沒有存儲對象本身,而是從對象轉換而成的 record 。record 的實作原理是利用反射機制,将對象的屬性名 field 和屬性值 value 分開存儲,以此支援按列查詢能力(一類對象可以類比為一張表):

// demo/db/record.go
package db
type record struct {
 primaryKey interface{}
    fields     map[string]int // key為屬性名,value屬性值的索引
    values     []interface{} // 存儲屬性值
}
// 從對象轉換成record
func recordFrom(key interface{}, value interface{}) (r record, e error) {
 ... // 異常處理
 vType := reflect.TypeOf(value)
 vVal := reflect.ValueOf(value)
 if vVal.Type().Kind() == reflect.Pointer {
 vType = vType.Elem()
 vVal = vVal.Elem()
 }
 record := record{
 primaryKey: key,
        fields: make(map[string]int, vVal.NumField()),
        values: make([]interface{}, vVal.NumField()),
 }
 for i := 0; i < vVal.NumField(); i++ {
 fieldType := vType.Field(i)
 fieldVal := vVal.Field(i)
 name := strings.ToLower(fieldType.Name)
 record.fields[name] = i
 record.values[i] = fieldVal.Interface()
 }
 return record, nil
}      

當然,用戶端并不會察覺 db 子產品背後的複雜機制,它們直接使用的仍是對象:

type testRegion struct {
    Id   int
    Name string
}
func client() {
 mdb := db.MemoryDbInstance()
 tableName := "testRegion"
 table := NewTable(tableName).WithType(reflect.TypeOf(new(testRegion)))
 mdb.CreateTable(table)
 mdb.Insert(tableName, "region1", &testRegion{Id: 0, Name: "region-1"})
 result := new(testRegion)
 mdb.Query(tableName, "region1", result)
}      
實踐GoF的設計模式:疊代器模式

另外,除了上述按 Key 查詢接口,我們還想提供全表查詢接口,有随機和有序 2 種表記錄周遊方式,并且支援用戶端自己擴充周遊方式。下面使用疊代器模式來實作該需求。

代碼實作

這裡并沒有按照标準的 UML 結構去實作,而是結合工廠方法模式來解決公共代碼的複用問題:

實踐GoF的設計模式:疊代器模式
// demo/db/table_iterator.go
package db
// 關鍵點1: 定義疊代器抽象接口,允許後續用戶端擴充周遊方式
// TableIterator 表疊代器接口
type TableIterator interface {
 HasNext() bool
 Next(next interface{}) error
}
// 關鍵點2: 定義疊代器接口的實作
// tableIteratorImpl 疊代器接口公共實作類
type tableIteratorImpl struct {
 // 關鍵點3: 定義一個集合存儲待周遊的記錄,這裡的記錄已經排序好或者随機打散
    records []record
 // 關鍵點4: 定義一個cursor遊标記錄目前周遊的位置
 cursor  int
}
// 關鍵點5: 在HasNext函數中的判斷是否已經周遊完所有記錄
func (r *tableIteratorImpl) HasNext() bool {
 return r.cursor < len(r.records)
}
// 關鍵點6: 在Next函數中取出下一個記錄,并轉換成用戶端期望的對象類型,記得增加cursor
func (r *tableIteratorImpl) Next(next interface{}) error {
 record := r.records[r.cursor]
 r.cursor++
 if err := record.convertByValue(next); err != nil {
 return err
 }
 return nil
}
// 關鍵點7: 通過工廠方法模式,完成不同類型的疊代器對象建立
// TableIteratorFactory 表疊代器工廠
type TableIteratorFactory interface {
 Create(table *Table) TableIterator
}
// 随機疊代器
type randomTableIteratorFactory struct{}
func (r *randomTableIteratorFactory) Create(table *Table) TableIterator {
 var records []record
 for _, r := range table.records {
        records = append(records, r)
 }
 rand.Seed(time.Now().UnixNano())
 rand.Shuffle(len(records), func(i, j int) {
        records[i], records[j] = records[j], records[i]
 })
 return &tableIteratorImpl{
        records: records,
        cursor: 0,
 }
}
// 有序疊代器
// Comparator 如果i<j傳回true,否則傳回false
type Comparator func(i, j interface{}) bool
// sortedTableIteratorFactory 根據主鍵進行排序,排序邏輯由Comparator定義
type sortedTableIteratorFactory struct {
    comparator Comparator
}
func (s *sortedTableIteratorFactory) Create(table *Table) TableIterator {
 var records []record
 for _, r := range table.records {
        records = append(records, r)
 }
 sort.Sort(newRecords(records, s.comparator))
 return &tableIteratorImpl{
        records: records,
        cursor: 0,
 }
}      

最後,為 Table 對象引入 TableIterator:

// demo/db/table.go
// Table 資料表定義
type Table struct {
    name            string
 recordType reflect.Type
    records         map[interface{}]record
 // 關鍵點8: 持有疊代器工廠方法接口
 iteratorFactory TableIteratorFactory // 預設使用随機疊代器
}
// 關鍵點9: 定義Setter方法,提供疊代器工廠的依賴注入
func (t *Table) WithTableIteratorFactory(iteratorFactory TableIteratorFactory) *Table {
 t.iteratorFactory = iteratorFactory
 return t
}
// 關鍵點10: 定義建立疊代器的接口,其中調用疊代器工廠完成執行個體化
func (t *Table) Iterator() TableIterator {
 return t.iteratorFactory.Create(t)
}      

用戶端這樣使用:

func client() {
 table := NewTable("testRegion").WithType(reflect.TypeOf(new(testRegion))).
 WithTableIteratorFactory(NewSortedTableIteratorFactory(regionIdComparator))
 iter := table.Iterator()
 for iter.HashNext() {
 next := new(testRegion)
 err := iter.Next(next)
 ... 
 }
}      

總結實作疊代器模式的幾個關鍵點:

  1. 定義疊代器抽象接口,目的是提供用戶端自擴充能力,通常包含 HashNext() 和 Next() 兩個方法,上述例子為 TableIterator。
  2. 定義疊代器接口的實作類,上述例子為 tableIteratorImpl,這裡主要起到了 Java/C++ 等帶繼承特性語言中,基類的作用,目的是複用代碼。
  3. 在實作類中持有待周遊的記錄集合,通常是已經排序好或随機打散後的,上述例子為 tableIteratorImpl.records。
  4. 在實作類中持有遊标值,記錄目前周遊的位置,上述例子為 tableIteratorImpl.cursor。
  5. 在 HashNext() 方法中判斷是否已經周遊完所有記錄。
  6. 在 Next() 方法中取出下一個記錄,并轉換成用戶端期望的對象類型,取完後增加遊标值。
  7. 通過工廠方法模式,完成不同類型的疊代器對象建立,上述例子為 TableIteratorFactory 接口,以及它的實作,randomTableIteratorFactory 和 sortedTableIteratorFactory。
  8. 在待周遊的對象中,持有疊代器工廠方法接口,上述例子為 Table.iteratorFactory。
  9. 為對象定義 Setter 方法,提供疊代器工廠的依賴注入,上述例子為 Table.WithTableIteratorFactory() 方法。
  10. 為對象定義建立疊代器的接口,上述例子為 Table.Iterator() 方法。

其中,7~9 步是結合工廠方法模式實作時的特有步驟,如果你的疊代器實作中沒有用到工廠方法模式,可以省略這幾步。

擴充

Go 風格的實作

前面的實作,是典型的面向對象風格,下面以随機疊代器為例,給出一個 Go 風格的實作:

// demo/db/table_iterator_closure.go
package db
// 關鍵點1: 定義HasNext和Next函數類型
type HasNext func() bool
type Next func(interface{}) error
// 關鍵點2: 定義建立疊代器的方法,傳回HashNext和Next函數
func (t *Table) ClosureIterator() (HasNext, Next) {
 var records []record
 for _, r := range t.records {
        records = append(records, r)
 }
 rand.Seed(time.Now().UnixNano())
 rand.Shuffle(len(records), func(i, j int) {
        records[i], records[j] = records[j], records[i]
 })
 size := len(records)
 cursor := 0
 // 關鍵點3: 在疊代器建立方法定義HasNext和Next的實作邏輯
 hasNext := func() bool {
 return cursor < size
 }
 next := func(next interface{}) error {
 record := records[cursor]
        cursor++
 if err := record.convertByValue(next); err != nil {
 return err
 }
 return nil
 }
 return hasNext, next
}      

用戶端這樣用:

func client() {
 table := NewTable("testRegion").WithType(reflect.TypeOf(new(testRegion))).
 WithTableIteratorFactory(NewSortedTableIteratorFactory(regionIdComparator))
 hasNext, next := table.ClosureIterator()
 for hasNext() {
 result := new(testRegion)
 err := next(result)
 ... 
 }
}      

Go 風格的實作,利用了函數閉包的特點,把原本在疊代器實作的邏輯,放到了疊代器建立方法上。相比面向對象風格,省掉了疊代器抽象接口和實作對象的定義,看起來更加的簡潔。

總結幾個實作關鍵點:

  1. 聲明 HashNext 和 Next 的函數類型,等同于疊代器抽象接口的作用。
  2. 定義疊代器建立方法,傳回類型為 HashNext 和 Next,上述例子為 ClosureIterator() 方法。
  3. 在疊代器建立方法内,定義 HasNext 和 Next 的具體實作,利用函數閉包來傳遞狀态(records 和 cursor)。

基于 channel 的實作

我們還能基于 Go 語言中的 channel 來實作疊代器模式,因為前文的 db 子產品應用場景并不适用,是以另舉一個簡單的例子:

type Record int
func (r *Record) doSomething() {
 // ...
}
type ComplexCollection struct {
    records []Record
}
// 關鍵點1: 定義疊代器建立方法,傳回隻能接收的channel類型
func (c *ComplexCollection) Iterator() <-chan Record {
 // 關鍵點2: 建立一個無緩沖的channel
 ch := make(chan Record)
 // 關鍵點3: 另起一個goroutine往channel寫入記錄,如果接收端還沒開始接收,會阻塞住
 go func() {
 for _, record := range c.records {
 ch <- record
 }
 // 關鍵點4: 寫完後,關閉channel
 close(ch)
 }()
 return ch
}      

用戶端這樣使用:

func client() {
 collection := NewComplexCollection()
 // 關鍵點5: 使用時,直接通過for-range來周遊channel讀取記錄
 for record := range collection.Iterator() {
 record.doSomething()
 }
}      

總結實作基于 channel 的疊代器模式的幾個關鍵點:

  1. 定義疊代器建立方法,傳回一個隻能接收的 channel。
  2. 在疊代器建立方法中,定義一個無緩沖的 channel。
  3. 另起一個 goroutine 往 channel 中寫入記錄。如果接收端沒有接收,會阻塞住。
  4. 寫完後,關閉 channel。
  5. 用戶端使用時,直接通過 for-range 周遊 channel 讀取記錄即可。

帶有 callback 函數的實作

還可以在建立疊代器時,傳入一個 callback 函數,在疊代器傳回記錄前,先調用 callback 函數對記錄進行一些操作。

比如,在基于 channel 的實作例子中,可以增加一個 callback 函數,将每個記錄列印出來:

// 關鍵點1: 聲明callback函數類型,以Record作為入參
type Callback func(record *Record)
//關鍵點2: 定義具體的callback函數
func PrintRecord(record *Record) {
 fmt.Printf("%+v\n", record)
}
// 關鍵點3: 定義以callback函數作為入參的疊代器建立方法
func (c *ComplexCollection) Iterator(callback Callback) <-chan Record {
 ch := make(chan Record)
 go func() {
 for _, record := range c.records {
 // 關鍵點4: 周遊記錄時,調用callback函數作用在每條記錄上
 callback(&record)
 ch <- record
 }
 close(ch)
 }()
 return ch
}
func client() {
 collection := NewComplexCollection()
 // 關鍵點5: 建立疊代器時,傳入具體的callback函數
 for record := range collection.Iterator(PrintRecord) {
 record.doSomething()
 }
}      

總結實作帶有 callback 的疊代器模式的幾個關鍵點:

  1. 聲明 callback 函數類型,以 Record 作為入參。
  2. 定義具體的 callback 函數,比如上述例子中列印記錄的 PrintRecord 函數。
  3. 定義疊代器建立方法,以 callback 函數作為入參。
  4. 疊代器内,周遊記錄時,調用 callback 函數作用在每條記錄上。
  5. 用戶端建立疊代器時,傳入具體的 callback 函數。

典型應用場景

  • 對象集合/存儲類子產品,并希望向用戶端隐藏子產品背後的複雜資料結構。
  • 希望支援用戶端自擴充多種周遊方式。

優缺點

優點

  • 隐藏子產品背後複雜的實作機制,為用戶端提供一個簡單易用的接口。
  • 支援擴充多種周遊方式,具備較強的可擴充性,符合開閉原則。
  • 周遊算法和資料存儲分離,符合單一職責原則。

缺點

  • 容易濫用,比如給簡單的集合類型實作疊代器接口,反而使代碼更複雜。
  • 相比于直接周遊集合,疊代器效率要更低一些,因為涉及到更多對象的建立,以及可能的對象拷貝。
  • 需要時刻注意在疊代器周遊過程中,由原始集合發生變更引發的并發問題。一種解決方法是,在建立疊代器時,拷貝一份原始資料(TableIterator 就這麼實作),但存在效率低、記憶體占用大的問題。

與其他模式的關聯

疊代器模式通常會與工廠方法模 一起使用,如前文實作。

文章配圖

可以在用Keynote畫出手繪風格的配圖中找到文章的繪圖方法。

參考

[1] 【Go實作】實踐GoF的23種設計模式:SOLID原則, 元閏子

[2] 【Go實作】實踐GoF的23種設計模式:工廠方法模式, 元閏子

[3] Design Patterns, Chapter 5. Behavioral Patterns, GoF

[4] Iterators in Go, Ewen Cheslack-Postava

[5] 疊代器模式, refactoringguru.cn

點選關注,第一時間了解華為雲新鮮技術~

繼續閱讀