軟體設計(成大)
ch2 OOP
part 1. Encapsulation
封裝 Encapsulation
將程式切割成一塊塊模組
降低程式的耦合度、增加可控度
避免被修改的風險
多型 Polymorphism
Method Overloading
在同個 calss,方法名一樣,相同或高相似度的行為,但不同實作方式的同名函式。
增加可讀性
part 2. Call by Reference
ToyClass sampleVariable = new ToyClass("JS", 42)
Image Not Showing
Possible Reasons
- The image was uploaded to a note which you don't have access to
- The note which the image was originally uploaded to has been deleted
Learn More →
argument value: 50
parameter value: 100
argument value: 50 (Call by Value)
Robot Dog 10
Robot Cat 20 (Call by Reference)
part 3. 繼承 Inheritance
讓子類別能夠使用父類別的行為、屬性跟方法,
也可以藉由抽象類別跟介面,先定義規格,而在不同的子類別做出不一樣的實作方式。
properties = member
蘋果繼承水果
鋼筆繼承筆
是 super 跟 Derived Class (Subclass) 的關係
overriding
不同class,同名方法,父親的行為能被兒子使用,但實作可以不同。
overloading 則是同個 class。
part 4. 抽象類別 Abstract Class & 介面 Interface
Abstract Class
抽象,未被實作的,實作留給相關類去實作。
用以定義規格或api,通常在父親。
必讓兒子去實作run(),在下面層級能讓其做不同的行為
Interface
Abstract Classes can Implementing Interfaces
part 5. 多型 Polymorphism
一個抽象行為有不同的實作
Early binding (through overriding)
compiler time 時就決定,靜態決定。
Late binding (through overriding)
run time時才決定,動態決定。
會往上尋找實作
- checkout() 只有被父親 Payment() 實作
- 父親裡的 pay() 有被兒子 CreditCardPayment() 所實作
- 所以最後是 call 兒子的 pay()
得到 "Pay with credit card"
Upcasting and Downcasting
Upcasting
Downcasting
ch3 UML Class Diagram
Class Notation

Abstract and Interface
Class name and Method 為斜體字


Relationship
Generalization 概括(抽象化)
是否有 is-a 的關係?
為 class 跟 Interface/Abstract class 的差別



Dependency 依賴
是否有 uses-a 的關係?
通常為 Method 的 Parameters 或 Local Variable
虛線:關係較薄弱


Association
是否有 has-a 的關係?
實線:關係跟狀態比較穩固跟強烈


Bidirectional Association 雙向關係
無箭頭直線
缺點:難以加入新的元素,如學生的課程分數

Association Class 關聯類別
改善方式:用新的 class 重構

Aggregation 聚合
一個類別「包含/擁有」另一個類別
為「較弱」的聚合(Whole-Part)關係
whole消失,part可繼續存在
使用 空菱形
如:即使班級不存在,學生也能在其他班級中繼續存在。
如:餐廳有許多顧客,但餐廳倒了,顧客可以去其他餐廳。
如:電腦有cpu,電腦壞了,cpu可以拿到其他電腦使用。


但兩者實作方式一樣
Composition
為較「強烈」「包含/擁有」的關係
whole消失,part不可繼續存在
使用 實心菱形


- 封裝在裡面
- 放在外面,確保其他物件不持有 part 物件的 Reference
- WeakReference
Tips
- code 不能完全實現 UML
- UML工具 才能實現一樣的 code

無法產生菱形箭頭,實作一樣
例題
- A country has a capital city.

- A dining philosopher is using a fork.

- A file is an ordinary file or a directory file.

- Files contain(包含) records.

- A polygon is composed of points.

- A drawing object is a text, a geometrical object, or a group.

總結
Inheritance(繼承) |
Implementation(實作) |
class |
Interface/Abstract |
is-a |
is-a |
三角形實線 |
三角形虛線 |
 |
 |
Dependency(依賴) |
Association(關聯) |
Parameters 或 Local Variable |
被設為Attribute |
uses-a 短期使用(臨時) |
has-a 長期擁有(結構) |
箭頭虛線 |
箭頭實線 |
 |
 |
Bidirectional Association(雙向關聯) |
Aggregation(聚合) |
Composition |
互相被設為Attribute |
whole消失 part不消失 |
whole消失 part也消失 |
互相擁有 |
弱引用(擁有) |
強引用(擁有) |
實線無箭頭 |
實線空心菱形 |
實線實心菱形 |
 |
 |
 |
ch3 Code Structure View via UML Class Diagram
- Legacy Code 理解既有程式碼
- Trace code 逆向工程過程:透過 zoom in/out 理解code
- Structure (靜態結構) ➔ 地圖
- Behavior (動態行為) ➔ 路徑
Class Diagram三步驟
- 設定New Class Diagram
add dependency

- 更新遺漏的Dependency
修正如下【Ctrl + a 全選】➔【在任一class上右鍵】➔【Add】➔【Dependencies】
- 更新Layout
【畫面右鍵】➔【Layout Diagram】
Levels of Abstraction in Java Code Structure

Package Level

- Cyclic dependency:問題
- Unidirectional dependency:理想
資訊減量
只看重要的部分

Class Level(Intra-Package)
- 關注依賴於高階抽象(interfaces、abstract classes)或低階實體(sub-classes)
- 關注bidirectional dependencies

資訊減量
- 可隱藏核心外如data classes、utility classes、exception classes、enums、composition root、UI-layer classes
- 可隱藏dependencies,只顯示實線(inheritance,implementation與association)

- Isolated classes (獨立的classes)
- 重複的association lines
- composition root

何謂Composition Root
- 負責初始化和組合應用程式中所有相互依賴物件的類別
A Composition Root is a single, logical location in an
application where modules are composed together.
- Close to the application’s entry point
- Takes care of composing object graphs of loosely coupled classes.
- Takes a direct dependency on all modules in the system.
Composition Root很混亂,可以先移除整理後再加入

Class Level (Cross-Package)

資訊減量



- 直接將大量所有package中的class關係結構全部一起視
覺化會相當困難,遊走(zoom in/out)於levels of
abstraction是個較可行的做法
- 透過各個level的觀察重點與資訊減量有助於理解code
structure
- 視覺化後的code structure有時顯得複雜,但不代表就是
不好的結構設計,需要搭配design principle進行評估與
權衡
- 請注意,其他code structure visualization工具可能會與
ObjectAid有差異,甚至有些程式語言的逆向工程工具
不是產生UML Class Diagram,但都值得進一步了解
ch4 Code Smells
不好的味道:不好的程式碼
Code Smells
Unresolved warnings
有 warning
比如:使用被棄用的code、null pointer

Memory Leak
memory deallocated
佔用記憶體空間
可能 out of memory

Long method
沒有標準:一個畫面的長度、參考行數、程式語意跟 Method name 是否吻合
問題:
- 不易讀、難理解
- 不易命名(方法的語意)
- 不易reuse
方法:用 Extract Method 抽出來成短 method

Feature Envy
大量使用其他(自己以外) class 的變數、內容

Unsuitable naming
不適當命名

Downcasting
向下轉型


但有不得不的狀況:
- api 不給動
- 舊程式
- Deserialization
Loop termination conditions are obvious and invariably achievable
結束條件不明顯

Parentheses are used to avoid ambiguity
不明確的括號,造成問題或不易讀

缺少註解:不易讀
- 結論:有需要再加,不需要就不用
- 避免 comments 跟 code 不一致,修改 code 但 comments 沒有更新
Files are checked for existence before attempting to access them
讀檔要確認有正確載入
Duplicated Code
重複的 code

Access modifiers
All methods have appropriate access modifiers and return types
適當的存取和返回

Redundant or Unused variables
沒用到、沒用的變數
Indexes or subscripts are properly initialized, just prior to the loop
沒有初始化或沒有宣告值

Is overflow or underflow?
數字是否超過型態的範圍
注意大數字

Are divisors tested for zero?
分母為'0',除法時要做判斷
Inconsistent coding standard
應符合程式風格

Data clumps

重複的 data 變數可以抽出class

Simulated Polymorphism

使用時會有分類、擴展時再使用:
比如有新的動物,減少去修改既有的class

Large class
沒有 Single Responsibility Principle (SRP)

能根據語意再次拆分,使用者/設定/log/檔案處理。
Long parameter list
參數多,代表參數可能也可以分群

Message Chains

如果一個區塊改變,那後續交錯或串聯的都會受影響,不好維護。

重構1:新增Method
不跟陌生人講話,透過窗口對話。
Client only talks to Company(Demeter’s Law)



違反RSP
重構2:新增中介Class(Façade Pattern)
新增一個窗口或API(CompanyService)幫我呼叫
讓這個窗口符合它的SRP,就是為了幫我呼叫的任務


總結

Literal constants
常數應被替代,增加可讀性

uncalled or unneeded procedures or any unreachable code
不需要存在

switch statement have a default

comparing floating-point numbers for equality

Divergent Change(發散式改變)(*)
一個類別會因為要因應太多的變更原因而需修改
方法:利用 Extract Class 重構
- 範例:

- 重構:

符合SRP(是否同一個Class中的Methods相互依賴或共用屬性)
舉例:
會因為編碼方式、傳輸方式、解碼方式多種原因需要修改該class
Shotgun Surgery(*)散彈式修改
- 每次為了因應一種變更,你必須同時在許多類別上做出許多修改。
- 當有太多需修改的地方時,將造成難以尋找所有需修改處,並容易遺漏。
- 常發生於Copy and Paste Programming

- 範例:
更改主題,要手動傳入新的主題設定,黑底要白字,白字要黑底,它們不會一起連動。
改善後
- 舉例(按鈕):
原本要修改所有按鈕的顏色要一個一個修改
這裡我可以一次修改所有按鈕顏色
Primitive Obsession
- 堅持用基本型態表達
- Loss of Type Safety 容易犯錯
- Lack of Encapsulated Behavior 沒辦法表示行為
將String PostalCode 改為 object
就能有行為去做如 check 的動作
- Replacing Primitives with(Value) Objects

- 舉例:
改善後:
Operation Class 行為物件
一般情況,class name應該要為名詞。
- Operation Class的Class Name通常為動詞(CreateReport),而非物件名詞(Report)
- 通常一個Class包含只有一個Method
- 由於Class Name已經限制了語意,因此很難再擴充Method,造成須相對創建了許多Class
- 由於Class Name為功能特性思維去命名,因此較難以物件導向思維去創建繼承關係以及動態多型的優勢
如果為動詞,通常只能做一個行為,那就會建立許多class來完成不同任務。
範例:

重構:

Alternative Classes with Different Interfaces
實現了類似的功能,但在不同的介面或是不同的實作
範例:

重構:


Refused Bequest
The unneeded methods may simply go unused or be redefined and give off exceptions.
兒子繼承父親,但不要父親全部的 methods
為什麼兒子不想繼承父親,可能要修改 method 或是拆解,使其合理化。
範例:

重構:

- 舉例:
飛行器中有翅膀的屬性,雖然直升機也是飛行器但它沒有翅膀。
改善後:
Parallel Inheritances Hierarchies 平行繼承
兩棵樹,如果一邊要增加,另一邊也要跟著增加。
問題:無法滿足兩個樹底下的物件互相有特定配對依賴關係的要求。
車子 搭配 駕駛員
飛機 搭配 飛行員
導致其有特定的配對

Defer Identification of State Variables Pattern
- 第一步(屬性降階層):將Vehicle的operator屬性移除,並在Car與Plane中各別加入欲配對的屬性型態
- 第二步(加Abstract Accessor):在Vehicle中加入getOperator (稱之為Abstract Accessor)讓Car與Plane實作,以達成維持原本Vehicle與Operator的關係

Middle Man
多餘的,沒有功能的中間人,只是在傳遞事情。
那個中間人同時也是 Feature Envy

Speculative Generality
過分假設未來的情況,預留空間導致程式很複雜,overdesign。

ch5 Design Principles
SOLID原則
Single Responsibility Principle(SRP)
- A module should have one, and only one, reason to change.
- A module should be responsible to one, and only one, actor.
不同使用者,就必須要改變它的作法。

具體判定法
- 結構內聚力判定法
Method間的結構關係
- 語意結合判定法
Class Name與Method Name間的結合語意
結構內聚力判定法
- they both access the same class-level variable, or
兩個 Method 都共用 variable 或 attribute
- A calls B, or B calls A.
- 精神:將一個class內不互相依賴的method群拆解出去
- 範例:例如一個class中有method A, B, C, D, E,關係如下,因此可參考是否判定為兩個responsibility,進而拆分為兩個Class

- 重構範例:

把相互依賴的,經過拆解分群,以增加內聚力:

語意結合判定法
- 用語意判定是否合理
- 依每個method填入下表,構成語句:The Automobile (主詞) starts (動詞) itself.
- 判定此句子是否具合理語意,若合理則留下,若不合理則考慮將此method移出此class


- 拆分以符合SRP

總結:兩者判定法的限制
- 當一個class內method間結構關係複雜時,結構內聚力判定法可能較困難
- 當一個class name語意太general時(如XXXManager/Controller),會讓所有method name都可與class name語意結合,造成語意結合判定失效
Open-Close Principle(開放關閉原則)OCP
- Open for extension, but closed for modification
一個模組必須有彈性的開放往後的擴充,並且避免因為修改而影響到使用此模組的程式碼。

不能有封閉迴路circle

- 舉例
根據不同的員工類型計算薪資
會因為增加不同的員工類型而受改變
改善後:
Liskov Substitution Principle(LSP)
T是父親S是兒子,兒子可以取代父親

Interface Segregation Principle(ISP)
將大型接口分割成多個更專門的小接口,使得類別只需實作自己真正需要的接口。

窗口在interface介面上

改善後:
Dependency Inversion Principle(依賴反向原則)(DIP)(*)
- 軟體設計的程序開始於簡單高層次的概念(Conceptual),慢慢的增
加細節和特性,使得越來越複雜
- Dependency Inversion Principle (依賴反向原則)
- 高階模組不應該依賴低階模組,兩者必須依賴抽象(即抽象層)。
越低層次被變動的機率越高,所以要降低高階依賴低階的情況。

介入一層中間層(介面或抽象層):

利用 Dependency Injection 的概念。
Encapsulate what varies(封裝改變)
-
將易改變之程式碼部份封裝起來,以後若需修改或擴充這些部份時,能避免不影響到其他不易改變的部份。
-
換言之,將潛在可能改變的部份隱藏在一個介面(Interface)之後,並成為一個實作(Implementation),爾後當此實作部份改變時,參考到此介面的其他程式碼部份將不需更改。


-
舉例
攻擊寫在角色的類別中,假設有不同的角色,那麼攻擊行為就必須被擴充或修改
當需要新增或修改攻擊行為時,可以直接創建新的策略類別,而不需要更改 Character 類別,這樣符合開放-封閉原則(Open-Close Principle)OCP
Favor composition over inheritance(善用合成取代繼承)
- 程式碼重用(Reuse)
- 不要一味的使用繼承,要有IS-A的關係
- 有別於繼承,Composition可在Runtime時更有彈性地動態新增或移除功能
Least Knowledge Principle(最小知識原則)
- 必須注意類別的數量,並且避免製造出太多類別之間的耦合關係。
- 知道子系統中的元件越少越好
- 不需要懂太多細節

Acyclic Dependencies Principle (ADP)


難以符合OCP
Don’t Repeat Yourself (DRY)
Keep It Simple Stupid(KISS)
- 簡潔是軟體系統設計的重要目標,應避免引入不必要的複雜性
- 考慮是否 over design
ch6 Design Patterns
Strategy Pattern
關鍵字:An algorithm
Requirements Statement
範例:文字編輯器
按照需求去增加:a new layout is required

重構:
- Encapsulate what varies
會改變的做封裝處理,抽出

- Generalize common features
建樹

- Program to an interface, not an implementation
使用樹的父親,對口interface,方便擴充跟修改

Recurrent Problems
需要增加新的 algorithms 的問題,就拉出來封裝
結構:

Composite & Decorator
關鍵字:Structure and composition of an object
面對的問題是由 object 組成
Requirements Statement
範例:小畫家
如果要畫三角形,就有新的問題

重構:
- Generalize common features
建樹

- Program to an interface, not an implementation.
使用父親


Recurrent Problem

Decorator Pattern
關鍵字:Responsibilities of an object without subclassing
動態地為一個物件添加行為或職責,而不需要透過繼承來實現。
Requirements Statement
範例:Starbuzz Coffee
cost 需要因應新服務而改變,變得不穩定

重構:
- Encapsulate what varies

- Generalize common features
概念是讓 condiment 也是一種食物配料

- Program to an interface, not an implementation.

裝飾它

Recurrent Problem
被裝飾放左邊
右邊的裝飾品用來擴充

Composite vs. Decorator

Factory Method & Abstract Factory
關鍵字:Subclass of object that is instantiated
Requirements Statement
範例:披薩店

新增新的披薩會有問題

重構:
- Encapsulate what varies

- Generalize common features

左樹:生產披薩
右樹:被生產的物件

Recurrent Problem
工廠生產產品,新增產品的生產步驟

補充:Parallel Inheritances Hierarchies問題
產品跟生產方式可能有配對的關係

能夠讓兒子去強制配對

Abstract Factory Pattern
關鍵字:Families of product objects
Requirements Statement
範例:佈景主題

重構:
- Encapsulate what varies

- Generalize common features

- Program to an interface, not an implementation.
生產一堆東西,抽出成工廠,限制其必須要實作

Recurrent Problem

Abstract Factory vs. Factory Method
- Factory Method
- Abstract Factory
- consists of multiple factory methods
- each factory method creates a related or dependent product
Template Method
關鍵字:Steps of an algorithm
動作是否有雷同或一樣的地方,如煮咖啡、泡茶
Requirements Statement
範例:煮咖啡、泡茶
1,3步驟一樣

重構:
- Encapsulate what varies

- Generalize common features

Recurrent Problem

Adapter
關鍵字:Interface to an object
Interface 被改變了
Requirements Statement
範例:
New Vendor in Existing Software
範例1:Object Adapter

更改API

重構:
- Encapsulate what varies

- Generalize common features
兒子去綁定不同的API

範例2:Class Adapter

重構:
- Encapsulate what varies

- Generalize common features

Recurrent Problem
- Object Adapter

- Class Adapter

- Object Adapter vs. Class Adapter

State
關鍵字:states of an object
什麼狀態做什麼事,討論狀態的變化
Requirements Statement
範例:口香糖機器

需要考慮到狀態的變化:投錢、退錢、售完、按下按鈕等等


重構:
- Encapsulate what varies
將狀態抽出

- Generalize common features
每個class負責一個狀態,該狀態應該怎麼做

- Program to an interface, not an implementation

Recurrent Problem

Visitor
關鍵字:Operations that can be applied to objects without changing their classes
可以動態加入一群物件的行為
Requirements Statement
範例:Compiler and AST

父親擴充了新的方法,兒子也要增加
重構:
- Encapsulate what varies

分成一個class,但兩個不同的方法
- Generalize common features

- Program to an interface, not an implementation

Recurrent Problem

Node 如果是病人的種類:兒童、成年人、老年人
Visitor 如果是醫生的種類:骨科、耳鼻喉科、內科
醫生根據不同病人做檢查跟觀察狀態,讓醫生作為一個Visitor檢查不同種類人的身體狀態。
ch7 Unit Testing
單元測試是什麼
- Michael Feature
- 小的、快的,快速定位問題所在:0.1秒內
- 不是單元測試
- 與資料庫有互動
- 進行了網路通訊
- 接觸到檔案系統
- 需要你對環境做特定的準備(如編輯設定檔案)才能夠執行
- Roy Osherove
- 一個單元測試是一段自動化的程式碼,這段程式會呼叫被測試的工作單元,之後對這個單元的單一最終結果的某些假設或期望進行驗證
- 一個單元測試範圍,可以小到一個方法(Method),大到實現某個功能的多個類別與函數
- 測試某個工作單元,其範圍大小不限於一對一的單元測試與方法
何時撰寫測試案例
- 程式開發前
- For TDD (Test-Driven Development)
- 程式開發後
- 程式變成Legacy Code時
- For Refactoring
- 已經寫好,可以運作的程式,但有一些氣味,需要被重構的
- 確保重構後,還能保持功能正常運行
- 書本

Refactor untestable code to testable code
Untestable Code
- 當被測試的物件依賴於另一個無法控制(或尚未實作)的物件時,要造成無法進行單元測試
- 例如依賴於一個Web Service、系統時間、執行緒、資料庫、本地檔案等
- 此時可利用Stub概念來重構解耦 (Refactor untestable code to testable code)


- 本來需要登入後才能驗證,利用重構,新增假資料(假裝有登入)進行測試。
- 可以讓它只為了一項工作進行單元驗證
- 如果加入登入的動作,首先速度慢,再來是增加為多個工作,那就變成整合測試,而非單元測試
解方
Steps:
- Extract Interface as Seam

- Create Stub Class

- Program to an interface, not an implementation

- Dependency Injection

- 結果

- 利用 ILoginManager 作為依賴注入的物件,只要
new StubLoginManager()
作為假資料就變得可以測試。

Tip (Stub vs. Mock)

- 因此,單元測試類型除了
- 運用Mock即可增加一種測試類型
clean code
強調可讀性
Meaningful Names
Use Intention-Revealing Names
Choosing good names takes time but saves more than it takes
命名要有意義

容易誤會、過長、太一般性的名稱
Make Meaningful Distinctions
盡量使用 source and destination
是否是一樣的?
Use Pronounceable Names
好發音的
應改成
Use Searchable Names
容易被搜尋
應改成
Member Prefixes
多餘的
應改成
Don’t Be Cute
不使用俚語或是幽默性的命名
HolyHandGrenade => DeleteItems
Pick One Word per Concept
意思一樣,應統一用一樣的詞
比如雷同的 fetch, retrieve,and get
統一用 get
或是 DeviceManager and a Protocol-Controller
Functions
Small
程式碼短小,看起來要 "eye-full"
One Level of Abstraction per Function
分層次,抽象地去理解系統
能夠用適當的命名以及function去包裝功能。
function 可以多也可以短
Reading Code from Top to Bottom: The Stepdown Rule
盡可能將被呼叫的擺在呼叫人的附近
Prefer Exceptions to Returning Error Codes
利用 try exceptions 除錯

try exceptions 的內容不要太長
How Do You Write Functions Like This?
程式很難一開始就乾淨
不好的code
重構 > 補充註解
Explain Yourself in Code
用 function 的名稱替代多餘的註解

不得不去解釋

Explanation of Intent
解釋動機、當初的決策 WHY TO DO

Clarification
無法修改,但概念不清楚或模糊的

Warning of Consequences
解釋後果

該去寫,但還未寫
Mumbling
讀完註解,但還是不清楚

不需要去解釋,冗餘的註解

除非外部需要,否則內部使用不用加


如果你可以用code直接表達清楚,使用重構,就不需要註解

被註解的code,可以刪除就刪除掉。
註解放的跟 code 太遠了
長度

Vertical Openness Between Concepts
斷空白行

Vertical Density
內容的密度高

Vertical Distance
呼叫跳來跳去
Variable Declarations
Variables should be declared as close to their usage as possible.
Dependent Functions
互相呼叫的如果可以就放在一起
Conceptual Affinity
概念一樣就放在一起
Horizontal Openness and Density
水平的空白,方便閱讀

Horizontal Alignment
水平對齊
應使用下方的code,讓type跟name較接近
也方便新增跟修改

Breaking Indentation
scopes 應隔開
應使用下方的code

Objects and Data Structures
Data/Object Anti-Symmetry
不一定都要化為 Object 或切成多型。
Data Abstraction
抽象化,不需要接露實作細節

Data Structure
如果不會再新增了,就化繁為簡,不需要另外建樹了。

Clean Architecture
WHAT IS DESIGN AND ARCHITECTURE?
- 定義:
- 架構:通常指系統的高層結構。
- 設計:通常指較細節的部分。
但實際上,兩者是連續的,沒有明確的分界線,從高層到細節都是一體的,彼此密不可分。
- 目標:
減少建構和維護系統所需的人力和成本。
- 設計和架構是一個連續的過程,好的架構設計可以降低開發和維護的成本,讓系統更易於管理和擴展。
- ARCHITECTURE
- software
- soft: 軟的,易修改、有彈性的。
- ware: 產品。
- 架構大於功能:容易改變與修改,比能用更重要。
- 功能:往往會需要更高的成本、壓力。
- 功能通常緊急但不重要。
- 架構通常重要但不緊急,這才是重點。
Design principles
How to arrange the bricks into walls and room
如何將磚塊排列成牆壁和房間
Component principles
How to arrange the rooms intobuilding
如何將房間佈置成建築物
- Componebts
- COMPONENT COHESION 內聚力(裡面)
- REP: 元件必須有明確的發布流程和版本管理,才能確保被有效且可靠地重用。
- CCP: 把一起改動的類別放在同一個元件裡,把不同原因改動的類別分開,避免無謂的影響擴散(SRP的延伸)。
- CRP: 把經常一起使用的類別放在同一個元件中,避免把不相關的類別放在一起,減少不必要的依賴關係,內聚力就高。
- COMPONENT COUPLING (外面)
- 不要循環依賴
- DIP 依賴反轉原則: 元件之間應透過抽象進行依賴,而不是直接依賴具體實現,從而降低耦合性並提高系統的可擴展性和穩定性。
The Clean Architecture
- Architecture 就是畫線:畫出邊界跟區塊
- Clean Architecture:獨立且易於維護的軟體系統,將不同部分進行明確的分層,讓系統具備靈活性和可測試性。
- 獨立於框架
- 可測試姓
- 獨立於UI
- 獨立於資料庫
- 獨立於外部機制
- 讓其完全解耦,達到靈活性、可測試性和可維護性。
- 外到內進行依賴
演講課 - Domain-Driven Design
Domain-Driven的概念
- 對某個區域有控制跟興趣
- 理解需求,找到問題,提出策略
- Domain-Driven rather then Data-Driven
我認為DD更注重在了解業務的背景與需求,所以提出的方法跟策略才趨近於實際的情況。這也比起以往工程師著重在程式或是資料層面上更為彈性,這些技術應該是服務於業務的工具,而非終點。
DD我覺得就像是建構了一種語言,讓Code更容易被理解跟詮釋,不管是透過圖像化還是流程圖,這讓工程師與開發團隊之間更好溝通,更甚至在缺乏技術的客戶之間,不會因為術語不統一而帶來誤解,是提升溝通效率的好方式。
Reverse design
- Data-Driven -> Domain-Driven
- Program -> System
- Functional -> Scenario
改變聚焦的問題點:適度依賴數據,但不要被其框架束縛,這種方式不僅僅是改進技術,而是整個想法的轉變。重點應在於需求與業務本身,不應該去迎合資料而做修改。
Program是分散且缺乏交流:過去的問題常常過於片面,每個模組的邏輯只看資料、功能或程序,但並不應該侷限在如何實現,而讓我們針對實際需求以及全局的角度去做設計,才能確保系統邏輯的完整性,並且有彈性地去應對業務的變化。
透過 Scenario Mapping 讓人與人之間有了共通的語言:這是一種橋梁,能幫助技術團隊與業務團隊打破溝通障礙。
Object in Domain
Object
- Identity: 可以被 identity(唯一的)
- Operations: 可以被操作(被打開、被使用)
- Attributes, State: 擁有屬性跟狀態
Class
- 就是一個集合
- Responsibility: 單一職責
- Operations: 可以被操作
這裡提到的概念,我認為是 OOP 程式設計與 DDD 之間的配合。OOP 提供了物件、類別這些實作方式,而 DDD 在這些基礎上去賦予某種目標或使命。透過 Domain 的語義化與模型化,我會知道該如何設計以及設計哪些類別來符合實際的需求,讓軟體系統是符合現實的邏輯去進行與設計。
Domain Storytelling:以實際案例演示情境
- Building Blocks: 分類成員的屬性
- Good language styles: 圖像化、便條紙、情境圖、流動圖
- Something about Principles: 沒有條件、who, what, whom
Domain-Driven Principle
- Collaboration
- Ubiquitous Language
- Domain modeling -> Domain Storytelling
Domain-Driven Design
- Strategic Design 戰略設計
- Problem Space
- Domain, Subdomain: 從流程,將領域以Logic分成幾個 Domain
Image Not Showing
Possible Reasons
- The image was uploaded to a note which you don't have access to
- The note which the image was originally uploaded to has been deleted
Learn More →
- Bounded context: 在程序中的 value stream
- Context Mapping: Mapping value stream
- Tactical Design 戰術設計
- solution space
- 組織細節
- 依照商業流程做架構分層,而非功能
- 辨識價值流中的價值物件,而非資料
- Entities: identity(唯一識別), state(生命週期,期間發生改變), operation(角色職責) -> 銀行發行
- Value Objects: no identity(固定值), attributes(不可變), operation(操作方法) -> 信用卡
- Aggregate
- Aggregate Root(群組基礎)
- Entity base(識別群組)
- Persistence base(一致性持久化)
- operation base(提供統一職責與操作)
- Lift base(狀態改變生命週期)
- Boundary
Services
- Application(商業流)
- domain(商業邏輯)
- stateless
- domain-specific task
- out of place as a method
透過不同的演示方法,包括圖像化、Domain化、分層、價值流的概念等等,將複雜的業務邏輯轉為情境或是故事作呈現。我覺得對於設計一個「系統」有很大的幫助,讓我們能直觀地將這些需求轉為程式碼或是一個個模組,引導我們去設計相對完整的架構和流程,就像課堂常常提到的 Design Principles。
當我們按照DDD的這些流程去設計系統時,每個類別都能對應到一個職責或需求,自然就能符合SRP;當我們按照需求去設計程式邏輯時,可以因應變動去做調整跟擴充,自然就能符合OCP。所以我認為DDD也有助於我們去設計一個「好的系統」,除了能符合業務需求外,還能有好維護、好擴充、有條理的特點。
演講課 - TDD 與重構工作坊
工作
FizzBuzz遊戲
把工作做對、做好
還要做「對的工作」
努力在對的方向
TDD
每件事都是對的事
每個功能都是正確
每個優化都不會「錯」
一次只做一件事
Test first:先寫測試

寫測試 寫程式 重構
符合 SOLID 原則
有效的測試
修改設計的需求 再去修改設計
專注於需求的修改
透過寫好的測試 保護重構後程式的完整性
先列測項 跟客戶確認好需求
而不是撰寫時再思考需求。
TDD每一步的大小
釐清需求 用測試來記錄下 來告訴程式的正確與否
心得
在該堂課中,我們進行了 FizzBuzz 遊戲 的實作,這是一個經典且簡單的練習,但課堂中最大的收穫不只是完成遊戲本身,而是模擬了面對客戶需求變更時 的情境,讓我學習到如何在有限的時間內保持程式的彈性並快速回應變化。
一開始,我們按照最基本的需求實作 FizzBuzz,例如輸出 1 到 100 的數字,其中 3 的倍數輸出 "Fizz",5 的倍數輸出 "Buzz",同時是 3 和 5 的倍數時輸出 "FizzBuzz"。然而在後續的過程中,老師(或模擬的客戶)陸續提出了新的需求,例如:
這樣的變更讓我體會到,實務中不可能所有需求一開始就明確,客戶往往會根據他們看到的結果提出新的想法,這也導致說一個系統的設計,必須寫得有"彈性"並且易於"理解"與修改,才能在有限的時間內,去滿足客戶提出的需求。
關於遊戲實作的想法
遊戲實作的問題
原本的程式,是按照直覺以及需求的順序,一一將規則寫入其中,這樣的程式雖然可以正確地運行,但存在一些問題包括:
- 難以修改跟擴充
每當新增或修改一個規則時,都必須重新編寫 if-else 邏輯。
- 重複邏輯
程式碼變得冗長且不易理解。i % 3 == 0、i % 5 == 0、i % 7 == 0
,不但具備重複的程式邏輯,還使用了魔術數字。
- 違反SRP 和 OCP 原則
- SRP:convert 負責了兩件事情,包括遍歷數字與判斷輸出的內容。
- OCP:面對新規則(新需求)時,需要修改現有邏輯。
實作如何改善
改善後的地方:
- SRP:讓每個方法負責一件工作。
Rule
負責遊戲規則的制定
convert
負責輸出 1 ~ number 的 FizzBuzz 結果
- OCP:易於修改與擴增
- 有新規則(需求)時,只要添加到
createDefaultRules
方法中即可:
這樣的設計符合 SOLID 原則,還讓程式碼能夠應對新的需求跟變化,更符合實際工作的情況。
關於單元測試的想法
原本的單元測試,需要人工將規則一一列出,所以我萌發了一個想法:
我能否重構單元測試呢?
使用原本通過單元測試但尚未重構的程式碼,作為單元測試,用來測試重構後的程式:
這樣的測試確保了重構後的程式不會改變原本的功能,並且還能測試更大的數字(比如500, 1000)來避免潛在的邏輯錯誤或效能問題。
演講課 - 軟體架構設計
https://gelis-dotnet.blogspot.com/
工程師成長階段

- 基礎(學習):模仿→懂學習
- DRY(工作):能應付工作需求→懂程式
- 分層Design Pattern(解決技術債):問題解決,自我成長→懂Pattern(好程式)
- OOD(設計):抽象化→懂設計
- OOA(分析):溝通協作→懂協作
- 專案分享:傳承(透過教學了解細節)→懂細節
- 系統整合:流程改善(好方法)→有好的開發流程
- 獨立作業:顧問(獨立)→有自己的一套方法
- 跨團隊:專家
- 教練/工匠:勘誤
工程師的成長是一個漸進的過程,從基礎的學習到最終成為能夠帶領他人前進的技術領袖,每個階段都蘊含著深刻的進步與挑戰。一開始,工程師多以模仿和學習為主,理解基礎的工具與技術,逐步具備完成工作需求的能力。在這個過程中,他們開始領會程式的運作邏輯,並以「不重複自己」(DRY)為原則,提升效率和質量。
隨著經驗的累積,他們會遇到更多技術債的問題,進而學習如何運用設計模式(Design Pattern)來解決這些挑戰。此時,他們不僅寫出更好的程式,還在解決問題中實現自我成長。再往前,他們進一步理解抽象化的概念,進入物件導向設計(OOD)的領域,能夠創建出可擴展、可維護的系統設計。
成長的下一步是掌握面向物件分析(OOA),這需要更多的溝通與協作能力。他們開始關注如何與團隊成員共同設計解決方案,並透過專案分享和教學來傳承知識,這不僅幫助他人,也讓他們自己更深入地了解細節。在更高的層次,他們致力於系統整合,優化開發流程,並逐漸形成自己的一套方法論,成為能獨立承擔責任的顧問。
當工程師跨越團隊,成為專家時,他們的角色更多的是指導與協調,分享經驗並推動技術進步。而作為技術教練或工匠,他們不僅專注於技術本身,還致力於培養他人,幫助團隊避免錯誤,將精益求精的精神融入整個工程文化。
這一過程顯示,工程師的成長不僅是技術的提升,更是對溝通、協作、分享和領導的全方位鍛煉。真正成熟的工程師,不僅是解決問題的高手,更是知識的傳承者與團隊的推動者。
什麼是軟體架構設計
- 軟體架構的目的:不讓專案變成大泥球(亂)
- 沒有軟體架構的問題:
- 沒有版控
- 原始碼
- 沒有文件
- 沒有需求訪談
- 沒有規範
- 沒有時程規劃
- 沒有測試(環境)
- 不求好只求有
- 軟體架構是什麼:怎麼解決
- 簡化軟體開發作業
- 統一規範(Coding style)
- 組件重用性
- 一致的開發技術與架構,減少管理跟技術債
- 軟體架構設計
- 設計什麼?:設計一個能賺錢的系統
- 讓系統變得可控、可抽換性、可靠、可維護(讓成本變低)
- 成本:包含維護成本、學習門檻等
- 目標:最小化建置和維護「需求系統」的人力資源。
- 傳統階層式的問題
- 什麼是設計:低層次
- 什麼是架構:高層次
- 關注系統的整體結構與組織方式,如模組間的關係、業務邏輯的劃分等。
- 問題:雜亂比整潔來得快
- 設計缺乏規範會讓代碼迅速變得雜亂,隨之帶來開發與維護成本的無限增長。
- 什麼是好的架構?
- 問題:實務上難以完全想清楚需求,導致系統需要不斷修改
- 畢竟不能一次到位,那就做好 Core Domain 下應該要做的事情就好
軟體架構的核心目的在於,讓一個專案變得有序並且好維護,能用更低的成本達到客戶的需求。但是呢,往往需求是會隨時間變化,難以一步到位,這就導致我們開發時,需要一個開發的準則或原則,讓工程師遵循這個框架進行設計,這就是軟體設計架構的核心概念。
這些準則不僅僅是技術層面的規範,而是提供了一套應對變化與複雜性的策略。這跟我們軟體設計所學的Design pattern與Design principles有直接的關係,比如採用模組化設計可以讓系統的不同部分彼此獨立,減少變更對全局的影響;統一的代碼風格與規範則讓團隊之間的協作更加順暢。同時,透過像 SOLID 原則這樣的設計原則,系統能更具彈性和可擴展性,為未來的修改留有足夠的空間。
但實務上,還是要考量到開發與成本的平衡,過於複雜或超前的設計會增加技術負擔,而過於簡單的設計又可能在需求變化時無法適應。因此,好的軟體架構需要專注於核心領域(Core Domain),解決當下最重要的問題,同時具備足夠的彈性來應對未來的挑戰。
軟體架構設計:API 設計準則

- API 設計準則是什麼?
API 設計準則是指在設計和實作應用程式介面(API)時,遵循的一系列原則。
- 一致性:介面風格和命名規則統一。
- 易用性:使開發者能快速理解和使用。
- 可擴展性:適應未來需求的增長。
- 安全性:防止數據洩露和攻擊。
- 性能:確保響應快速,資源使用效率高。
- API 設計的目的
- 促進系統整合:使不同系統、服務和應用程式之間能無縫通信。
- 提高開發效率:為開發者提供清晰的介面,減少溝通成本和開發時間。
- 增強可維護性:通過一致的設計,降低後續修改和維護的難度。
- 支持業務需求:滿足當前需求並為未來的業務擴展留有空間。
- 提升用戶體驗:對於開放型 API,為開發者提供良好的使用體驗,增強產品競爭力。
- 如何做到 API 準則
- API First 思維:以產品化思維設計 API,從企業的商業能力出發,而非僅僅滿足單一客戶的需求。
- 領域驅動設計(DDD):透過事件風暴(Event Storming)等方法,確定系統的核心領域(Core Domain)和子領域,並以此驅動 API 的開發。
- 整潔架構(Clean Architecture):在程式碼實作中,遵循整潔架構的原則,確保系統的可維護性和可測試性。
- 模組化設計:將系統劃分為不同的上下文(Contexts),如購票、管理票卷、簡訊發送等,明確各自的系統邊界。
- 重視應用層服務(Application Services):在應用層實作中,定義清晰的服務介面,處理業務邏輯,並與領域層進行互動。
- 使用設計模式:在實作中,運用適當的設計模式,如工廠模式(Factory Pattern)來建立物件,確保程式碼的彈性和可維護性。
- 資料傳輸物件(DTO):在應用層與外部系統交互時,使用 DTO 來傳遞資料,避免直接暴露領域物件。
- 儲存庫模式(Repository Pattern):透過儲存庫介面(Repository Interface)來抽象資料存取層,促進程式碼的可測試性和維護性。
- 錯誤處理與驗證:在 API 設計中,考慮適當的錯誤處理和資料驗證機制,確保系統的穩定性和安全性。
- 文件化:為 API 提供詳細的文件,方便開發者理解和使用,提升開發效率和協作性。
- ADDR
- API Design-First 的重要性:
- 強調設計 API 的過程優先於其他開發工作。
- 將用戶與開發者的需求置於首位,降低技術使用門檻。
- 避免傳統開發中因重新設計而產生的冗長週期和失敗風險。
- 從 RESTful 到 API Design-First 優先設計
- 以 Resource-Based 為基礎,將網路中的一切視為「資源」。
- 強調資源與數據的關聯。
- API Design-First 的理念:
- 資源 ≠ 資料模型:
- API 應該專注於交付企業的數位能力,而非後端的資料結構。
- 適合跨企業資料交換,避免特定平台綁定,確保標準化與開放性。
- 資源 ≠ 物件或領域模型(Domain Model):
- API 應避免與後端程式碼直接耦合,提升維護性和穩定性。
- 強調模組化設計、封裝、低耦合和高內聚等基礎軟體設計原則。
- API Design-First 與傳統軟體開發方法的差異
- 設計順序
- API Design-First:
在撰寫程式碼和設計使用者介面(UI)之前,先設計 API。
- 傳統開發方法:
先完成核心應用程式邏輯和 UI,最後設計 API。
- 協作模式:
- API Design-First:
前端、後端及利害關係人早期參與,強調跨部門協作。
- 傳統開發方法:
開發流程較孤立,系統整合常在後期進行,存在更多風險。
- 敏捷開發的影響:
- API Design-First 實踐了敏捷開發的理念,快速回應需求變更。
ADDR(API Design-First Design & Runtime)是一種從領域驅動設計(DDD)延伸而來的實踐方法,它強調在開發 API 時以設計為優先,並將設計與執行環境緊密結合。這種方法的核心理念在於,API 不僅是一種技術實現工具,更是一種數位能力的表達方式,因此設計的重點應該放在如何清晰地傳達業務能力,而非僅僅參照後端的資料。
更強調跨團隊的協作,將前端、後端和業務人員緊密聯繫在一起,屬於一種團隊上理念與溝通的橋樑,讓開發過程更加敏捷和協作化。