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")
}
}
}
更多的物件導向設計
物件導向的設計基礎如下,如果是初學者或是不熟悉的各位,建議可以從這些基礎開始認識,打好基底才能走個更穩(在學習的時候也需要不斷回頭看)!
創建模式 Creation Patterns
● 創建模式 PK
● 創建模式 - Creation Patterns
:
創建模式用於「物件的創建」,它關注於如何更靈活、更有效地創建物件。這些模式可以隱藏創建物件的細節,並提供創建物件的機制,例如單例模式、工廠模式… 等等,詳細解說請點擊以下連結
● Singleton 單例模式 | 解說實現 | Android Framework Context Service
● Abstract Factory 設計模式 | 實現解說 | Android MediaPlayer
● Factory 工廠方法模式 | 解說實現 | Java 集合設計
● Builder 建構者模式 | 實現與解說 | Android Framwrok Dialog 視窗
● Clone 原型模式 | 解說實現 | Android Framework Intent
行為模式 Behavioral Patterns
● 行為模式 PK
● 行為模式 - Behavioral Patterns
:
行為模式關注物件之間的「通信」和「職責分配」。它們描述了一系列物件如何協作,以完成特定任務。這些模式專注於改進物件之間的通信,從而提高系統的靈活性。例如,策略模式、觀察者模式… 等等,詳細解說請點擊以下連結
● Stragety 策略模式 | 解說實現 | Android Framework 動畫
● Interpreter 解譯器模式 | 解說實現 | Android Framework PackageManagerService
● Chain 責任鏈模式 | 解說實現 | Android Framework View 事件傳遞
● Specification 規格模式 | 解說實現 | Query 語句實做
● Command 命令、Servant 雇工模式 | 實現與解說 | 物件導向設計
● Memo 備忘錄模式 | 實現與解說 | Android Framwrok Activity 保存
● Visitor 設計模式 | 實現與解說 | 物件導向設計
● Template 設計模式 | 實現與解說 | 物件導向設計
● Mediator 模式設計 | 實現與解說 | 物件導向設計
● Composite 組合模式 | 實現與解說 | 物件導向設計
● Observer 觀察者模式 | JDK Observer | Android Framework Listview
結構模式 Structural Patterns
● 結構模式 PK
● 結構模式 - Structural Patterns
:
結構模式專注於「物件之間的組成」,以形成更大的結構。這些模式可以幫助你確保當系統進行擴展或修改時,不會破壞其整體結構。例如,外觀模式、代理模式… 等等,詳細解說請點擊以下連結
● Decorate 裝飾模式 | 解說實現 | 物件導向設計
● Iterator 迭代設計 | 解說實現 | 物件導向設計