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

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

Overview of Content

設計類型重點目標標準模式
創建類型創建物件工廠、抽象工廠、Builder、單例、原型模式
行為類型管理物件行為責任鏈、命令、解釋器、迭代、仲介、備忘錄、觀察者、狀態、策略、模板、訪問者模式
結構類型重用架構適配器、橋樑、組合、裝飾、門面、享元、代理模式

以上是軟體的 3 大種類,不過這些種類並不是完全區隔開,它們有重疊點(完全一個業務邏輯可以用不同類型的設計)

本文將探討軟體設計中常見的幾種模式,包括策略、橋樑、門面、仲介和包裝模式。

我們將深入研究每種模式的方案和封裝方式,並比較策略和橋樑模式之間的差異以及最佳實踐。同時,我們將討論門面和仲介模式在共享資源和隔離方面的應用,並比較它們的最佳實踐。

最後,我們將介紹幾種常見的包裝模式,包括代理模式、裝飾模式、適配器模式和橋樑模式,並討論它們在軟體設計中的用途和優缺點。透過本文,讀者將能夠深入了解這些設計模式的運作原理,並學會在實際項目中應用它們。


策略、橋樑

策略橋樑
類型行為設計結構設計

策略模式:策略模式只有一個抽象,它使用界面(或抽象)來達成自由切換算法

橋樑模式:橋樑模式 有兩個抽象,個別抽象分來了資料類、實做類,讓其子類可以自由拓展,並各自維護

它們的 UML 相差也不大,可以看出橋接模式就是將策略模式的環境類(Context)改裝為抽象 & 子類實現

策略之方案

● 策略模式可以很好的封裝一個方案,對外隱藏實做的細節,對外則承諾業務邏輯

封裝原子邏輯

這個封裝是以業務邏輯的角度來封裝,封裝一個不可拆分的最小單元

● 以下實現一個下載方案,我們可以真正的下載,或是假的下載,但目的都是呈現下載的行為

A. 抽象策略定義原子邏輯


abstract class DownloadFile {

    open fun startDownload(url: String, progress: (Int) -> Unit): Boolean {
        return false
    }

}

● 抽象類有實現,幹麻還要寫成抽象?

這裡要提及一個概念、一個考量點

概念抽象是無法被實例化的,它可以定義多個預設方法、參數,但是不可以被使用,可以用來強制使用者使用明確的類創建

考量點:這種複寫的方式並不是里式原則推薦的(但還是可以),它可能會混淆父類最初的設計想法

除非你非常清楚該類的設計概念,不然盡量少複寫

B. 策略實現類:邏輯、算法的實現處


class HttpDownload: DownloadFile() {

    override fun startDownload(url: String, progress: (Int) -> Unit): Boolean {

        println("Start http get...$url")

        for(a in 0..100 step 25) {
            progress.invoke(a)
        }

        println("Finish http get...")

        return true
    }

}

// -----------------------------------------------------------------

class MockDownload: DownloadFile() {

    override fun startDownload(url: String, progress: (Int) -> Unit): Boolean {


        println("Start mock download action...$url")

        for(a in 0..100 step 10) {
            progress.invoke(a)
        }

        println("Finish mock download action...")

        return true
    }

}

C. 環境類:隔離使用者直接呼叫抽象類

在這個環境類中也可以做一些加強


class DownloadActionContext {
    var downloadAction: DownloadFile = HttpDownload()

    fun start(url: String, progress: (Int) -> Unit) {
        downloadAction.startDownload(url, progress)
    }
}

● 使用策略模式設計:由高層模組決定最終算法

● 外部必須知道實際算法的實做類,有點不符合最少知識原則(迪米特)


fun main() {
    DownloadActionContext().apply {
        val progress: (Int) -> Unit = { value ->
            println("Progress: $value")
        }

        start("http://www.testFile", progress)

        downloadAction = MockDownload()

        start("url://assert/myFile", progress)
    }
}

橋樑之封裝方案

● 橋樑模式的重點在於 抽象化資料、行為;行為這個地方我們使用上面完成的下載器(策略模式完成)

這裡我們把原先橋樑設計模式進行對調,改成資料為主,行為是輔助

A. 抽象資料類:這裡我 start 作為調用行為的入口,它也負責檢查使用者傳入的資料

這裡有一點使用到了模板設計的概念


abstract class DownloadServer {

    abstract var downloadFileAction: DownloadFile

    abstract var fileUrl: String

    abstract var progress: (Int) -> Unit

    open fun start(): Boolean {
        return downloadFileAction.startDownload(fileUrl, progress)
    }
}

B. 實做檢查資料類:這裡給予一個預設的行為類,減輕使用者調用 API 負擔


class StorageDownload(override var fileUrl: String, override var progress: (Int) -> Unit) : DownloadServer() {

    override var downloadFileAction: DownloadFile = MockDownload()

    override fun start(): Boolean {
        return super.start().also {
            println("Storage download res: $it")
        }
    }

}

class OnlineDownload(override var fileUrl: String, override var progress: (Int) -> Unit) : DownloadServer() {

    override var downloadFileAction: DownloadFile = HttpDownload()

    override fun start(): Boolean {
        if (!fileUrl.startsWith("http://")) {
            throw IllegalArgumentException()
        }

        return super.start().also {
            println("Online download res: $it")
        }
    }

}

● 使用者使用:這裡高層模組需要決定兩件事,就是資料、行為的實做


fun main() {
    val task = OnlineDownload("http://www.towlg.123") {
        println("progress: $it")
    }.apply {
        start()
    }


    task.apply {
        downloadFileAction = MockDownload()
        start()
    }
}

策略 vs. 橋樑 - 最佳實踐

意圖上分析

● 策略模式:專注於封裝行為(算法)

● 橋樑模式:作為策略模式的包裝,它不破壞策略模式的封裝,將業務邏輯需求抽象,來滿足需求

● 這兩個模式很常混淆,但你可以想成 橋樑包裹了策略,也就橋接模式在 在行為上建構一個結構


門面 & 仲介

門面仲介
類型結構設計行為設計

兩者的相同點是,這兩個設計模式都是高內聚的一種表現

門面模式

A. 門面可以隔離一個複雜的子系統內部細節,不讓外部知道內部完成的業務邏輯的細節

B. 門面中 不會有業務邏輯

仲介模式

A. 仲介模式可以 用在有強耦合關係的業務邏輯中,將其轉為弱耦合

B. 讓業務關係依賴在抽象上,而非細節中


仲介之共享資源

● 仲介模式使用的情況是,一組物件一起操控時會相互影響的時候,也就是其實 這些物件之間都有一個相關連的物件(或是說關係)

● 這有個幾個很好的例子,操作一個有限的資源,而這個資源會相互影響到其他操作...像是 金錢、時間

這裡舉個時間的例子

A. 抽象同事類:它協助子類 訪問限的抽象資源,它算是一個標誌類,內部不會有過多的商業邏輯


// 抽象資源 AbsMediator

abstract class AbsCommon(val mediator: AbsMediator)

B. 各個商業邏輯界面:這些界面會讓與其他相關的商業邏輯交互


interface IStudy {

    fun dailyStudy()

}

interface IPlay {

    fun playOnlineGame()

}

interface ISleep {

    fun rest(time: Int)

}

C. 各個商業邏輯實做:這些商業邏輯實做類需要做兩件事

● 實做各自的商業邏輯界面

● 繼承抽象同事類(AbsCommon)可以透過它來調用其他界面的實做


class AndroidStudy(mediator: AbsMediator) : AbsCommon(mediator), IStudy {

    override fun dailyStudy() {
        if (!super.mediator.costTime("AndroidStudy", 4)) {
            println("No time to study")
            return
        }
        super.mediator.restTime(1)
    }

}


class CatsFight(mediator: AbsMediator) : AbsCommon(mediator), IPlay {

    override fun playOnlineGame() {
        if (!super.mediator.costTime("CatsFight Game", 1)) {
            println("No time to play")
        }

        super.mediator.studyTime()
    }

}

class SleepRest(mediator: AbsMediator) : AbsCommon(mediator), ISleep {

    override fun rest(time: Int) {
        if (!super.mediator.costTime("SleepRest", time)) {
            println("No time to rest")
            return
        }
    }

}

D. 抽象仲介類:仲介類有兩個重點功能

● 對外提供被調用的方法

調用方可能是:上層調用者、或是其他商業邏輯實做類(AbsCommon 的子類)

● 實現各個商業邏輯實做類(AbsCommon 的子類),並 將自身傳入,這個 自身就是一個共享的資源


abstract class AbsMediator {

    protected val study: IStudy = lazy {
        AndroidStudy(this)
    }.value

    protected val play: IPlay = lazy {
        CatsFight(this)
    }.value

    protected val rest: ISleep = lazy {
        SleepRest(this)
    }.value

    var totalHour = 24

    fun costTime(msg: String, costTime: Int): Boolean {
        if (totalHour == 0) {
            return false
        }

        totalHour -= costTime

        if (totalHour < 0) {
            return false
        }

        println("$msg cost time: $costTime, final time: $totalHour\n")
        return true
    }

    abstract fun studyTime()

    abstract fun restTime(time: Int = 1)

    abstract fun playTime()

}

E. 仲介類實做:依照邏輯調用真正實現類

● 這裡有個重點:小心遞歸

仲介類只做傳遞的動作,而不調用與當前無關的子類方法

像是 study 就不該呼叫 reset 的方法,否則可能會造成遞歸行為


class DayMediator: AbsMediator() {
    override fun studyTime() {
        study.dailyStudy()
    }

    override fun restTime(time: Int) {
        rest.rest(time)
    }

    override fun playTime() {
        play.playOnlineGame()
    }
}

門面之隔離

● 門面模式可以 隔離實現子系統時的複雜操作,在移除門面時,仍可以正常操作子系統;範例如下

以下完成一個薪水計算

子系統:先做子系統內的複雜邏輯,類與類之間的關係較複雜;這裡主要分為兩個分類 1. 出勤、2. 薪資計算

A. 出勤相關類


class Attendance {
    fun getWorkDays(): Int{
        return Random.nextInt(30)
    }
}

class Bonus {

    private val atte = Attendance()

    fun getBonus(): Int {
        return atte.getWorkDays() * 100 * 2
    }
}

B. 薪資計算相關類


data class SalaryBean constructor(val salary: Int = 50000)

class Performance {
    private val salaryBean = SalaryBean()

    fun getPerformance(): Int {
        val perf = Random.nextInt(100)
        return  perf * salaryBean.salary / 100
    }
}

class Tex {
    fun getTex(): Int {
        return Random.nextInt(100)
    }
}


class SalaryProvider {
    private val salaryBean = SalaryBean()
    private val bonus = Bonus()
    private val performance = Performance()
    private val tex = Tex()

    fun totalSalary(): Int {

        val totalMoney = salaryBean.salary + bonus.getBonus() + performance.getPerformance()

        return (totalMoney * (1 - tex.getTex() / 100f)).toInt()
    }
}

門面類:再次強調門面類不帶有邏輯、判斷、異常處理,它單純負責創建、使用子系統,並讓外部來調用它提供的方法


class HRFacade {
    private val provider = SalaryProvider()

    private val attendance = Attendance()

    fun getSalary(): Int = provider.totalSalary()

    fun getWorkDays(): Int = attendance.getWorkDays()
}

使用者使用:使用者只須關注門面的方法即可使用,不需要調用子系統實現類、方法


fun main() {

    HRFacade().apply {

        println("Alien info, salary ${getSalary()}, work days: ${getWorkDays()}")

        println("Kyle info, salary ${getSalary()}, work days: ${getWorkDays()}")

    }

}

門面 vs. 仲介:最佳實踐

● 兩種設計模式的特色

門面模式特色

以封裝、隔離為主要任務,門面類不會有任何的校驗、判斷、異常處理... 等等行為

在這裡門面就像是一個硬代理(不讓外部設定代理類)

仲介模式特色

調和各個相關類之間的關係,重在調和,所以仲介類有部份的業務邏輯控制

● 兩種設計模式的差異

功能區別

門面模式仲介模式
對外提供功能界面,對於內部(子系統)完全沒有影響將原先的強耦合關聯類都集中到仲介類做管理,同事(業務邏輯)類無法脫離仲介

從這裡可以看出,仲介模式的耦合關係較重(畢竟它提供給各個同事類操控)

狀態的掌控

門面模式仲介模式
子系統完全不用知道門面的存在仲介類須掌控大局(像是控制資源…等等),並且同事類須依靠仲介類

封裝程度

門面模式仲介模式
簡易的封裝擁有所有的同事類實例,並有控制權做協調的動作

包裝模式

包裝模式是 一組模式,像是:裝飾、適配器、門面、代理、橋樑模式,這些模式的共同特色是,它們的 UML 中都有一個類其實是不做過多事,但純做轉發

我們現在就來分析包裝模式的差異

● 以下我們將以一個工程師為例,使用不同包裝模式來操作,再體會每種包裝模式的不同點

代理模式

● 這裡使用工程師 & PO 之間的關係

任務往往不會直接發配給工程師,而是由 PO 來進行安排,之後再跟工程師討論並安排任務;這時 PO 就作為代理

A. 共同界面:定義代理的行為契約,而這個契約又是由 實做者、代理者 來實做


interface IEngineer {

    fun programing(code: String)

}

B. 代理類身為代理,要做的事情不是真正的履行界面契約,而是做到轉接,在這個過程中,可以做一些而外的管理


class PlatformProjectOwner constructor(val realEngineer: IEngineer): IEngineer {

    private val weekTaskList = mutableListOf<String>()

    private val maxTask = 5

    override fun programing(code: String) {
        
        // 管理是否真正呼叫實做者
        if (weekTaskList.size >= maxTask) {
            println("This week job full.")
            return
        }

        println("Our developer is working now.")

        realEngineer.programing(code)
        
        weekTaskList.add(code)

    }

}

C. 實做類:真正履行契約的實做類


class AndroidEngineer constructor(val name: String): IEngineer {

    override fun programing(code: String) {
        println("$name programing $code now")
    }

}

● 使用代理模式:

會發現每個任務都由代理方來轉接給真正的實做類,並且代理方也有 權力 停下外部的訪問


fun main() {

    PlatformProjectOwner(AndroidEngineer("Alien")).apply {

        for (i in 0..6) {
            programing("App $i")

            println()
        }
    }

}

裝飾模式

● 工程師如何晉身中級、高級、資深工程師?

工程師本身是一個學習能力很強、亦或是很努力的人(偶爾有混吃等死的 XD),藉由各種學習經驗,來累積處理業務的能力,這時就可以使用裝飾

用學習的技能來裝飾工程師

A. 共同界面:定義代理的行為契約,而這個契約又是由 抽象裝飾、裝飾類、基礎類 三者來實做


interface IEngineer {

    fun programing(code: String)

}

B. 基礎類:基礎類也是一個 準備被包裝的目標類


class AndroidEngineer constructor(val name: String): IEngineer {

    override fun programing(code: String) {
        println("$name programing $code now")
    }

}

C. 抽象裝飾類:這裡就是包裝模式的共同點,它只做了一層轉發調用而已

● 這裡接收一個界面類(IEngineer)是裝飾模式的特色,透過手動呼叫來達到迴避多層繼承的作法


abstract class LearnDecorator constructor(val baseEngineer: IEngineer): IEngineer {

    override fun programing(code: String) {
        baseEngineer.programing(code)
    }

}

D. 實際裝飾類:在這裡可以做加強、減弱,原先類的業務能力,但終將會呼叫到實做類(而呼叫時機可由這個類的邏輯控制)


class JavaEngineer constructor(baseEngineer: IEngineer): LearnDecorator(baseEngineer) {

    override fun programing(code: String) {
        println("Learn Java, Kotlin First, base engineer")

        super.programing(code)
    }

}

class KotlinEngineer constructor(baseEngineer: IEngineer): LearnDecorator(baseEngineer) {

    override fun programing(code: String) {
        println("Analyze program performance, senior engineer")

        super.programing(code)
    }

}

● 使用裝飾模式:

可以看到裝飾類會被輪番調用,最終才會到達基礎例(或是說核心類)


fun main() {

    val baseEngineer = AndroidEngineer("Kyle")

    JavaEngineer(KotlinEngineer(baseEngineer)).programing("Bike app")

}

適配器模式

軟體工程師業務的 Cover

軟體工程師在中小企業通常不會只幹一個領域的事情,有時候 Android 開發會去支援 iOS 開發 (甚至前端、後端、韌體...),在這種狀況下就可以使用適配器(Adapter)模式

A. 新業務類:這個類則你準備接收的新任務,要去開發 iOS App


interface IiOSDev {

    fun iOSDev(code: String)

}

class IOSAppDevelop: IiOSDev {


    override fun iOSDev(code: String) {
        println("iOS app: $code")
    }

}

B. 原始業務類:這個類是你原始的業務項目,它有一個 Adapter 可以轉換,將原始的業務邏輯轉換到新的業務邏輯


class AndroidAppDevelop: IAndroidDev {

    private var adapter: IAndroidDev? = null

    override fun androidDev(code: String) 
        // 業務邏輯轉換處
        if (adapter != null) {
            adapter!!.androidDev(code)
            return
        }

        println("Android app: $code")
    }

    fun setAdapter(adapter: IAndroidDev) {
        this.adapter = adapter
    }

}

C. Adapter 類:該類為重點,它 1. 聚合 你的新業務,同時在外部 2. 呼叫時就業務的方法時(androidDev)將其替換為 新業務 方法(iOSDev


class AndroidDevAdapter constructor(private val iiOSDev: IiOSDev): IAndroidDev {

    override fun androidDev(code: String) {
        iiOSDev.iOSDev(code)
    }

}

使用 Adapter 轉換:透過設置 Adapter 就可以轉換其輸出結果


fun main() {

    AndroidAppDevelop().apply {
        // 尚未設置 Adapter 類時
        androidDev("Bike App")

        // 設置 Adapter
        setAdapter(AndroidDevAdapter(IOSAppDevelop()))

        // 設置 Adapter 呼叫原來方法,會做不一樣的事情
        androidDev("Bike App")
    }

}

橋樑模式

軟體工程師寫 Code 的方式

寫 Code 的方式每個人都不同,有人習關規劃完再去寫,有人偏好直接下手寫,有人使用 TDD 模式開發... 等等,各種方式的不同就可以使用橋樑模式

橋樑模式:分離資料、行為

A. 行為界面:該界面定義了寫 Code 的方式


interface ICodingAction {

    fun codeAction(code: String): Int

}

B. 行為實做類:這裡定義每一種不同的開發方式


class TDDStyleAction: ICodingAction {

    override fun codeAction(code: String): Int {
        println("Write Test first: $code")

        println("Write failure case: $code")

        println("Fix failure case: $code")

        println("Pass case: $code")

        return 4
    }

}

class FreeStyleAction: ICodingAction {

    override fun codeAction(code: String): Int {
        println("Thinking...: $code")

        println("Write code: $code")

        return 2
    }

}

C. 抽象資料類:這個類中依賴於行為的界面,並且也同時設定抽象資料的函數


abstract class EngineerCoding(val action: ICodingAction) {

    fun write() {
        action.codeAction(targetApp())
    }

    // 抽象資料的函數
    abstract fun targetApp(): String

}

D. 實做資料類:子類中定義了該類所需的行為、資料


class JuniorEngineer: EngineerCoding(FreeStyleAction()) {

    override fun targetApp(): String = "Web App"

}

class SeniorEngineer: EngineerCoding(TDDStyleAction()) {

    override fun targetApp(): String = "Mix App"

}

使用 Bridge 模式


fun main() {

    fun startCoding(engineer: EngineerCoding) {
        engineer.write()

        println()
    }

    startCoding(JuniorEngineer())

    startCoding(SeniorEngineer())

}


更多的物件導向設計

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

設計建模 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?

發表迴響