Specification 規格模式 | 解說實現 | Query 語句實做

Specification 規格模式 | 解說實現 | Query 語句實做

Overview of Content

本文深入探討了軟體設計中的 Specification 模式,以及規格查詢的實際實現。

文章涵蓋了多個方面,包括規格模式的使用時機、使用 UML 圖表進行規格設計的方法、優缺點分析等。此外,它還提供了對規格模式的實際實現的見解,涵蓋了分析原始需求、通過策略封裝變化、遞歸結果組合等主題,以及 Kotlin 中中置函數(infix)的應用


Specification 使用時機

當有多個相同物件 需要過濾,並且過濾條件需要可拓展、或要求連續過濾時…就可以使用 Specification 模式

Specification 設計 UML

Specification 模式是用兩種經典模式拓展,一個是 ^1.^ 策略模式,另一個是 ^2.^ 組合模式,透過這兩個模式的組合來達到過濾規格的拓展

模式在 Specification 中的幫助
策略模式簡易拓展規格、條件,將其使用類包裝細節實做
組合模式覆用其他規格的結果進行 複合條件判斷

Specification 設計優缺點

Specification 設計優點

封裝界面的變化為另外一個界面,可以輕鬆拓展變化(規格)

● 組合遞迴自身的結果(某個規格的結果)來滿足 複合操作

Specification 設計缺點

不符合依賴倒置

變化(規格)的實做類依賴於實體類,要多加考慮依賴的實做子類是否有可拓展性,如果有拓展性則不可以依賴該實體類;

Specification 實現

這裡我們用一種 需求推進的方式 來慢慢實現 Specification 設計模式;

● 在 C#3.5 有一個重要的特性 LINQ (Language Integrated Query),它提供類類似 SQL 語句的功能,透過一系列的描述來過濾數據庫中的資料

Dim DataList As string() = {"abc", "aaa", "bbbb"}

// 類似 SQL Select 語法
Dim Result = From T As String In DataList where T = "bbbb"

其中的 where T = "bbbb" 就是條件,我們可以把它稱為一種規格,用來限制、過濾數據庫中的資料,這個條件就可以使用 Specification 規格模式

● 接下來我們實現一個需求,該需求需要依照特定條件過濾數據庫中的數據

Specification 原始需求:分析需求

● 依照原始需求,基礎來建立一個抽象界面,該界面會依照過濾條件(eg. name, age... 等等條件)來創建不同函數

初始設計:User 使用抽象的 IDataProvider 界面提供的方法來解析 String(可以想成是外部的 MetaData)並過濾條件,最後取得 DataInfo 列表資料

A. BO 類(Business Object)

單純的儲存資料 (setter)、取得資料 (getter)

data class DataInfo constructor(val name: String, val age: Int) {
    override fun toString(): String {
        return "name=($name), age=($age)"
    }
}

B. 需求界面

IDataProvider 界面按照需求來創建不同函數,每個函數會依照不同條件做不同的過濾行為

interface IDataProvider {

    fun findDataBytName(name: String): List<DataInfo>

    fun findDataByAgeThan(age: Int): List<DataInfo>

}

C. 實做需求界面

這時做界面很簡單,從資料庫中取出資料遍歷,並判斷需求進行過濾,最終返回結果

class DataProviderImpl: IDataProvider {

    val dataList = mutableListOf<DataInfo>()

    override fun findDataBytName(name: String): List<DataInfo> {
        return mutableListOf<DataInfo>().apply {
            dataList.forEach {
                if (it.name == name) {
                    this.add(it)
                }
            }
        }
    }

    override fun findDataByAgeThan(age: Int): List<DataInfo> {
        return mutableListOf<DataInfo>().apply {
            dataList.forEach {
                if (it.age > age) {
                    this.add(it)
                }
            }
        }
    }

}

● 設計完設計完畢後,作為使用者來使用最基礎的過濾、分析功能功能

fun main() {
    DataProviderImpl().apply {
        dataList.add(DataInfo("Kyle", 10))
        dataList.add(DataInfo("Alien", 20))
        dataList.add(DataInfo("Pan", 30))
        dataList.add(DataInfo("Yo", 10))
        dataList.add(DataInfo("Platform-Apple", 21))
        dataList.add(DataInfo("Platform-iOS", 13))
        dataList.add(DataInfo("Platform-Android", 43))
        dataList.add(DataInfo("Platform-Window", 33))

        findDataBytName("Alien").forEach {
            println(it)
        }

        println()

        findDataByAgeThan(30).forEach {
            println(it)
        }
    }
}

探討 - 問題點

這邊我們可以發現幾個問題,由於這些問題我們無法達成可拓展的需求(無法依照業務需求來變化)

A. 每次需求的變更都需要更改界面,這不符合開閉原則;可能下次是希望過濾名子字母開頭?那這樣就需要拓展界面

B. 修改界面後,所有子類都需要 被迫拓展實做

Specification 策略:封裝變化

● 透過上一個範例我們再來分析狀況

這個界面中的方法有啥不同點? name & age 的差別?

// 原本的界面

interface IDataProvider {

    fun findDataBytName(name: String): List<DataInfo>

    fun findDataByAgeThan(age: Int): List<DataInfo>

}

不同點就是 變化點,這兩個函數的不同點在於接收參數不同,導致判斷方式不同;發現變化點後,我們就可以 封裝變化區域

● 優化後程式的 UML 如下

重複出現的程式碼不再重寫,請看上面範例,這裡只寫出不同點、修改點

A. 封裝判斷界面

● 將變化點封裝為 IDataPredicate 界面,之後的透過 拓展該界面來達成不同的過濾條件

interface IDataPredicate {

    fun isSatisfied(info: DataInfo) : Boolean

}

● 將原來的界面 IDataProvider2(稍微修改個名子) 依賴於封裝

interface IDataProvider2 {

    // 依賴界面
    fun findData(predicate: IDataPredicate): List<DataInfo>

}

謂何要封裝?可以寫一個 predicate: (DataInfo) -> Boolean 參數吧?

確實可以,不過也請注意,如果你這樣做就同時 暴露了 資料庫結構(DataInfo)給使用者,並讓使用者自己過濾

而這裡的選擇是 使用類封裝,使者需要知道類,而不必知道詳細的資料結構,這保證了資料的安全性、可變化性

B. 實做判斷、提供界面

● 判斷界面實做:實做 IDataPredicate 界面,也就是判斷條件;這個好處也是讓使用者可以透過函數名稱來清出的知道當前調用判斷的條件,方便維護以及覆用,有良好的封裝效果~

class FindByName(private val name: String): IDataPredicate {

    override fun isSatisfied(info: DataInfo): Boolean {
        return info.name == name
    }

}

class FindByAge(private val age: Int): IDataPredicate {

    override fun isSatisfied(info: DataInfo): Boolean {
        return info.age > age
    }

}

● 這不就是 策略模式

沒錯!這裡就有策略模式的影子,隱藏實做的細節,用不同的方式實現對外的合約界面

● 提供界面實做:實做 IDataProvider2 界面,過濾多個使用者設定的條件

class DataProviderImprove: IDataProvider2 {

    val dataList = mutableListOf<DataInfo>()
    override fun findData(predicate: IDataPredicate): List<DataInfo> {
        
        return mutableListOf<DataInfo>().apply {
            dataList.forEach {
                // 呼叫判斷界面!
                if (predicate.isSatisfied(it)) {
                    this.add(it)
                }
            }
        }
    }

}

● 使用優化過的程式

fun main() {
    DataProviderImprove().apply {
        dataList.add(DataInfo("Kyle", 10))
        dataList.add(DataInfo("Alien", 20))
        dataList.add(DataInfo("Pan", 30))
        dataList.add(DataInfo("Yo", 10))
        dataList.add(DataInfo("Platform-Apple", 21))
        dataList.add(DataInfo("Platform-iOS", 13))
        dataList.add(DataInfo("Platform-Android", 43))
        dataList.add(DataInfo("Platform-Window", 33))

        findData(FindByName("Alien")).forEach {
            println("Improve: $it")
        }

        println()

        findData(FindByAge(30)).forEach {
            println("Improve: $it")
        }
    }
}

● 再次省思,這樣滿足所有需求條件了嘛?

似乎還差一點... 假如有多個條件(連續)要過濾,那我們就必須寫兩次過濾條件,還可以在優化;如下範例

DataProviderImprove().apply {
    dataList.add(DataInfo("Kyle", 10))
    dataList.add(DataInfo("Alien", 20))
    dataList.add(DataInfo("Pan", 30))
    dataList.add(DataInfo("Yo", 10))
    dataList.add(DataInfo("Platform-Apple", 21))
    dataList.add(DataInfo("Platform-iOS", 13))
    dataList.add(DataInfo("Platform-Android", 43))
    dataList.add(DataInfo("Platform-Window", 33))

    // 1. 首字過濾的結果
    val result1 = findData(FindByAge(30))

    // 2. 接著上一個結果,再次進行過濾
    DataProviderImprove().dataList.addAll(result1).apply {

        findData(FindByName("Alien")).apply { 

        }

    }

}

Specification 組合:遞迴結果

● 透過上一個 Specification 策略優化過後,我們可以自由拓展判斷條件,但 不能自由的複合使用And, Or, Not... 等等條件),這裡我們在思考一下

A. 組合出所有條件,並寫出對應的處理類 ?

No~ 這會產生組合的爆炸 !!

假設每個條件有兩種可能,那 3 個條件就有 23 種可能性,如果有更多條件那就會造成維護的困難性!

B. 規格覆用 ?

Okay~ 但是要看場合

經過仔細觀察,其實這裡所謂的複合使用(And, Or, Not... 等等條件),其實是 使用了原有規格的結果,再次進行另一個規格的過濾

● 優化後程式如下

省略 User 使用的 UML 圖

A. 條件界面拓展

該條件界面是給使用者使用,使用者可以透過該界面去進行複合操作

● 這裡有個要注意的點, And, Or, Not 這些 操作都返回規格書界面(IDataSpecification2),這是為了 遞歸操作

interface IDataSpecification2 {

    fun isSatisfied(info: DataInfo) : Boolean

    fun and(anotherPredicate: IDataSpecification2): IDataSpecification2

    fun or(anotherPredicate: IDataSpecification2): IDataSpecification2

    operator fun not(): IDataSpecification2

}

B. 界面實做的共用類

由於 And, Or, Not 這些操作是不可拓展的操作,所以我們可以創建一個基礎類實現,之後再讓其詳細條件判斷的子類去繼承

abstract class CompositePredicate: IDataSpecification2 {
    override fun and(anotherPredicate: IDataSpecification2): IDataSpecification2 {
        return AndSpecification(this, anotherPredicate)
    }

    override fun or(anotherPredicate: IDataSpecification2): IDataSpecification2 {
        return OrSpecification(this, anotherPredicate)
    }

    override fun not(): IDataSpecification2 {
        return NotSpecification(this)
    }

}

● 在這裡可以看到一個奇怪現象,抽象父類依賴子類的細節實做!?

這個設計只在 非常明確不會發生變化的場景 中使用(確實 And, Or, Not 這些操作的確不會有多大的變動)

C. 複合操作的實做

該實做其實是調用「已知的操作」+「新操作」,再進行 And, Or, Not 判斷而已

class AndSpecification constructor(private val origin: IDataSpecification2, private val new: IDataSpecification2): CompositePredicate() {

    override fun isSatisfied(info: DataInfo): Boolean {
        // 已知的操作 + 新操作
        return origin.isSatisfied(info) && new.isSatisfied(info)
    }

}

class OrSpecification constructor(private val origin: IDataSpecification2, private val new: IDataSpecification2): CompositePredicate() {

    override fun isSatisfied(info: DataInfo): Boolean {
        // 已知的操作 + 新操作
        return origin.isSatisfied(info) || new.isSatisfied(info)
    }

}

class NotSpecification constructor(private val origin: IDataSpecification2): CompositePredicate() {

    override fun isSatisfied(info: DataInfo): Boolean {
        // 已知的操作 + 新操作(反向)
        return !origin.isSatisfied(info)
    }

}

D. 條件(規格)類實做

這些類繼承於共用類 CompositePredicate 並實現詳細的業務邏輯(過濾)判斷

class FindByAgeBigThan(private val age: Int): CompositePredicate() {

    override fun isSatisfied(info: DataInfo): Boolean {
        return info.age > age
    }

}

class FindByAgeSmallThan(private val age: Int): CompositePredicate() {

    override fun isSatisfied(info: DataInfo): Boolean {
        return info.age < age
    }

}

E. 提供界面、實做

該界面、實做並沒有做多大的修改

interface IDataProvider3 {

    fun findData(predicate: IDataSpecification2): List<DataInfo>

}

class DataProviderImproveAgain: IDataProvider3 {

    val dataList = mutableListOf<DataInfo>()
    override fun findData(predicate: IDataSpecification2): List<DataInfo> {
        return mutableListOf<DataInfo>().apply {
            dataList.forEach {
                if (predicate.isSatisfied(it)) {
                    this.add(it)
                }
            }
        }
    }

}

● 使用者再次使用

可以看到這次使用起來相對簡單,使用者不用負擔每次規格書(IDataSpecification2)過濾的結果,僅須了解規格書實做類即可(FindByAgeSmallThan, FindByAgeSmallThan

fun main() {
    DataProviderImproveAgain().apply {
        dataList.add(DataInfo("Kyle", 10))
        dataList.add(DataInfo("Alien", 20))
        dataList.add(DataInfo("Pan", 30))
        dataList.add(DataInfo("Yo", 10))
        dataList.add(DataInfo("Platform-Apple", 21))
        dataList.add(DataInfo("Platform-iOS", 13))
        dataList.add(DataInfo("Platform-Android", 43))
        dataList.add(DataInfo("Platform-Window", 33))

        findData(FindByAgeBigThan(30).or(FindByAgeSmallThan(20))).forEach {
            println("Improve: $it")
        }
    }
}

結合 Kotlin infix 函數

● 使用 Kotlin 提供的 中綴表達式infix)可以將函數寫的更像是 SQL 語法

interface IDataSpecification2 {

    fun isSatisfied(info: DataInfo) : Boolean

    // infix 函數
    infix fun and(anotherPredicate: IDataSpecification2): IDataSpecification2

    // infix 函數
    infix fun or(anotherPredicate: IDataSpecification2): IDataSpecification2

    operator fun not(): IDataSpecification2

}

使用起來一樣,但更像是 SQL 語法

fun main() {
    DataProviderImproveAgain().apply {
        dataList.add(DataInfo("Kyle", 10))
        dataList.add(DataInfo("Alien", 20))
        dataList.add(DataInfo("Pan", 30))
        dataList.add(DataInfo("Yo", 10))
        dataList.add(DataInfo("Platform-Apple", 21))
        dataList.add(DataInfo("Platform-iOS", 13))
        dataList.add(DataInfo("Platform-Android", 43))
        dataList.add(DataInfo("Platform-Window", 33))

        findData(FindByAgeBigThan(30) or !FindByAgeSmallThan(20)).forEach {
            println("Improve: $it")
        }
    }
}


更多的物件導向設計

物件導向的設計基礎如下,如果是初學者或是不熟悉的各位,建議可以從這些基礎開始認識,打好基底才能走個更穩(在學習的時候也需要不斷回頭看)!

設計建模 2 大概念- UML 分類、使用

物件導向設計原則 – 6 大原則(一)

物件導向設計原則 – 6 大原則(二)

創建、行為、結構型設計 8 個比較 | 包裝模式 | 最佳實踐

創建模式 Creation Patterns

創建模式 PK

創建模式 - Creation Patterns

結構模式 Structural Patterns

結構模式 PK

結構模式 - Structural Patterns

結構模式專注於「物件之間的組成」,以形成更大的結構。這些模式可以幫助你確保當系統進行擴展或修改時,不會破壞其整體結構。例如,外觀模式、代理模式… 等等,詳細解說請點擊以下連結

Bridge 橋接模式 | 解說實現 | 物件導向設計

Decorate 裝飾模式 | 解說實現 | 物件導向設計

Proxy 代理模式 | 解說實現 | 分析動態代理

Iterator 迭代設計 | 解說實現 | 物件導向設計

Facade 外觀、門面模式 | 解說實現 | 物件導向設計

Adapter 設計模式 | 解說實現 | 物件導向設計

Leave a Comment

Comments

No comments yet. Why don’t you start the discussion?

發表迴響