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())
}
更多的物件導向設計
物件導向的設計基礎如下,如果是初學者或是不熟悉的各位,建議可以從這些基礎開始認識,打好基底才能走個更穩(在學習的時候也需要不斷回頭看)!
創建模式: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 迭代設計 | 解說實現 | 物件導向設計