# 軟體設計(成大) [toc] ## ch2 OOP ### part 1. Encapsulation ```java public class Duck { public boolean canfly = false; // instance variable public void quack(){ System.out.println("Quack!!"); } } ``` #### 封裝 Encapsulation 將程式切割成一塊塊模組 降低程式的耦合度、增加可控度 避免被修改的風險 ```java public class Duck { private boolean canfly = false; //** public boolean getCanfly(){ return canfly; } … } ``` #### 多型 Polymorphism ##### Method Overloading 在同個 calss,方法名一樣,相同或高相似度的行為,但不同實作方式的同名函式。 增加可讀性 ```java public class Duck { … public void quack(){ System.out.println("Quack!!"); } public void quack(String sound){ System.out.println(sound); } … } public class Farm { public static void main(String[] args) { Duck duck = new Duck(true); … duck.quack(); duck.quack("Ga Ga Ga"); } } ``` ### part 2. Call by Reference `ToyClass sampleVariable = new ToyClass("JS", 42)` ![image](https://hackmd.io/_uploads/rJtFCZK6R.png) ``` java variable2 = sampleVariable variable2 // 指向同一個 sampleVariable ``` ```java public class Cat { int age = 1; public static void main(String[] args) { Cat cat1 = new Cat(); Cat cat2 = cat1; cat1.age = 2; System.out.println(cat2.age); // 2 } ``` ```java public class PrimitiveParameterDemo { public static void main(String[] args) { int speed = 50; System.out.println("argument value:" + speed); changer(speed); // Call by Value System.out.println("argument value:" + speed); //50 } public static void changer(int speed) { speed = 100; System.out.println("parameter value:" + speed); } } ``` argument value: 50 parameter value: 100 argument value: 50 (Call by Value) ```java public class ClassParameterDemo { public static void main(String[] args) { ToyClass anObject = new ToyClass("Robot Dog", 10); System.out.println(anObject); changer(anObject); // Call by Reference System.out.println(anObject); } public static void changer(ToyClass aParameter) { aParameter.set("Robot Cat",20); } } ``` Robot Dog 10 Robot Cat 20 (Call by Reference) ### part 3. 繼承 Inheritance 讓子類別能夠使用父類別的行為、屬性跟方法, 也可以藉由抽象類別跟介面,先定義規格,而在不同的子類別做出不一樣的實作方式。 properties = member 蘋果繼承水果 鋼筆繼承筆 是 super 跟 Derived Class (Subclass) 的關係 #### overriding 不同class,同名方法,父親的行為能被兒子使用,但實作可以不同。 overloading 則是同個 class。 ```java public class Employee { protected String name; protected Date hireDate; public Employee(){} public Employee(String theName, Date theDate){ name = theName; hireDate = theDate; } public Date getHireDate(){ return hireDate; } public String getName(){ return name; } } ``` ```java import java.util.Date; public class HourlyEmployee extends Employee{ private double wageRate; public HourlyEmployee(String theName, Date theDate, double rate){ name = theName; hireDate = theDate; wageRate = rate; } public String getName(){ return "Hourly Employee:" + name; // 不同實作方式 } } ``` ### part 4. 抽象類別 Abstract Class & 介面 Interface #### Abstract Class 抽象,未被實作的,實作留給相關類去實作。 用以定義規格或api,通常在父親。 ```java public abstract class Animal { public abstract void run(); public void sit(){ System.out.println(“Sit down…”); } } ``` 必讓兒子去實作run(),在下面層級能讓其做不同的行為 ```java public class Dog extends Animal { public void run(){ System.out.println("The dog is running"); } } ``` ```java public class Cat extends Animal{ public void run(){ System.out.println("The cat is running"); } } ``` #### Interface ```java public interface Shape { int color = 1; // => public static final int color = 1; 常數 } ``` ```java public class Paint { public static void main(String[] args) { System.out.println(Shape.color); } } ``` ```java public interface Shape { int color = 1; // => public static final int color = 1; public abstract double area(); //=> 或是 double area(); // 只能為抽象方法 } ``` Abstract Classes can Implementing Interfaces ### part 5. 多型 Polymorphism 一個抽象行為有不同的實作 #### Early binding (through overriding) compiler time 時就決定,靜態決定。 ```java public class SayHello { public String sayHello(String name){ return "Hello! "+ name; } public String sayHello(String name, String gender){ if(gender.equals("boy")){ return "Hello! Mr. "+ name; } else if(gender.equals("girl")){ return "Hello! Miss. "+ name; }else{ return "Hello! "+ name; } } public static void main(String[] args){ SayHello hello = new SayHello(); System.out.println(hello.sayHello("S.J.")); //decided at compile time System.out.println(hello.sayHello("S.J.","boy")); //decided at compile time } } ``` #### Late binding (through overriding) run time時才決定,動態決定。 ```java public class Payment { public void pay(){ System.out.println("Pay in cash"); } public void checkout(){ pay(); } } ``` ```java public class CreditCardPayment extends Payment{ public void pay() { System.out.println("Pay with credit card"); } } ``` ```java public class Store { public static void main(String[] args) { Payment p1 = new Payment(); p1.checkout(); Payment p2 = new CreditCardPayment(); p2.checkout(); // ("Pay with credit card") } } ``` 會往上尋找實作 1. checkout() 只有被父親 Payment() 實作 2. 父親裡的 pay() 有被兒子 CreditCardPayment() 所實作 3. 所以最後是 call 兒子的 pay() 得到 "Pay with credit card" #### Upcasting and Downcasting ##### Upcasting ```java Payment p2 = new CreditCardPayment(); p2.checkout(); ``` ##### Downcasting ```java Payment p1 = new Payment(); CreditCardPayment p2 = (CreditCardPayment)p1; //runtime error ``` ## ch3 UML Class Diagram ### Class Notation ![image](https://hackmd.io/_uploads/By04DEzAA.png =300x) #### Abstract and Interface **Class name** and **Method** 為斜體字 ![image](https://hackmd.io/_uploads/HkCTvNMAA.png =200x)![image](https://hackmd.io/_uploads/HkPAw4zRR.png =198x) ### Relationship #### Generalization 概括(抽象化) 是否有 <font color=red>is-a</font> 的關係? 為 **class** 跟 **Interface/Abstract** class 的差別 ![image](https://hackmd.io/_uploads/Hkvk6VMRR.png =150x)![image](https://hackmd.io/_uploads/rJDATNM0C.png =200x) ![image](https://hackmd.io/_uploads/H1yqbSzCA.png) #### Dependency 依賴 是否有 <font color=red>uses-a</font> 的關係? 通常為 Method 的 **Parameters** 或 **Local Variable** 虛線:關係較薄弱 ![image](https://hackmd.io/_uploads/BkM34SM0R.png) ```java public class Tourist { public void buy(TicketCounter tc) { tc.sellTicket(); // use TicketCounter's Method } } ``` ```java public class TicketCounter { public String sellTicket() { return "Random Ticket No."; } } ``` ![image](https://hackmd.io/_uploads/ry8xLSzCA.png) #### Association 是否有 <font color=red>has-a</font> 的關係? 實線:關係跟狀態比較穩固跟強烈 ![image](https://hackmd.io/_uploads/HkB5PrGCC.png) ```java import java.util.ArrayList; public class Library { private ArrayList<Book> books = new ArrayList<Book>(); public void addBook(Book book) { // 創建了穩固的 book 實例 books.add(book); } } ``` ```java public class Book { private String title; } ``` ![image](https://hackmd.io/_uploads/rJyiHlORC.png) #### Bidirectional Association 雙向關係 無箭頭直線 缺點:難以加入新的元素,如學生的課程分數 ![image](https://hackmd.io/_uploads/HJx8oSGA0.png) ```java import java.util.ArrayList; import java.util.List; public class Student { private List<Course> courses = new ArrayList<>(); public void enroll(Course course) { courses.add(course); } } ``` ```java import java.util.ArrayList; import java.util.List; public class Course { private List<Student> students = new ArrayList<>(); public void add(Student student) { students.add(student); } } ``` ##### Association Class 關聯類別 改善方式:用新的 class 重構 ![image](https://hackmd.io/_uploads/S1hPaSGAA.png) ```java public class Enrollment { private Student student; private Course course; public Enrollment(Student student,Course course) { this.student = student; this.course = course; } } ``` ```java public class Student { private List<Enrollment> enrollments = new ArrayList<>(); public void enroll(Course course) { Enrollment enrollment = new Enrollment(this, course); enrollments.add(enrollment); course.addEnrollment(enrollment); } ``` ```java public class Course { private List<Enrollment> enrollments = new ArrayList<>(); public void add(Enrollment enrollment){ enrollments.add(enrollment); } ``` ##### Aggregation 聚合 一個類別「包含/擁有」另一個類別 為「較弱」的聚合(Whole-Part)關係 whole消失,part可繼續存在 使用 <font color=red> 空菱形 </font> 如:即使班級不存在,學生也能在其他班級中繼續存在。 如:餐廳有許多顧客,但餐廳倒了,顧客可以去其他餐廳。 如:電腦有cpu,電腦壞了,cpu可以拿到其他電腦使用。 ![image](https://hackmd.io/_uploads/Bk4A6rzRC.png) ![image](https://hackmd.io/_uploads/SkfcRrfRR.png) 但兩者實作方式一樣 ##### Composition 為較「強烈」「包含/擁有」的關係 whole消失,part**不**可繼續存在 使用 <font color=red> 實心菱形 </font> ![image](https://hackmd.io/_uploads/Bkt8gIMC0.png) ![image](https://hackmd.io/_uploads/rJ25zIfAA.png) 1. 封裝在裡面 ```java class Car { private Engine engine; public Car() { this.engine = new Engine(); } private class Engine { ... } } ``` 2. 放在外面,確保其他物件不持有 part 物件的 Reference ```java class Car { private Engine engine; public Car() { this.engine = new Engine(); } ``` ```java class Engine { ... } ``` 3. WeakReference ```java import java.lang.ref.WeakReference; class Car { private Engine engine; public Car() { this.engine = new Engine(); } public WeakReference<Engine> getEngineReference() { return new WeakReference<>(engine); } } ``` #### Tips * code 不能完全實現 UML * UML工具 才能實現一樣的 code ![image](https://hackmd.io/_uploads/BJ0q4LG0A.png) 無法產生菱形箭頭,實作一樣 ### 例題 1. A country <font color=red>has a</font> capital city. ![image](https://hackmd.io/_uploads/BJrnhVKC0.png) :::spoiler ```java public class Country { private String name; private City CapitalCity; public Country(String name, City city) { this.name = name; this.CapitalCity = city; } } ``` ```java public class City { private String name; } ``` ::: 2. A dining philosopher is <font color=red>using a</font> fork. ![image](https://hackmd.io/_uploads/HJXJ6EF00.png) :::spoiler ```java public class DiningPhilosopher { public void useFork(Fork fork) { fork.eat(); } } ``` ```java public class Fork { public void eat() { // 實作 } } ``` ::: 3. A file <font color=red>is an</font> ordinary file or a directory file. ![image](https://hackmd.io/_uploads/BybKTNKRR.png) :::spoiler ```java public abstract class File { private String name; private Record records; public File(String name) { this.name = name; } public abstract void open(); public abstract void close(); } ``` ```java public class OrdinaryFile extends File{ public OrdinaryFile(String name) { super(name); } @Override public void open() {// 實作} @Override public void close() {// 實作} } ``` ```java public class DirectoryFile extends File{ public DirectoryFile(String name) { super(name); } @Override public void open() {// 實作} @Override public void close() {// 實作} } ``` ::: 4. Files <font color=red>contain(包含)</font> records. ![image](https://hackmd.io/_uploads/BkBXR4YRA.png) :::spoiler ```java public class File { private String name; private Record records; } ``` ```java public class Record { private int id; } ``` ::: 5. A polygon <font color=red>is composed of</font> points. ![image](https://hackmd.io/_uploads/BywTANF0A.png) :::spoiler ```java import java.util.List; import java.util.ArrayList; public class Polygon { private List<Point> points; public Polygon() { points = new ArrayList<>(); } } ``` ```java public class Point { private double x; private double y; public Point(double x, double y) { this.x = x; this.y = y; } } ``` ::: 6. A drawing object <font color=red>is a</font> text, a geometrical object, or a group. ![image](https://hackmd.io/_uploads/rJ21yHYCC.png) :::spoiler ```java public interface DrawingObject { void draw(); } ``` ```java public class TextObject implements DrawingObject{ private String text; public void TextObject(String text) { this.text = text; } public void draw() { System.out.println("Drawing text: " + text); } } ``` ```java public class GeometricObject implements DrawingObject{ private String shape; public GeometricObject(String shape) { this.shape = shape; } public void draw() { System.out.println("Drawing shape: " + shape); } } ``` ```java public class Group implements DrawingObject{ private List<DrawingObject> objects; public Group() { objects = new ArrayList<>(); } public void draw() {// 實作} } ``` ::: ### 總結 | Inheritance(繼承) | Implementation(實作) | | -------- | -------- | | **class** | **Interface/Abstract** | | is-a | is-a | | 三角形實線 | 三角形虛線 | | ![image](https://hackmd.io/_uploads/Hkvk6VMRR.png =70x)| ![image](https://hackmd.io/_uploads/rJDATNM0C.png =90x) | | Dependency(依賴) | Association(關聯) | | -------- | -------- | | Parameters 或 Local Variable | 被設為Attribute | | uses-a 短期使用(臨時) | has-a 長期擁有(結構) | | 箭頭虛線 | 箭頭實線 | | ![image](https://hackmd.io/_uploads/BkM34SM0R.png =200x)| ![image](https://hackmd.io/_uploads/rJyiHlORC.png =200x) | | Bidirectional Association(雙向關聯) | Aggregation(聚合) | Composition | | -------- | -------- | -------- | | 互相被設為Attribute | whole消失 part不消失 | whole消失 part也消失 | | 互相擁有 | 弱引用(擁有) | 強引用(擁有) | | 實線無箭頭 | 實線空心菱形 | 實線實心菱形 | | ![image](https://hackmd.io/_uploads/HJx8oSGA0.png =200x)| ![image](https://hackmd.io/_uploads/SyCWBluRC.png =200x) | ![image](https://hackmd.io/_uploads/Bkt8gIMC0.png =200x)| ## ch3 Code Structure View via UML Class Diagram * Legacy Code 理解既有程式碼 * Trace code 逆向工程過程:透過 zoom in/out 理解code * Structure (靜態結構) ➔ 地圖 * Behavior (動態行為) ➔ 路徑 ### Class Diagram三步驟 1. 設定New Class Diagram add dependency ![image](https://hackmd.io/_uploads/BJtSelCJ1g.png =200x) 2. 更新遺漏的Dependency 修正如下【Ctrl + a 全選】➔【在任一class上右鍵】➔【Add】➔【Dependencies】 3. 更新Layout 【畫面右鍵】➔【Layout Diagram】 ### Levels of Abstraction in Java Code Structure ![image](https://hackmd.io/_uploads/H1dai1Ry1x.png =400x) #### Package Level ![image](https://hackmd.io/_uploads/Syku31RJyg.png =400x) * Cyclic dependency:問題 * Unidirectional dependency:理想 ##### 資訊減量 只看重要的部分 ![image](https://hackmd.io/_uploads/ByXP7gCk1g.png =300x) #### Class Level(Intra-Package) 1. 關注依賴於高階抽象(interfaces、abstract classes)或低階實體(sub-classes) 2. 關注bidirectional dependencies ![image](https://hackmd.io/_uploads/SJdbvgCyye.png) ##### 資訊減量 * 可隱藏核心外如data classes、utility classes、exception classes、enums、composition root、UI-layer classes * 可隱藏dependencies,只顯示實線(inheritance,implementation與association) ![image](https://hackmd.io/_uploads/BkiADlC1Jl.png) * Isolated classes (獨立的classes) * 重複的association lines * composition root ![image](https://hackmd.io/_uploads/Hk-d7ZC11e.png) ##### 何謂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很混亂,可以先移除整理後再加入 ![image](https://hackmd.io/_uploads/SJQE7-AJJg.png) #### Class Level (Cross-Package) ![image](https://hackmd.io/_uploads/SksbYWC11x.png) ##### 資訊減量 ![image](https://hackmd.io/_uploads/BkF8cbRy1e.png) ![image](https://hackmd.io/_uploads/SJDt5bRJJx.png) ![image](https://hackmd.io/_uploads/r1b59-Akye.png) * 直接將大量所有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 ![image](https://hackmd.io/_uploads/BkQQ8mDgkx.png) #### Memory Leak memory deallocated 佔用記憶體空間 可能 out of memory ![image](https://hackmd.io/_uploads/Hy9cUXvgyg.png) #### Long method 沒有標準:一個畫面的長度、參考行數、程式語意跟 Method name 是否吻合 問題: 1. 不易讀、難理解 2. 不易命名(方法的語意) 3. 不易reuse 方法:用 Extract Method 抽出來成短 method ![image](https://hackmd.io/_uploads/rkRrKmwxJl.png) #### Feature Envy 大量使用其他(自己以外) class 的變數、內容 ![image](https://hackmd.io/_uploads/ryrxR7Dg1x.png) #### Unsuitable naming 不適當命名 ![image](https://hackmd.io/_uploads/ryVlgEDxkl.png) #### Downcasting 向下轉型 ![image](https://hackmd.io/_uploads/rJX7gEDeJg.png) ![image](https://hackmd.io/_uploads/HyoFg4Pgyx.png) 但有不得不的狀況: 1. api 不給動 2. 舊程式 3. Deserialization #### Loop termination conditions are obvious and invariably achievable 結束條件不明顯 ![image](https://hackmd.io/_uploads/Hy6KWNvlJl.png) #### Parentheses are used to avoid ambiguity 不明確的括號,造成問題或不易讀 ![image](https://hackmd.io/_uploads/SJ5CWVwl1x.png) #### Lack of comments 缺少註解:不易讀 - 結論:有需要再加,不需要就不用 - 避免 comments 跟 code 不一致,修改 code 但 comments 沒有更新 #### Files are checked for existence before attempting to access them 讀檔要確認有正確載入 ```c if (inputFileStream.is_open()) { // do something } ``` #### Duplicated Code 重複的 code ![image](https://hackmd.io/_uploads/SJtb5bRZ1l.png) #### Access modifiers All methods have appropriate access modifiers and return types 適當的存取和返回 ![image](https://hackmd.io/_uploads/rJefENvlye.png) #### Redundant or Unused variables 沒用到、沒用的變數 ### Indexes or subscripts are properly initialized, just prior to the loop 沒有初始化或沒有宣告值 ![image](https://hackmd.io/_uploads/BJmGrEDeJe.png) #### Is overflow or underflow? 數字是否超過型態的範圍 注意大數字 ![image](https://hackmd.io/_uploads/SJWwSNPg1g.png) #### Are divisors tested for zero? 分母為'0',除法時要做判斷 #### Inconsistent coding standard 應符合程式風格 ![image](https://hackmd.io/_uploads/BJAJI4vgkx.png) #### Data clumps ![image](https://hackmd.io/_uploads/rkMEIVDxyl.png) 重複的 data 變數可以抽出class ![image](https://hackmd.io/_uploads/H1fwLVvgJg.png) #### Simulated Polymorphism ![image](https://hackmd.io/_uploads/SJM5LEDx1x.png) 使用時會有分類、擴展時再使用: 比如有新的動物,減少去修改既有的class ![image](https://hackmd.io/_uploads/SJJmDVDxyl.png) #### Large class 沒有 Single Responsibility Principle (SRP) ![image](https://hackmd.io/_uploads/rJmqnEPx1x.png) 能根據語意再次拆分,使用者/設定/log/檔案處理。 #### Long parameter list 參數多,代表參數可能也可以分群 ![image](https://hackmd.io/_uploads/H1F364wgJl.png) #### Message Chains ![image](https://hackmd.io/_uploads/ByMOAEDxke.png) 如果一個區塊改變,那後續交錯或串聯的都會受影響,不好維護。 ![image](https://hackmd.io/_uploads/HJRa0Vwe1g.png) ##### 重構1:新增Method 不跟陌生人講話,透過窗口對話。 Client only talks to Company(Demeter’s Law) ![image](https://hackmd.io/_uploads/SJxwJrDxke.png) ![image](https://hackmd.io/_uploads/HyKvJrwxyl.png) ![image](https://hackmd.io/_uploads/ByR1gSvxyx.png) 違反RSP ##### 重構2:新增中介Class(Façade Pattern) 新增一個窗口或API(CompanyService)幫我呼叫 讓這個窗口符合它的SRP,就是為了幫我呼叫的任務 ![image](https://hackmd.io/_uploads/ry8EbrDxJg.png) ![image](https://hackmd.io/_uploads/S17WZrweye.png) ##### 總結 ![image](https://hackmd.io/_uploads/HkkwbHDe1x.png) #### Literal constants 常數應被替代,增加可讀性 ![image](https://hackmd.io/_uploads/BknD7HPlkx.png) #### uncalled or unneeded procedures or any unreachable code 不需要存在 ![image](https://hackmd.io/_uploads/H1uM4rDgyx.png) #### switch statement have a default ![image](https://hackmd.io/_uploads/rJ6sErDgkl.png) #### comparing floating-point numbers for equality ![image](https://hackmd.io/_uploads/BkNAVBPgkx.png) #### Divergent Change(發散式改變)(*) 一個類別會因為要因應太多的變更原因而需修改 方法:利用 Extract Class 重構 * 範例: ![image](https://hackmd.io/_uploads/S1YOHHvgyl.png) * 重構: ![image](https://hackmd.io/_uploads/SyYsrrDgJl.png) 符合SRP(是否同一個Class中的Methods相互依賴或共用屬性) 舉例: 會因為編碼方式、傳輸方式、解碼方式多種原因需要修改該class ```java class VideoService { public void encodeVideo(String format, String filePath) { // 將影片編碼成指定格式 } public void transmitVideo(String protocol, String filePath) { // 通過指定協議傳輸影片 } public void decodeVideo(String filePath) { // 解碼影片 } } ``` ```java // 負責影片編碼的類別 class VideoEncoder { public void encode(String format, String filePath) { // 將影片編碼成指定格式 } } // 負責影片傳輸的類別 class VideoTransmitter { public void transmit(String protocol, String filePath) { // 通過指定協議傳輸影片 } } // 負責影片解碼的類別 class VideoDecoder { public void decode(String filePath) { // 解碼影片 } } ``` #### Shotgun Surgery(*)散彈式修改 * 每次為了因應一種變更,你必須同時在許多類別上做出許多修改。 * 當有太多需修改的地方時,將造成難以尋找所有需修改處,並容易遺漏。 * 常發生於Copy and Paste Programming ![image](https://hackmd.io/_uploads/SJMJwqtbJg.png) * 範例: 更改主題,要手動傳入新的主題設定,黑底要白字,白字要黑底,它們不會一起連動。 ```java public class ThemeApp { public static void main(String[] args) { ThemeApp app = new ThemeApp(); Button button = new Button("Dark"); TextBox textBox = new TextBox("White"); // 更改主題,要手動傳入新的主題設定,黑底要白字,白字要黑底,它們不會一起連動。 } } ``` 改善後 ```java // 抽象主題接口 interface Theme { String getBackgroundColor(); String getTextColor(); } // Dark主題 class DarkTheme implements Theme { public String getBackgroundColor() { return "Black"; } public String getTextColor() { return "White"; } } // Light主題 class LightTheme implements Theme { public String getBackgroundColor() { return "White"; } public String getTextColor() { return "Black"; } } public class ThemeApp { public static void main(String[] args) { Theme darkTheme = new DarkTheme(); Theme lightTheme = new LightTheme(); Button button = new Button(darkTheme); TextBox textBox = new TextBox(darkTheme); } } ``` * 舉例(按鈕): 原本要修改所有按鈕的顏色要一個一個修改 ```java public class Button { private String color; public Button(String color) { this.color = color; } public class BuildAllButton { public void createButtons() { Button button1 = new Button("Red"); Button button2 = new Button("Red"); // 修改顏色 button1 = new Button("Blue"); button2 = new Button("Blue"); } } ``` 這裡我可以一次修改所有按鈕顏色 ```java // Button 類別 public class Button { public String getColor() { return ButtonStyleManager.getButtonColor(); // 即時取得最新顏色 } public void display() { System.out.println("Button color: " + getColor()); } } // ButtonStyleManager 類別,管理按鈕的顏色 public class ButtonStyleManager { private static String buttonColor = "Red"; // 默認顏色 // 設定顏色 public static void setButtonColor(String color) { buttonColor = color; } // 取得顏色 public static String getButtonColor() { return buttonColor; } } // BuildAllButton 類別,負責建立並管理按鈕 public class BuildAllButton { public void createButtons() { Button button1 = new Button(); Button button2 = new Button(); // 顯示按鈕的初始顏色 button1.display(); // Button color: Red button2.display(); // Button color: Red // 修改顏色 ButtonStyleManager.setButtonColor("Blue"); // 顯示按鈕的更新後顏色 button1.display(); // Button color: Blue button2.display(); // Button color: Blue } } ``` #### Primitive Obsession * 堅持用基本型態表達 * Loss of Type Safety 容易犯錯 * Lack of Encapsulated Behavior 沒辦法表示行為 將String PostalCode 改為 object 就能有行為去做如 check 的動作 * Replacing Primitives with(Value) Objects ![image](https://hackmd.io/_uploads/S121t5tWJe.png) * 舉例: ```java public class Person { private String name; private String idCard; // 身分證 ID 用 String 來表示 public Person(String name, String idCard) { this.name = name; this.idCard = idCard; } ``` 改善後: ```java public class IdCard { private String idCardNumber; public IdCard(String idCardNumber) { if (!isValidIdCard(idCardNumber)) { throw new IllegalArgumentException("Invalid ID Card number"); } this.idCardNumber = idCardNumber; } public class Person { private String name; private IdCard idCard; // 身分證 ID 現在是 IdCard 類別的實例 public Person(String name, IdCard idCard) { this.name = name; this.idCard = idCard; } ``` #### Operation Class 行為物件 一般情況,class name應該要為名詞。 * Operation Class的Class Name通常為動詞(CreateReport),而非物件名詞(Report) * 通常一個Class包含只有一個Method * 由於Class Name已經限制了語意,因此很難再擴充Method,造成須相對創建了許多Class * 由於Class Name為功能特性思維去命名,因此較難以物件導向思維去創建繼承關係以及動態多型的優勢 如果為動詞,通常只能做一個行為,那就會建立許多class來完成不同任務。 範例: ![image](https://hackmd.io/_uploads/SydmjctW1l.png) 重構: ![image](https://hackmd.io/_uploads/HJJVs5YZye.png) #### Alternative Classes with Different Interfaces 實現了類似的功能,但在不同的介面或是不同的實作 範例: ![image](https://hackmd.io/_uploads/r13Po9Fbye.png) 重構: ![image](https://hackmd.io/_uploads/Hy-3i5F-1e.png) ![image](https://hackmd.io/_uploads/SyuS3cYWke.png) #### Refused Bequest The unneeded methods may simply go unused or be redefined and give off exceptions. 兒子繼承父親,但不要父親全部的 methods 為什麼兒子不想繼承父親,可能要修改 method 或是拆解,使其合理化。 範例: ![image](https://hackmd.io/_uploads/S1E4T5Fbye.png) 重構: ![image](https://hackmd.io/_uploads/BkDrpqF-ye.png) * 舉例: 飛行器中有翅膀的屬性,雖然直升機也是飛行器但它沒有翅膀。 ```java public class FlyingVehicle { private String engine; private String wings; // 不適用於所有飛行器,直升機不需要翅膀 } public class Helicopter extends FlyingVehicle { private String rotor; } ``` 改善後: ```java public abstract class FlyingVehicle { private String engine; } public class Airplane extends FlyingVehicle { private String wings; public Airplane(String engine, String wings) { super(engine); this.wings = wings; } } public class Helicopter extends FlyingVehicle { private String rotor; public Helicopter(String engine, String rotor) { super(engine); this.rotor = rotor; } } ``` #### Parallel Inheritances Hierarchies 平行繼承 兩棵樹,如果一邊要增加,另一邊也要跟著增加。 問題:無法滿足兩個樹底下的物件互相有特定配對依賴關係的要求。 車子 搭配 駕駛員 飛機 搭配 飛行員 導致其有特定的配對 ![image](https://hackmd.io/_uploads/r1foRcYZyx.png) ##### Defer Identification of State Variables Pattern * 第一步(屬性降階層):將Vehicle的operator屬性移除,並在Car與Plane中各別加入欲配對的屬性型態 * 第二步(加**Abstract Accessor**):在Vehicle中加入getOperator (稱之為Abstract Accessor)讓Car與Plane實作,以達成維持原本Vehicle與Operator的關係 ![image](https://hackmd.io/_uploads/Hy8MeoYZ1g.png) #### Middle Man 多餘的,沒有功能的中間人,只是在傳遞事情。 那個中間人同時也是 Feature Envy ![image](https://hackmd.io/_uploads/HJcOEiKZyg.png) * 舉例 ```java public class Order { private String customerName; private String product; public Order(String customerName, String product) { this.customerName = customerName; this.product = product; } public String getCustomerName() { return customerName; } public String getProduct() { return product; } } public class OrderProcessor { private Order order; public OrderProcessor(Order order) { this.order = order; } public void processOrder() { // 這些本來應該是 Order 類別的工作 String customerName = order.getCustomerName(); String product = order.getProduct(); // 然後傳遞給其他物件處理 System.out.println("Processing order for " + customerName + " who ordered " + product); // 實際工作是由 Order 類別來處理的,OrderProcessor 只是中介人 } } public class Main { public static void main(String[] args) { Order order = new Order("John Doe", "Laptop"); OrderProcessor processor = new OrderProcessor(order); processor.processOrder(); } } ``` #### Speculative Generality 過分假設未來的情況,預留空間導致程式很複雜,overdesign。 ![image](https://hackmd.io/_uploads/H1OQHoFb1g.png) * 舉例: ```java // 訂單類別,可能會有多種不同的訂單類型 public abstract class Order { public abstract void process(); } public class PhysicalOrder extends Order { @Override public void process() { // 處理實體商品訂單 System.out.println("Processing physical order..."); } } public class DigitalOrder extends Order { @Override public void process() { // 處理數位商品訂單 System.out.println("Processing digital order..."); } } // 訂單處理器,過於通用,設計上沒有真正需要 public class OrderProcessor { public void processOrder(Order order) { // 處理任何類型的訂單 order.process(); } } ``` ## 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. * 應該只扮演一個角色,只負責一個職責。 不同使用者,就必須要改變它的作法。 ![image](https://hackmd.io/_uploads/H1hpLitbJx.png) #### 具體判定法 1. 結構內聚力判定法 Method間的結構關係 2. 語意結合判定法 Class Name與Method Name間的結合語意 ##### 結構內聚力判定法 1. they both access the same class-level variable, or 兩個 Method 都共用 variable 或 attribute 2. A calls B, or B calls A. * 精神:將一個class內不互相依賴的method群拆解出去 * 範例:例如一個class中有method A, B, C, D, E,關係如下,因此可參考是否判定為兩個responsibility,進而拆分為兩個Class ![image](https://hackmd.io/_uploads/H1oKdjt-Jl.png) * 重構範例: ![image](https://hackmd.io/_uploads/S1M7tiKZyl.png) 把相互依賴的,經過拆解分群,以增加內聚力: ![image](https://hackmd.io/_uploads/SJ67tjtbJg.png) ##### 語意結合判定法 * 用語意判定是否合理 * 依每個method填入下表,構成語句:The Automobile (主詞) starts (動詞) itself. * 判定此句子是否具合理語意,若合理則留下,若不合理則考慮將此method移出此class ![image](https://hackmd.io/_uploads/rykfqiK-1g.png)![image](https://hackmd.io/_uploads/rJUzciK-yg.png) * 拆分以符合SRP ![image](https://hackmd.io/_uploads/HkN35oF-ke.png) ##### 總結:兩者判定法的限制 * 當一個class內method間結構關係複雜時,結構內聚力判定法可能較困難 * 當一個class name語意太general時(如XXXManager/Controller),會讓所有method name都可與class name語意結合,造成語意結合判定失效 ### Open-Close Principle(開放關閉原則)OCP * Open for extension, but closed for modification 一個模組必須有彈性的開放往後的擴充,並且避免因為修改而影響到使用此模組的程式碼。 ![image](https://hackmd.io/_uploads/BJW3ojtZyl.png) 不能有封閉迴路circle ![image](https://hackmd.io/_uploads/SkG-6stW1g.png) * 舉例 根據不同的員工類型計算薪資 會因為增加不同的員工類型而受改變 ```java // Employee class and subclasses abstract class Employee { String name; public abstract String getType(); } class FullTimeEmployee extends Employee { public String getType() { return "FullTime"; } } class PartTimeEmployee extends Employee { public String getType() { return "PartTime"; } } // SalaryCalculator that violates OCP class EmployeeSalaryCalculator { public double calculateSalary(Employee employee) { if (employee.getType().equals("FullTime")) { return 50000; // Full-time salary } else if (employee.getType().equals("PartTime")) { return 30000; // Part-time salary } return 0; } } ``` 改善後: ```java // Employee class and subclasses abstract class Employee { String name; public abstract double calculateSalary(); } class FullTimeEmployee extends Employee { public double calculateSalary() { return 50000; } } class PartTimeEmployee extends Employee { public double calculateSalary() { return 30000; } } class Freelancer extends Employee { public double calculateSalary() { return 20000; } } // SalaryCalculator that conforms to OCP class EmployeeSalaryCalculator { public double calculateSalary(Employee employee) { return employee.calculateSalary(); } } ``` ### Liskov Substitution Principle(LSP) T是父親S是兒子,兒子可以取代父親 ![image](https://hackmd.io/_uploads/Byq_TitW1l.png) * 舉例:正方形不是一種長方形,不能取代。 正方形不能被設定成不同長寬,所以不是長方形。 ![image](https://hackmd.io/_uploads/B1oCaitbkg.png) * 改善: ```java interface Shape { int getArea(); } class Rectangle implements Shape { protected int width; protected int height; public Rectangle(int width, int height) { this.width = width; this.height = height; } public void setWidth(int width) { this.width = width; } public void setHeight(int height) { this.height = height; } @Override public int getArea() { return width * height; } } class Square implements Shape { private int side; public Square(int side) { this.side = side; } public void setSide(int side) { this.side = side; } @Override public int getArea() { return side * side; } } ``` ### Interface Segregation Principle(ISP) 將大型接口分割成多個更專門的小接口,使得類別只需實作自己真正需要的接口。 ![image](https://hackmd.io/_uploads/SyoY0jt-1l.png) 窗口在interface介面上 ![image](https://hackmd.io/_uploads/Skbc0jKZJx.png) * 舉例 ```java interface Worker { void developSoftware(); void testSoftware(); void deploySoftware(); } class Developer implements Worker { @Override public void developSoftware() { System.out.println("Developer is developing software."); } @Override public void testSoftware() { // Developers一般不負責測試,這裡留空或隨便實作 throw new UnsupportedOperationException("Developer does not test software."); } @Override public void deploySoftware() { System.out.println("Developer is deploying software."); } } ``` 改善後: ```java interface Developer { void developSoftware(); void deploySoftware(); } interface Tester { void testSoftware(); } class SoftwareDeveloper implements Developer { @Override public void developSoftware() { System.out.println("Developer is developing software."); } @Override public void deploySoftware() { System.out.println("Developer is deploying software."); } } class SoftwareTester implements Tester { @Override public void testSoftware() { System.out.println("Tester is testing software."); } } ``` ### Dependency Inversion Principle(依賴反向原則)(DIP)(*) * 軟體設計的程序開始於簡單高層次的概念(Conceptual),慢慢的增 加細節和特性,使得越來越複雜 * 從高層次的模組開始,再設計低層詳細的模組。 * Dependency Inversion Principle (依賴反向原則) * 高階模組不應該依賴低階模組,兩者必須依賴抽象(即抽象層)。 越低層次被變動的機率越高,所以要降低高階依賴低階的情況。 ![image](https://hackmd.io/_uploads/BywzV3tWJe.png) 介入一層中間層(介面或抽象層): ![image](https://hackmd.io/_uploads/rJCG4hFWye.png) 利用 Dependency Injection 的概念。 ```java // 定義抽象的資料傳輸接口 interface DataTransport { connect(url: string): void; sendMessage(message: string): void; onMessage(callback: (message: string) => void): void; disconnect(): void; } // Dependency Injection class WebSocketTransport implements DataTransport { ... } class WebRTCTransport implements DataTransport { ... } // 使用 WebSocket const webSocketTransport = new WebSocketTransport(); const webSocketService = new DataService(webSocketTransport); webSocketService.start("ws://example.com/socket"); webSocketService.send("Hello via WebSocket!"); webSocketService.stop(); // 使用 WebRTC const webRtcTransport = new WebRTCTransport(); const webRtcService = new DataService(webRtcTransport); webRtcService.start("wss://example.com/webrtc"); webRtcService.send("Hello via WebRTC!"); webRtcService.stop(); // 可以隨時做服務的切換 ``` ### Encapsulate what varies(封裝改變) * 將易改變之程式碼部份封裝起來,以後若需修改或擴充這些部份時,能避免不影響到其他不易改變的部份。 * 換言之,將潛在可能改變的部份隱藏在一個介面(Interface)之後,並成為一個實作(Implementation),爾後當此實作部份改變時,參考到此介面的其他程式碼部份將不需更改。 ![image](https://hackmd.io/_uploads/rkGuUnKWkx.png) ![image](https://hackmd.io/_uploads/HyYIv2KW1e.png) * 舉例 攻擊寫在角色的類別中,假設有不同的角色,那麼攻擊行為就必須被擴充或修改 ```java class Character { private String name; public Character(String name) { this.name = name; } public void attack() { System.out.println(name + " is attacking with a sword!"); } } ``` 當需要新增或修改攻擊行為時,可以直接創建新的策略類別,而不需要更改 Character 類別,這樣符合開放-封閉原則(Open-Close Principle)OCP ```java interface AttackStrategy { void attack(String name); } class SwordAttack implements AttackStrategy { @Override public void attack(String name) { System.out.println(name + " attacks with a sword!"); } } class Character { private String name; private AttackStrategy attackStrategy; public Character(String name, AttackStrategy attackStrategy) { this.name = name; this.attackStrategy = attackStrategy; } } public class Game { public static void main(String[] args) { // 劍士角色,初始攻擊方式為劍擊 Character knight = new Character("Knight", new SwordAttack()); } } ``` ### Favor composition over inheritance(善用合成取代繼承) * 程式碼重用(Reuse) * 不要一味的使用繼承,要有IS-A的關係 * 有別於繼承,Composition可在Runtime時更有彈性地動態新增或移除功能 ### Least Knowledge Principle(最小知識原則) * 必須注意類別的數量,並且避免製造出太多類別之間的耦合關係。 * 知道子系統中的元件越少越好 * 不需要懂太多細節 ![image](https://hackmd.io/_uploads/HyfWd3tWyx.png) ### Acyclic Dependencies Principle (ADP) ![image](https://hackmd.io/_uploads/Sk7BOhKW1l.png) ![image](https://hackmd.io/_uploads/BJ5_u3t-Je.png) 難以符合OCP ### Don’t Repeat Yourself (DRY) * NO duplicate ### Keep It Simple Stupid(KISS) * 簡潔是軟體系統設計的重要目標,應避免引入不必要的複雜性 * 考慮是否 over design ## ch6 Design Patterns ### Strategy Pattern 關鍵字:An algorithm #### Requirements Statement 範例:文字編輯器 按照需求去增加:a new layout is required ![image](https://hackmd.io/_uploads/B1dxi-hzJe.png) 重構: 1. Encapsulate what varies 會改變的做封裝處理,抽出 ![image](https://hackmd.io/_uploads/HJa9n-2f1e.png) 2. Generalize common features 建樹 ![image](https://hackmd.io/_uploads/Skdh2b2G1l.png) 3. Program to an interface, not an implementation 使用樹的父親,對口interface,方便擴充跟修改 ![image](https://hackmd.io/_uploads/HJIJ6W2zyg.png) #### Recurrent Problems 需要增加新的 algorithms 的問題,就拉出來封裝 結構: ![image](https://hackmd.io/_uploads/HkyDkfnfke.png) ### Composite & Decorator 關鍵字:Structure and composition of an object 面對的問題是由 object 組成 #### Requirements Statement 範例:小畫家 如果要畫三角形,就有新的問題 ![image](https://hackmd.io/_uploads/H1g9gM3fkg.png) 重構: 1. Generalize common features 建樹 ![image](https://hackmd.io/_uploads/BkhJbznz1l.png) 2. Program to an interface, not an implementation. 使用父親 ![image](https://hackmd.io/_uploads/HkzqWMhGkg.png) ![image](https://hackmd.io/_uploads/ryOsZf2zyx.png) #### Recurrent Problem ![image](https://hackmd.io/_uploads/S1NcMznzyl.png) ### Decorator Pattern 關鍵字:Responsibilities of an object without subclassing 動態地為一個物件添加行為或職責,而不需要透過繼承來實現。 #### Requirements Statement 範例:Starbuzz Coffee cost 需要因應新服務而改變,變得不穩定 ![image](https://hackmd.io/_uploads/HkdH4f2Myx.png) 重構: 1. Encapsulate what varies ![image](https://hackmd.io/_uploads/B1FSSG3Gyg.png) 2. Generalize common features 概念是讓 condiment 也是一種食物配料 ![image](https://hackmd.io/_uploads/SkTOrM2fkg.png) 3. Program to an interface, not an implementation. ![image](https://hackmd.io/_uploads/HyOZIG2fke.png) 裝飾它 ![image](https://hackmd.io/_uploads/rk5wIz2G1x.png) #### Recurrent Problem 被裝飾放左邊 右邊的裝飾品用來擴充 ![image](https://hackmd.io/_uploads/HJRaDz2z1l.png) #### Composite vs. Decorator ![image](https://hackmd.io/_uploads/SJnF_f3zJx.png) ### Factory Method & Abstract Factory 關鍵字:Subclass of object that is instantiated #### Requirements Statement 範例:披薩店 ![image](https://hackmd.io/_uploads/Bkgafnz3fkl.png) 新增新的披薩會有問題 ![image](https://hackmd.io/_uploads/rk8i3f2zyx.png) 重構: 1. Encapsulate what varies ![image](https://hackmd.io/_uploads/Bym03Gnz1l.png) 2. Generalize common features ![image](https://hackmd.io/_uploads/rylb6M3zyl.png) 左樹:生產披薩 右樹:被生產的物件 ![image](https://hackmd.io/_uploads/ByMLpfnGke.png) #### Recurrent Problem 工廠生產產品,新增產品的生產步驟 ![image](https://hackmd.io/_uploads/r1r9AMnGkg.png) #### 補充:Parallel Inheritances Hierarchies問題 產品跟生產方式可能有配對的關係 ![image](https://hackmd.io/_uploads/ryWnJm3GJg.png) 能夠讓兒子去強制配對 ![image](https://hackmd.io/_uploads/H1ahJ7nMkl.png) ### Abstract Factory Pattern 關鍵字:Families of product objects #### Requirements Statement 範例:佈景主題 ![image](https://hackmd.io/_uploads/r13dxQnGJe.png) 重構: 1. Encapsulate what varies ![image](https://hackmd.io/_uploads/B108Zm3GJx.png) 2. Generalize common features ![image](https://hackmd.io/_uploads/HkvY-7nMJg.png) 3. Program to an interface, not an implementation. 生產一堆東西,抽出成工廠,限制其必須要實作 ![image](https://hackmd.io/_uploads/HkVs-m2zke.png) #### Recurrent Problem ![image](https://hackmd.io/_uploads/rkVHMQnMJl.png) #### Abstract Factory vs. Factory Method * Factory Method * creates single products * 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步驟一樣 ![image](https://hackmd.io/_uploads/HJ4XQQnz1l.png) 重構: 1. Encapsulate what varies ![image](https://hackmd.io/_uploads/H1RdX72fyx.png) 2. Generalize common features ![image](https://hackmd.io/_uploads/BkGqXmnzyl.png) #### Recurrent Problem ![image](https://hackmd.io/_uploads/HJ08Sm3f1g.png) ### Adapter 關鍵字:Interface to an object Interface 被改變了 #### Requirements Statement 範例: New Vendor in Existing Software 範例1:Object Adapter ![image](https://hackmd.io/_uploads/BkoPTm3z1g.png) 更改API ![image](https://hackmd.io/_uploads/r1ti6XnMJx.png) 重構: 1. Encapsulate what varies ![image](https://hackmd.io/_uploads/Sy5RaXnMyl.png) 2. Generalize common features 兒子去綁定不同的API ![image](https://hackmd.io/_uploads/BkDyC73G1x.png) 範例2:Class Adapter ![image](https://hackmd.io/_uploads/rkWpR73GJe.png) 重構: 1. Encapsulate what varies ![image](https://hackmd.io/_uploads/B1lk1VnMye.png) 2. Generalize common features ![image](https://hackmd.io/_uploads/B1jyyNhM1g.png) #### Recurrent Problem * Object Adapter ![image](https://hackmd.io/_uploads/r1Xt1VnzJl.png) * Class Adapter ![image](https://hackmd.io/_uploads/SkRYkVhGyl.png) * Object Adapter vs. Class Adapter ![image](https://hackmd.io/_uploads/rJXnJE3fke.png) ### State 關鍵字:states of an object 什麼狀態做什麼事,討論狀態的變化 #### Requirements Statement 範例:口香糖機器 ![image](https://hackmd.io/_uploads/rJkuYm3zJx.png) 需要考慮到狀態的變化:投錢、退錢、售完、按下按鈕等等 ![image](https://hackmd.io/_uploads/H15uYmnM1l.png) ![image](https://hackmd.io/_uploads/S1AFY7nGyx.png) 重構: 1. Encapsulate what varies 將狀態抽出 ![image](https://hackmd.io/_uploads/r1ZRqQhfkl.png) 2. Generalize common features 每個class負責一個狀態,該狀態應該怎麼做 ![image](https://hackmd.io/_uploads/rJqAcmnzJx.png) 3. Program to an interface, not an implementation ![image](https://hackmd.io/_uploads/SJMzi7nfJg.png) #### Recurrent Problem ![image](https://hackmd.io/_uploads/S1xK27hGJl.png) ### Visitor 關鍵字:Operations that can be applied to objects without changing their classes 可以動態加入一群物件的行為 #### Requirements Statement 範例:Compiler and AST ![image](https://hackmd.io/_uploads/H1IgDHSmJx.png) 父親擴充了新的方法,兒子也要增加 重構: 1. Encapsulate what varies ![image](https://hackmd.io/_uploads/SJbRDBSm1g.png) 分成一個class,但兩個不同的方法 2. Generalize common features ![image](https://hackmd.io/_uploads/H14kuBBmkx.png) 3. Program to an interface, not an implementation ![image](https://hackmd.io/_uploads/SytqKHB7kg.png) #### Recurrent Problem ![image](https://hackmd.io/_uploads/BJHhYrHX1l.png) Node 如果是病人的種類:兒童、成年人、老年人 Visitor 如果是醫生的種類:骨科、耳鼻喉科、內科 醫生根據不同病人做檢查跟觀察狀態,讓醫生作為一個Visitor檢查不同種類人的身體狀態。 ## ch7 Unit Testing ### 單元測試是什麼 * Michael Feature * 小的、快的,快速定位問題所在:0.1秒內 * 不是單元測試 * 與資料庫有互動 * 進行了網路通訊 * 接觸到檔案系統 * 需要你對環境做特定的準備(如編輯設定檔案)才能夠執行 * Roy Osherove * 一個單元測試是一段自動化的程式碼,這段程式會呼叫被測試的工作單元,之後對這個單元的單一最終結果的某些假設或期望進行驗證 * 用於確保某個工作是否正確執行 * 一個單元測試範圍,可以小到一個方法(Method),大到實現某個功能的多個類別與函數 * 測試某個工作單元,其範圍大小不限於一對一的單元測試與方法 ### 何時撰寫測試案例 * 程式開發前 * For TDD (Test-Driven Development) * 程式開發後 * For Regression Testing * 程式變成Legacy Code時 * For Refactoring * 已經寫好,可以運作的程式,但有一些氣味,需要被重構的 * 確保重構後,還能保持功能正常運行 * 書本 ![image](https://hackmd.io/_uploads/H1kMbLrQJg.png =300x) ### Refactor untestable code to testable code #### Untestable Code * 當被測試的物件依賴於另一個無法控制(或尚未實作)的物件時,要造成無法進行單元測試 * 例如依賴於一個Web Service、系統時間、執行緒、資料庫、本地檔案等 * 此時可利用Stub概念來重構解耦 (Refactor untestable code to testable code) ![image](https://hackmd.io/_uploads/BJ4YTIrmyl.png) ![image](https://hackmd.io/_uploads/rJhYTIBmyg.png) * 本來需要登入後才能驗證,利用重構,新增假資料(假裝有登入)進行測試。 * 可以讓它只為了一項工作進行單元驗證 * 如果加入登入的動作,首先速度慢,再來是增加為多個工作,那就變成整合測試,而非單元測試 ##### 解方 Steps: 1. Extract Interface as Seam ![image](https://hackmd.io/_uploads/HJsxAIS7kl.png) 2. Create Stub Class ![image](https://hackmd.io/_uploads/Sy2fAUrmJl.png) 3. Program to an interface, not an implementation ![image](https://hackmd.io/_uploads/rJlVRIHXye.png) 4. Dependency Injection ![image](https://hackmd.io/_uploads/ryvd0IBXyl.png) * 結果 ![image](https://hackmd.io/_uploads/SJCj0LrXyg.png) * 利用 ILoginManager 作為依賴注入的物件,只要 `new StubLoginManager()` 作為**假資料**就變得**可以測試**。 ![image](https://hackmd.io/_uploads/By6yJPHX1x.png) #### Tip (Stub vs. Mock) ![image](https://hackmd.io/_uploads/BJ5hmwH7Jg.png) * 因此,單元測試類型除了 * 驗證回傳值 * 驗證系統狀態 * 運用Mock即可增加一種測試類型 * **驗證互動** ## clean code 強調可讀性 ### Meaningful Names #### Use Intention-Revealing Names Choosing good names takes time but saves more than it takes 命名要有意義 ![image](https://hackmd.io/_uploads/HJXw7b-SJe.png) #### Avoid Disinformation 容易誤會、過長、太一般性的名稱 ```c int a = l; if ( O == l ) a = O1; else l = 01; ``` #### Make Meaningful Distinctions 盡量使用 source and destination ```java public static void copyChars(char a1[], char a2[]) { for (int i = 0; i < a1.length; i++) { a2[i] = a1[i]; } } ``` 是否是一樣的? ```java getActiveAccount(); getActiveAccounts(); getActiveAccountInfo(); ``` #### Use Pronounceable Names 好發音的 ``` class DtaRcrd102 { private Date genymdhms; private Date modymdhms; private final String pszqint = "102"; /* ... */ }; ``` 應改成 ``` class Customer { private Date generationTimestamp; private Date modificationTimestamp;; private final String recordId = "102"; /* ... */ }; ``` #### Use Searchable Names 容易被搜尋 ``` for (int j=0; j<34; j++) { s += (t[j]*4)/5; } ``` 應改成 ``` int realDaysPerIdealDay = 4; const int WORK_DAYS_PER_WEEK = 5; int sum = 0; for (int j=0; j < NUMBER_OF_TASKS; j++) { int realTaskDays = taskEstimate[j] * realDaysPerIdealDay; int realTaskWeeks = (realdays / WORK_DAYS_PER_WEEK); sum += realTaskWeeks; } ``` #### Member Prefixes 多餘的 ``` public class Part { private String m_dsc; // The textual description void setName(String name) { m_dsc = name; } } ``` 應改成 ``` public class Part { String description; void setDescription(String description) { this.description = description; } } ``` #### 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 除錯 ![image](https://hackmd.io/_uploads/ryva5b-BJe.png) #### Extract Try/Catch Blocks try exceptions 的內容不要太長 #### How Do You Write Functions Like This? 程式很難一開始就乾淨 ### Comments #### Comments Do Not Make Up for Bad Code 不好的code 重構 > 補充註解 #### Explain Yourself in Code 用 function 的名稱替代多餘的註解 ![image](https://hackmd.io/_uploads/SJvbxG-HJg.png) ### Good Comments #### Legal Comments #### Informative Comments 不得不去解釋 ![image](https://hackmd.io/_uploads/r1OFgfZrkl.png) #### Explanation of Intent 解釋動機、當初的決策 WHY TO DO ![image](https://hackmd.io/_uploads/rJh2gzbB1x.png) #### Clarification 無法修改,但概念不清楚或模糊的 ![image](https://hackmd.io/_uploads/SJwzZzWB1e.png) #### Warning of Consequences 解釋後果 ![image](https://hackmd.io/_uploads/r1dPbGWHJx.png) #### TODO Comments 該去寫,但還未寫 ### Bad Comments #### Mumbling 讀完註解,但還是不清楚 ![image](https://hackmd.io/_uploads/B11yMMZrJg.png) #### Redundant Comments 不需要去解釋,冗餘的註解 ![image](https://hackmd.io/_uploads/BJxBGGbHJl.png) #### Mandated Comments 除非外部需要,否則內部使用不用加 ![image](https://hackmd.io/_uploads/H1u0GGWSyx.png) #### Noise Comments ![image](https://hackmd.io/_uploads/rkxGYXG-rke.png) #### Don’t Use a Comment When You Can Use a Function or a Variable 如果你可以用code直接表達清楚,使用重構,就不需要註解 ![image](https://hackmd.io/_uploads/rkdp7MWr1l.png) #### Commented-Out Code 被註解的code,可以刪除就刪除掉。 #### Nonlocal Information 註解放的跟 code 太遠了 ### Formatting 長度 ![image](https://hackmd.io/_uploads/rJRDHG-HJx.png) #### Vertical Openness Between Concepts 斷空白行 ![image](https://hackmd.io/_uploads/SkpFrMbrJg.png) #### Vertical Density 內容的密度高 ![image](https://hackmd.io/_uploads/HkgJpSGWByg.png) #### Vertical Distance 呼叫跳來跳去 #### Variable Declarations Variables should be declared as close to their usage as possible. #### Dependent Functions 互相呼叫的如果可以就放在一起 #### Conceptual Affinity 概念一樣就放在一起 #### Horizontal Openness and Density 水平的空白,方便閱讀 ![image](https://hackmd.io/_uploads/By0gwGWByl.png) #### Horizontal Alignment 水平對齊 應使用下方的code,讓type跟name較接近 也方便新增跟修改 ![image](https://hackmd.io/_uploads/Sk9DvGWSyg.png) #### Breaking Indentation scopes 應隔開 應使用下方的code ![image](https://hackmd.io/_uploads/Hy2edG-SJl.png) ### Objects and Data Structures #### Data/Object Anti-Symmetry 不一定都要化為 Object 或切成多型。 #### Data Abstraction 抽象化,不需要接露實作細節 ![image](https://hackmd.io/_uploads/Skpa_z-B1x.png) #### Data Structure 如果不會再新增了,就化繁為簡,不需要另外建樹了。 ![image](https://hackmd.io/_uploads/rk2DFMWHye.png) ## 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 * units of deployment * COMPONENT COHESION 內聚力(裡面) * REP: 元件必須有明確的發布流程和版本管理,才能確保被有效且可靠地重用。 * CCP: 把一起改動的類別放在同一個元件裡,把不同原因改動的類別分開,避免無謂的影響擴散(SRP的延伸)。 * CRP: 把經常一起使用的類別放在同一個元件中,避免把不相關的類別放在一起,減少不必要的依賴關係,內聚力就高。 * COMPONENT COUPLING (外面) * 不要循環依賴 * DIP 依賴反轉原則: 元件之間應透過抽象進行依賴,而不是直接依賴具體實現,從而降低耦合性並提高系統的可擴展性和穩定性。 ### The Clean Architecture * Architecture 就是畫線:畫出邊界跟區塊 ![image](https://hackmd.io/_uploads/S1qJwBqSyl.png) * 精神:把重要跟不重要的做切割 * Clean Architecture:獨立且易於維護的軟體系統,將不同部分進行明確的分層,讓系統具備靈活性和可測試性。 ![image](https://hackmd.io/_uploads/SyFQtrcByl.png) * 獨立於框架 * 可測試姓 * 獨立於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 - Domain 由 object 組成跟流動 #### 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 <img src="https://hackmd.io/_uploads/rkOkv5Cm1l.png" width="300"> - 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:先寫測試 ![image](https://hackmd.io/_uploads/Hk_63aP4yl.png =300x) 寫測試 寫程式 重構 符合 SOLID 原則 有效的測試 修改設計的需求 再去修改設計 專注於需求的修改 透過寫好的測試 保護重構後程式的完整性 先列測項 跟客戶確認好需求 而不是撰寫時再思考需求。 TDD每一步的大小 釐清需求 用測試來記錄下 來告訴程式的正確與否 ### 心得 在該堂課中,我們進行了 FizzBuzz 遊戲 的實作,這是一個經典且簡單的練習,但課堂中最大的收穫不只是完成遊戲本身,而是模擬了面對客戶需求變更時 的情境,讓我學習到如何在有限的時間內保持程式的彈性並快速回應變化。 一開始,我們按照最基本的需求實作 FizzBuzz,例如輸出 1 到 100 的數字,其中 3 的倍數輸出 "Fizz",5 的倍數輸出 "Buzz",同時是 3 和 5 的倍數時輸出 "FizzBuzz"。然而在後續的過程中,老師(或模擬的客戶)陸續提出了新的需求,例如: * 增加其他倍數的條件輸出特定文字。 這樣的變更讓我體會到,實務中不可能所有需求一開始就明確,客戶往往會根據他們看到的結果提出新的想法,這也導致說一個系統的設計,必須寫得有"彈性"並且易於"理解"與修改,才能在有限的時間內,去滿足客戶提出的需求。 #### 關於遊戲實作的想法 ##### 遊戲實作的問題 ```java public class FizzBuzz { public String convert(int number) { List<String> result = new ArrayList<String>(); for (int i = 1; i <= number; i++) { if (i % 3 == 0 && i % 5 == 0) { result.add("FizzBuzz"); } else if (i % 3 == 0 && i % 7 == 0) { result.add("FizzDizz"); } else if (i % 5 == 0 && i % 7 == 0) { result.add("BuzzDizz"); } else if (i % 3 == 0 && i % 5 == 0 && i % 7 == 0) { result.add("FizzBuzzDizz"); } else if (i % 3 == 0) { result.add("Fizz"); } else if (i % 5 == 0) { result.add("Buzz"); } else if (i % 7 == 0) { result.add("Dizz"); } else { result.add(""+i); } } return String.join(" ", result); } } ``` 原本的程式,是按照直覺以及需求的順序,一一將規則寫入其中,這樣的程式雖然可以正確地運行,但存在一些問題包括: 1. 難以修改跟擴充 每當新增或修改一個規則時,都必須重新編寫 if-else 邏輯。 2. 重複邏輯 程式碼變得冗長且不易理解。`i % 3 == 0、i % 5 == 0、i % 7 == 0`,不但具備重複的程式邏輯,還使用了魔術數字。 3. 違反SRP 和 OCP 原則 * SRP:convert 負責了兩件事情,包括遍歷數字與判斷輸出的內容。 * OCP:面對新規則(新需求)時,需要修改現有邏輯。 ##### 實作如何改善 ```java public class FizzBuzz { private final List<Rule> rules; public FizzBuzz() { this.rules = createDefaultRules(); } // 輸出 1 ~ number 的 FizzBuzz 結果 public String convert(int number) { List<String> result = new ArrayList<>(); for (int i = 1; i <= number; i++) { result.add(applyRules(i)); } return makeReturnValue(result); } // 將FizzBuzz的規則放入rules中 private List<Rule> createDefaultRules() { List<Rule> rules = new ArrayList<>(); rules.add(new Rule(3, "Fizz")); rules.add(new Rule(5, "Buzz")); rules.add(new Rule(7, "Dizz")); return rules; } // 將規則套用到number上 private String applyRules(int number) { StringBuilder result = new StringBuilder(); for (Rule rule : rules) { if (rule.appliesTo(number)) { result.append(rule.getValue()); } } return handleEmptyValue(result.toString(), number); } // 處理空值 private String handleEmptyValue(String value, int number) { return value.isEmpty() ? String.valueOf(number) : value; } // 將結果串接成字串 private String makeReturnValue(List<String> result) { return String.join(" ", result); } // FizzBuzz 的判斷規則 private static class Rule { private final int divisor; private final String value; public Rule(int divisor, String value) { this.divisor = divisor; this.value = value; } public boolean appliesTo(int number) { return number % divisor == 0; } public String getValue() { return value; } } } ``` 改善後的地方: 1. SRP:讓每個方法負責一件工作。 * `Rule` 負責遊戲規則的制定 * `convert` 負責輸出 1 ~ number 的 FizzBuzz 結果 2. OCP:易於修改與擴增 * 有新規則(需求)時,只要添加到 `createDefaultRules` 方法中即可: ```java rules.add(new Rule(11, "Jazz")); rules.add(new Rule(13, "Pop")); ``` 這樣的設計符合 SOLID 原則,還讓程式碼能夠應對新的需求跟變化,更符合實際工作的情況。 #### 關於單元測試的想法 ```java class FizzBuzzTest { @Test void test21() { FizzBuzz fizzBuzz = new FizzBuzz(); String actual = fizzBuzz.convert(21); Assertions.assertEquals("1 2 Fizz 4 Buzz Fizz Dizz 8 Fizz Buzz 11 Fizz 13 Dizz FizzBuzz 16 17 Fizz 19 Buzz FizzDizz", actual); } } ``` 原本的單元測試,需要人工將規則一一列出,所以我萌發了一個想法: **我能否重構單元測試呢?** 使用原本通過單元測試但尚未重構的程式碼,作為單元測試,用來測試重構後的程式: ```java class FizzBuzzTest { @Test void test500() { // 使用舊版 FizzBuzz 類別 FizzBuzz oldFizzBuzz = new FizzBuzz(); String oldResult = oldFizzBuzz.convert(500); // 使用重構後的 FizzBuzz 類別 FizzBuzz refactoredFizzBuzz = new FizzBuzz(); // 假設這是重構後的程式 String refactoredResult = refactoredFizzBuzz.convert(500); // 驗證兩個結果是否相同 Assertions.assertEquals(oldResult, refactoredResult, "重構後的結果與舊版結果不符"); } } ``` 這樣的測試確保了重構後的程式不會改變原本的功能,並且還能測試更大的數字(比如500, 1000)來避免潛在的邏輯錯誤或效能問題。 ## 演講課 - 軟體架構設計 https://gelis-dotnet.blogspot.com/ ### 工程師成長階段 ![image](https://hackmd.io/_uploads/rJDDLpn81l.png) * 基礎(學習):模仿→懂學習 * DRY(工作):能應付工作需求→懂程式 * 分層Design Pattern(解決技術債):問題解決,自我成長→懂Pattern(好程式) * OOD(設計):抽象化→懂設計 * OOA(分析):溝通協作→懂協作 * 專案分享:傳承(透過教學了解細節)→懂細節 * 系統整合:流程改善(好方法)→有好的開發流程 * 獨立作業:顧問(獨立)→有自己的一套方法 * 跨團隊:專家 * 教練/工匠:勘誤 工程師的成長是一個漸進的過程,從基礎的學習到最終成為能夠帶領他人前進的技術領袖,每個階段都蘊含著深刻的進步與挑戰。一開始,工程師多以模仿和學習為主,理解基礎的工具與技術,逐步具備完成工作需求的能力。在這個過程中,他們開始領會程式的運作邏輯,並以「不重複自己」(DRY)為原則,提升效率和質量。 隨著經驗的累積,他們會遇到更多技術債的問題,進而學習如何運用設計模式(Design Pattern)來解決這些挑戰。此時,他們不僅寫出更好的程式,還在解決問題中實現自我成長。再往前,他們進一步理解抽象化的概念,進入物件導向設計(OOD)的領域,能夠創建出可擴展、可維護的系統設計。 成長的下一步是掌握面向物件分析(OOA),這需要更多的溝通與協作能力。他們開始關注如何與團隊成員共同設計解決方案,並透過專案分享和教學來傳承知識,這不僅幫助他人,也讓他們自己更深入地了解細節。在更高的層次,他們致力於系統整合,優化開發流程,並逐漸形成自己的一套方法論,成為能獨立承擔責任的顧問。 當工程師跨越團隊,成為專家時,他們的角色更多的是指導與協調,分享經驗並推動技術進步。而作為技術教練或工匠,他們不僅專注於技術本身,還致力於培養他人,幫助團隊避免錯誤,將精益求精的精神融入整個工程文化。 這一過程顯示,工程師的成長不僅是技術的提升,更是對溝通、協作、分享和領導的全方位鍛煉。真正成熟的工程師,不僅是解決問題的高手,更是知識的傳承者與團隊的推動者。 ### 什麼是軟體架構設計 * 軟體架構的目的:不讓專案變成大泥球(亂) * 沒有軟體架構的問題: * 沒有版控 * 原始碼 * 沒有文件 * 沒有需求訪談 * 沒有規範 * 沒有時程規劃 * 沒有測試(環境) * 不求好只求有 * 軟體架構是什麼:怎麼解決 * 簡化軟體開發作業 * 統一規範(Coding style) * 組件重用性 * 一致的開發技術與架構,減少管理跟技術債 * 軟體架構**設計** * 設計什麼?:設計一個能賺錢的系統 * 讓系統變得可控、可抽換性、可靠、可維護(讓成本變低) * 成本:包含維護成本、學習門檻等 * 目標:最小化建置和維護「需求系統」的人力資源。 * 傳統階層式的問題 * 資料導向設計 * 強調底層實現細節 * 什麼是**設計**:低層次 * 聚焦於具體技術與模組實現的細節,如 API等。 * 什麼是**架構**:高層次 * 關注系統的整體結構與組織方式,如模組間的關係、業務邏輯的劃分等。 * 問題:雜亂比整潔來得快 * 設計缺乏規範會讓代碼迅速變得雜亂,隨之帶來開發與維護成本的無限增長。 * 什麼是好的架構? * 問題:實務上難以完全想清楚需求,導致系統需要不斷修改 * 畢竟不能一次到位,那就做好 Core Domain 下應該要做的事情就好 軟體架構的核心目的在於,讓一個專案變得有序並且好維護,能用更低的成本達到客戶的需求。但是呢,往往需求是會隨時間變化,難以一步到位,這就導致我們開發時,需要一個開發的準則或原則,讓工程師遵循這個框架進行設計,這就是軟體設計架構的核心概念。 這些準則不僅僅是技術層面的規範,而是提供了一套應對變化與複雜性的策略。這跟我們軟體設計所學的Design pattern與Design principles有直接的關係,比如採用模組化設計可以讓系統的不同部分彼此獨立,減少變更對全局的影響;統一的代碼風格與規範則讓團隊之間的協作更加順暢。同時,透過像 SOLID 原則這樣的設計原則,系統能更具彈性和可擴展性,為未來的修改留有足夠的空間。 但實務上,還是要考量到開發與成本的平衡,過於複雜或超前的設計會增加技術負擔,而過於簡單的設計又可能在需求變化時無法適應。因此,好的軟體架構需要專注於核心領域(Core Domain),解決當下最重要的問題,同時具備足夠的彈性來應對未來的挑戰。 ### 軟體架構設計:API 設計準則 ![image](https://hackmd.io/_uploads/ByFggZaL1e.png) * 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 不僅是一種技術實現工具,更是一種數位能力的表達方式,因此設計的重點應該放在如何清晰地傳達業務能力,而非僅僅參照後端的資料。 更強調跨團隊的協作,將前端、後端和業務人員緊密聯繫在一起,屬於一種團隊上理念與溝通的橋樑,讓開發過程更加敏捷和協作化。