iT邦幫忙

第 11 屆 iT 邦幫忙鐵人賽

DAY 19
0

還記得小時後唸書的座號嗎?老師點名的時候通常會從一號點到最後一號,不會故意從最後一號開始,也不會跳著點。但是有一種情況例外:就是那個座號的學生今天剛好請假或是轉校了。也就是說,在老師的腦中其實有一個小小的處理器,知道下一個應該要念哪一個學生的名字,而且知道什麼時候該停止

沒錯,這個處理器就是 Iterator,Iterator 可以用來巡訪一個聚合器裡的資料,並且可以把巡訪的邏輯跟聚合器分開,讓容器的職責保持單一。這邊所指的聚合器其實不限制任何形式,會在稍後多做說明。

Iterator Pattern 介紹

Iterator pattern 有兩個主要的角色,一個是 Iterator 本身,另一個是容器(也稱作 Aggregate)。標準的實作方式會是容器本身包含了相對應的 Iterator。要開始巡訪時,就會使用該容器的某一個公開介面 (public interface)來取得在其中的 Iterator,再開始進行操作。

好啦!我知道上面描述很抽象,這是因為實在有很多不一樣的實作方式,這邊來舉一個實際一點的例子吧!

以下使用 Kotlin 來做範例

// Aggregrate
class ClassRoom(val students: List<Student>, 
                val className: String) {

    //Iterator 實作在容器裡面
    fun getIterator(): StudentIterator {
        TODO()
    }
}

// Iterator
interface StudentIterator {
    // 需要知道有沒有下一個元素,false 代表沒有下一個了
    fun hasNext(): Boolean
    fun toNext()
    // 拿出下一個的元素
    fun nextStudent(): Student
}

先別急著看實作!先來看看 Iterator 有哪些 public function 吧!首先是 hasNext(),這個 function 可以讓操作的類別不需要知道巡訪的細節而得知現在的狀態,如果回傳值是 true 就可以放心呼叫 nextStudent() 來拿到該元素的實例,再使用 toNext() 來指到下一個元素。以下這段程式碼示範如何使用它。

fun printStudents(classRoom: ClassRoom) {
    val iterator = classRoom.getIterator()
    while (iterator.hasNext()) {
        val student = iterator.nextStudent()
        println(student)
        iterator.toNext()
    }
}

Iterator 實作

如果點名只有從頭走點尾會有點無聊對吧!這邊來加幾個簡單的規則:1. 學生有可能生病 2. 學生有可能不讀這間學校了。在這種情況下 Iterator 會跳過他們,先來看看 Student 的資料結構

data class Student(val number: Int, val name: String, val isSick: Boolean, val isDropout: Boolean)

所以相對應的 Iterator 實作就有可能長這樣

class ClassRoom(val students: List<Student>, val className: String) {

    fun getIterator(): StudentIterator {
        return object : StudentIterator {
            var index = 0

            override fun hasNext(): Boolean {
                return index < students.size
            }

            // 這邊是 Iterator 的核心,會決定下一個元素的位置
            override fun toNext() {
                index ++
                while (hasNext() && (students[index].isSick || students[index].isDropout)) {
                    index ++
                }
            }

            override fun nextStudent(): Student {
                return students[index]
            }
        }
    }
}

External vs Internal

以上這種做法我們會稱作是 External Iterator ,我們必須要自己處理 Iterator 的 hasNext()toNext(),相對應的另外一種 Iterator 會是 Internal Iterator。這種 Iterator 會著重在如何處理每一個元素,而不是巡訪的邏輯。以下是一個示範:

abstract class StudentTriverser(private val classRoom: ClassRoom) {

    protected abstract fun process(student: Student)

    fun start() {
        // 其實裡面用了 External Iterator 來巡訪
        val iterator = classRoom.getIterator()
        while (iterator.hasNext()) {
            val student = iterator.nextStudent()
            process(student)
            iterator.toNext()
        }
    }
}

這邊可以注意到 Iterator 跟 Aggregate 的角色反過來了,現在是 Aggregate 住在 Iterator 裡面,而且不再有 hasNext() 或是nextStudent() 等 function 。重點會放在 process() ,也就是要對每一個元素所進行的操作,以下示範如何使用:

fun triverse(classRoom: ClassRoom) {
    val triverser = object : StudentTriverser(classRoom) {
        
        override fun process(student: Student) {
            //只需要知道怎麼處理就好,達到關注點分離的效果
            print(student.name)
        }
    }
    triverser.start()
}

不用這麼麻煩吧!

沒錯,現在的確很多程式語言都有內建的 Iterator ,使用上也非常方便,再搭配上 functional programming 的 style ,以上這種需求只要兩三行就可以解決了,幹嘛還需要建立一個 Iterator 的類別呢?

classRoom.students
    .filter { student -> !student.isSick && !student.isDropout }
    .forEach { student -> println(student) }

沒那麼簡單

上面的 Iterator 範例也許現在來看有點過時了,不需要這麼複雜的設計。但是也要意識到世界上是有很多複雜的問題是需要被解決的,例如 JSON parser。

JSON 擁有相對複雜的結構,像是 [], {}, =, 還有字串。所以一般的 JSON parser 會提供 iterator function 像是 hasNext, nextInt, nextString 等等來完成特定需求。另一個例子則是 Java 的 BufferReader ,提供了 readLine()ready()

也許你將來會碰到一樣複雜的結構,或是新的 Streaming 技術,這時候請別忘了 Iterator!它將能幫助你解決困難的問題。

順帶一提,到這裡大家應該可以了解為什麼另一個角色是命名為 Aggregate 而不是 List 了,List 只能用來表示線性的資料結構,Aggregate 則更加抽象,代表某一種聚合體,可以包含各種不同的可能性!

Iterator 優缺點

  • 無法動態新增刪除元素,因為使用 Iterator 的類別並不知道實作的機制,所以在任何時機點都有可能會破壞 Iterator 的正確性。
  • 關注點分離,將如何使用元素的邏輯與如何巡訪的邏輯分開

總結

寫程式的時候很容易遇到一種狀況,就是深陷在複雜的邏輯裡而無法解開困難問題,但藉由 Iterator pattern ,我們可以解耦(decoupling)巡訪的邏輯,把更多心力放在處理個別的資料,同時增加可讀性以及可維護性。另一方面 Iterator 也可以跟其他 Pattern 結合一起使用,也不限於一種形式(像上面的 JSON Parser 跟一般的 Iterator 不一樣)。今天就到這邊,希望大家可以更多加活用 Design pattern!

作者:Yanbin


上一篇
[Design Pattern] Visitor 訪問者模式
下一篇
[Design Pattern] Command 命令模式
系列文
什麼?又是/不只是 Design Patterns!?32

尚未有邦友留言

立即登入留言