Try   HackMD

軟體設計(成大)

ch2 OOP

part 1. Encapsulation

public class Duck {
 public boolean canfly = false; // instance variable
 public void quack(){
  System.out.println("Quack!!");
 }
}

封裝 Encapsulation

將程式切割成一塊塊模組
降低程式的耦合度、增加可控度
避免被修改的風險

public class Duck {
 private boolean canfly = false; //**
 public boolean getCanfly(){
  return canfly;
 }}

多型 Polymorphism

Method Overloading

在同個 calss,方法名一樣,相同或高相似度的行為,但不同實作方式的同名函式。
增加可讀性

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 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 →

variable2 = sampleVariable
variable2 // 指向同一個 sampleVariable
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

 }
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)

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。

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;
 }
}
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,通常在父親。

public abstract class Animal {
 public abstract void run();
 public void sit(){ System.out.println(Sit down…”); }
}

必讓兒子去實作run(),在下面層級能讓其做不同的行為

public class Dog extends Animal {
 public void run(){
 System.out.println("The dog is running");
 }
}
public class Cat extends Animal{
 public void run(){
 System.out.println("The cat is running");
 }
}

Interface

public interface Shape {
 int color = 1; // => public static final int color = 1; 常數
}
public class Paint {
 public static void main(String[] args) {
  System.out.println(Shape.color);
 }
}
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 時就決定,靜態決定。

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時才決定,動態決定。

public class Payment {
 public void pay(){
  System.out.println("Pay in cash");
 }
 public void checkout(){
  pay();
 }
}

public class CreditCardPayment extends Payment{
 public void pay() {
  System.out.println("Pay with credit card");
 }
}

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
Payment p2 = new CreditCardPayment();
p2.checkout();
Downcasting
Payment p1 = new Payment();
CreditCardPayment p2 = (CreditCardPayment)p1; //runtime error

ch3 UML Class Diagram

Class Notation

image

Abstract and Interface

Class name and Method 為斜體字

image
image

Relationship

Generalization 概括(抽象化)

是否有 is-a 的關係?
classInterface/Abstract class 的差別

image
image

image

Dependency 依賴

是否有 uses-a 的關係?
通常為 Method 的 ParametersLocal Variable
虛線:關係較薄弱

image

public class Tourist {
 public void buy(TicketCounter tc) {
  tc.sellTicket(); // use TicketCounter's Method
 }
}
public class TicketCounter {
 public String sellTicket() {
  return "Random Ticket No.";
 }
}

image

Association

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

image

import java.util.ArrayList;
 public class Library {
  private ArrayList<Book> books = new ArrayList<Book>();
  public void addBook(Book book) { // 創建了穩固的 book 實例
   books.add(book);
  }
}
public class Book {
 private String title;
}

image

Bidirectional Association 雙向關係

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

image

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);
 }
}
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

public class Enrollment {
 private Student student;
 private Course course;
 public Enrollment(Student student,Course course) {
  this.student = student;
  this.course = course;
 }
}
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);
 }
public class Course {
 private List<Enrollment> enrollments = new ArrayList<>();
 public void add(Enrollment enrollment){
  enrollments.add(enrollment);
 }
Aggregation 聚合

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

image
image

但兩者實作方式一樣

Composition

為較「強烈」「包含/擁有」的關係
whole消失,part可繼續存在
使用 實心菱形

image
image

  1. 封裝在裡面
class Car {
 private Engine engine;
 public Car() {
  this.engine = new Engine();
 }
 private class Engine {
  ...
 }
}
  1. 放在外面,確保其他物件不持有 part 物件的 Reference
class Car {
private Engine engine;
public Car() {
 this.engine = new Engine();
}
class Engine {
 ...
}
  1. WeakReference
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
無法產生菱形箭頭,實作一樣

例題

  1. A country has a capital city.
    image
public class Country {
	private String name;
	private City CapitalCity;
	
	public Country(String name, City city) {
		this.name = name;
		this.CapitalCity = city;
	}
}
public class City {
	private String name;
}
  1. A dining philosopher is using a fork.
    image
public class DiningPhilosopher {
	public void useFork(Fork fork) {
		fork.eat();
	}
}
public class Fork {
	public void eat() {
		// 實作
	}
}
  1. A file is an ordinary file or a directory file.
    image
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();
}
public class OrdinaryFile extends File{
	public OrdinaryFile(String name) {
		super(name);
	}
	@Override
	public void open() {// 實作}
	@Override
	public void close() {// 實作}
}
public class DirectoryFile extends File{
	public DirectoryFile(String name) {
		super(name);
	}
	@Override
	public void open() {// 實作}
	@Override
	public void close() {// 實作}
}
  1. Files contain(包含) records.
    image
public class File {
	private String name;
	private Record records;
}
public class Record {
	private int id;
}
  1. A polygon is composed of points.
    image
import java.util.List;
import java.util.ArrayList;

public class Polygon {
	private List<Point> points;
	
	public Polygon() {
		points = new ArrayList<>();
	}
}
public class Point {
	private double x;
	private double y;
	
	public Point(double x, double y) {
		this.x = x;
		this.y = y;
	}
}
  1. A drawing object is a text, a geometrical object, or a group.
    image
public interface DrawingObject {
	void draw();
}
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);
	}
	
}
public class GeometricObject implements DrawingObject{
	private String shape;  

    public GeometricObject(String shape) {
        this.shape = shape;
    }
    public void draw() {
    	 System.out.println("Drawing shape: " + shape);
    }
}
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
image
Dependency(依賴) Association(關聯)
Parameters 或 Local Variable 被設為Attribute
uses-a 短期使用(臨時) has-a 長期擁有(結構)
箭頭虛線 箭頭實線
image
image
Bidirectional Association(雙向關聯) Aggregation(聚合) Composition
互相被設為Attribute whole消失 part不消失 whole消失 part也消失
互相擁有 弱引用(擁有) 強引用(擁有)
實線無箭頭 實線空心菱形 實線實心菱形
image
image
image

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
  2. 更新遺漏的Dependency
    修正如下【Ctrl + a 全選】➔【在任一class上右鍵】➔【Add】➔【Dependencies】
  3. 更新Layout
    【畫面右鍵】➔【Layout Diagram】

Levels of Abstraction in Java Code Structure

image

Package Level

image

  • Cyclic dependency:問題
  • Unidirectional dependency:理想
資訊減量

只看重要的部分

image

Class Level(Intra-Package)

  1. 關注依賴於高階抽象(interfaces、abstract classes)或低階實體(sub-classes)
  2. 關注bidirectional dependencies
    image
資訊減量
  • 可隱藏核心外如data classes、utility classes、exception classes、enums、composition root、UI-layer classes
  • 可隱藏dependencies,只顯示實線(inheritance,implementation與association)
    image
  • Isolated classes (獨立的classes)
  • 重複的association lines
  • composition root
    image
何謂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

Class Level (Cross-Package)

image

資訊減量

image
image

image

  • 直接將大量所有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

Memory Leak

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

image

Long method

沒有標準:一個畫面的長度、參考行數、程式語意跟 Method name 是否吻合

問題:

  1. 不易讀、難理解
  2. 不易命名(方法的語意)
  3. 不易reuse

方法:用 Extract Method 抽出來成短 method

image

Feature Envy

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

image

Unsuitable naming

不適當命名

image

Downcasting

向下轉型

image
image

但有不得不的狀況:

  1. api 不給動
  2. 舊程式
  3. Deserialization

Loop termination conditions are obvious and invariably achievable

結束條件不明顯

image

Parentheses are used to avoid ambiguity

不明確的括號,造成問題或不易讀

image

Lack of comments

缺少註解:不易讀

  • 結論:有需要再加,不需要就不用
    • 避免 comments 跟 code 不一致,修改 code 但 comments 沒有更新

Files are checked for existence before attempting to access them

讀檔要確認有正確載入

if (inputFileStream.is_open()) {
 // do something
}

Duplicated Code

重複的 code

image

Access modifiers

All methods have appropriate access modifiers and return types

適當的存取和返回

image

Redundant or Unused variables

沒用到、沒用的變數

Indexes or subscripts are properly initialized, just prior to the loop

沒有初始化或沒有宣告值

image

Is overflow or underflow?

數字是否超過型態的範圍

注意大數字

image

Are divisors tested for zero?

分母為'0',除法時要做判斷

Inconsistent coding standard

應符合程式風格

image

Data clumps

image

重複的 data 變數可以抽出class

image

Simulated Polymorphism

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

Large class

沒有 Single Responsibility Principle (SRP)

image
能根據語意再次拆分,使用者/設定/log/檔案處理。

Long parameter list

參數多,代表參數可能也可以分群

image

Message Chains

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

重構1:新增Method

不跟陌生人講話,透過窗口對話。
Client only talks to Company(Demeter’s Law)

image
image

image

違反RSP

重構2:新增中介Class(Façade Pattern)

新增一個窗口或API(CompanyService)幫我呼叫
讓這個窗口符合它的SRP,就是為了幫我呼叫的任務

image
image

總結

image

Literal constants

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

image

uncalled or unneeded procedures or any unreachable code

不需要存在

image

switch statement have a default

image

comparing floating-point numbers for equality

image

Divergent Change(發散式改變)(*)

一個類別會因為要因應太多的變更原因而需修改
方法:利用 Extract Class 重構

  • 範例:
    image
  • 重構:
    image

    符合SRP(是否同一個Class中的Methods相互依賴或共用屬性)

舉例:
會因為編碼方式、傳輸方式、解碼方式多種原因需要修改該class

class VideoService {
    public void encodeVideo(String format, String filePath) {
        // 將影片編碼成指定格式
    }

    public void transmitVideo(String protocol, String filePath) {
        // 通過指定協議傳輸影片
    }

    public void decodeVideo(String filePath) {
        // 解碼影片
    }
}
// 負責影片編碼的類別
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

  • 範例:
    更改主題,要手動傳入新的主題設定,黑底要白字,白字要黑底,它們不會一起連動。
public class ThemeApp {
 public static void main(String[] args) {
  ThemeApp app = new ThemeApp();
  Button button = new Button("Dark");
  TextBox textBox = new TextBox("White");
  // 更改主題,要手動傳入新的主題設定,黑底要白字,白字要黑底,它們不會一起連動。
 }
}

改善後

// 抽象主題接口
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);

    }
}
  • 舉例(按鈕):
    原本要修改所有按鈕的顏色要一個一個修改
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");
    
    }
}

這裡我可以一次修改所有按鈕顏色

// 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
  • 舉例:
public class Person {
    private String name;
    private String idCard; // 身分證 ID 用 String 來表示

    public Person(String name, String idCard) {
        this.name = name;
        this.idCard = idCard;
    }

改善後:

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
重構:
image

Alternative Classes with Different Interfaces

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

image
重構:
image

image

Refused Bequest

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

image
重構:
image

  • 舉例:
    飛行器中有翅膀的屬性,雖然直升機也是飛行器但它沒有翅膀。
public class FlyingVehicle {
    private String engine;
    private String wings;  // 不適用於所有飛行器,直升機不需要翅膀
}
public class Helicopter extends FlyingVehicle {
    private String rotor;
}

改善後:

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

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

Middle Man

多餘的,沒有功能的中間人,只是在傳遞事情。

那個中間人同時也是 Feature Envy

image

  • 舉例
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

  • 舉例:
// 訂單類別,可能會有多種不同的訂單類型
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

具體判定法

  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
  • 重構範例:
    image

    把相互依賴的,經過拆解分群,以增加內聚力:
    image
語意結合判定法
  • 用語意判定是否合理
  • 依每個method填入下表,構成語句:The Automobile (主詞) starts (動詞) itself.
  • 判定此句子是否具合理語意,若合理則留下,若不合理則考慮將此method移出此class
    image
    image
  • 拆分以符合SRP
    image
總結:兩者判定法的限制
  • 當一個class內method間結構關係複雜時,結構內聚力判定法可能較困難
  • 當一個class name語意太general時(如XXXManager/Controller),會讓所有method name都可與class name語意結合,造成語意結合判定失效

Open-Close Principle(開放關閉原則)OCP

  • Open for extension, but closed for modification
    一個模組必須有彈性的開放往後的擴充,並且避免因為修改而影響到使用此模組的程式碼。
    image

不能有封閉迴路circle

image

  • 舉例
    根據不同的員工類型計算薪資
    會因為增加不同的員工類型而受改變
// 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;
    }
}

改善後:

// 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

  • 舉例:正方形不是一種長方形,不能取代。
    正方形不能被設定成不同長寬,所以不是長方形。

    image

  • 改善:

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
窗口在interface介面上
image

  • 舉例
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.");
    }
}

改善後:

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
介入一層中間層(介面或抽象層):
image

利用 Dependency Injection 的概念。

// 定義抽象的資料傳輸接口
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
    image

  • 舉例
    攻擊寫在角色的類別中,假設有不同的角色,那麼攻擊行為就必須被擴充或修改

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

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

Acyclic Dependencies Principle (ADP)

image
image

難以符合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

重構:

  1. Encapsulate what varies
    會改變的做封裝處理,抽出
    image
  2. Generalize common features
    建樹
    image
  3. Program to an interface, not an implementation
    使用樹的父親,對口interface,方便擴充跟修改
    image

Recurrent Problems

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

image

Composite & Decorator

關鍵字:Structure and composition of an object
面對的問題是由 object 組成

Requirements Statement

範例:小畫家
如果要畫三角形,就有新的問題

image

重構:

  1. Generalize common features
    建樹
    image
  2. Program to an interface, not an implementation.
    使用父親
    image

    image

Recurrent Problem

image

Decorator Pattern

關鍵字:Responsibilities of an object without subclassing
動態地為一個物件添加行為或職責,而不需要透過繼承來實現。

Requirements Statement

範例:Starbuzz Coffee
cost 需要因應新服務而改變,變得不穩定

image

重構:

  1. Encapsulate what varies
    image
  2. Generalize common features
    概念是讓 condiment 也是一種食物配料
    image
  3. Program to an interface, not an implementation.
    image

    裝飾它
    image

Recurrent Problem

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

image

Composite vs. Decorator

image

Factory Method & Abstract Factory

關鍵字:Subclass of object that is instantiated

Requirements Statement

範例:披薩店

image
新增新的披薩會有問題
image

重構:

  1. Encapsulate what varies
    image
  2. Generalize common features
    image

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

Recurrent Problem

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

image

補充:Parallel Inheritances Hierarchies問題

產品跟生產方式可能有配對的關係

image
能夠讓兒子去強制配對
image

Abstract Factory Pattern

關鍵字:Families of product objects

Requirements Statement

範例:佈景主題

image

重構:

  1. Encapsulate what varies
    image
  2. Generalize common features
    image
  3. Program to an interface, not an implementation.
    生產一堆東西,抽出成工廠,限制其必須要實作
    image

Recurrent Problem

image

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

重構:

  1. Encapsulate what varies
    image
  2. Generalize common features
    image

Recurrent Problem

image

Adapter

關鍵字:Interface to an object
Interface 被改變了

Requirements Statement

範例:
New Vendor in Existing Software

範例1:Object Adapter

image
更改API
image

重構:

  1. Encapsulate what varies
    image
  2. Generalize common features
    兒子去綁定不同的API
    image

範例2:Class Adapter

image

重構:

  1. Encapsulate what varies
    image
  2. Generalize common features
    image

Recurrent Problem

  • Object Adapter
    image
  • Class Adapter
    image
  • Object Adapter vs. Class Adapter
    image

State

關鍵字:states of an object
什麼狀態做什麼事,討論狀態的變化

Requirements Statement

範例:口香糖機器

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

image

重構:

  1. Encapsulate what varies
    將狀態抽出
    image
  2. Generalize common features
    每個class負責一個狀態,該狀態應該怎麼做
    image
  3. Program to an interface, not an implementation
    image

Recurrent Problem

image

Visitor

關鍵字:Operations that can be applied to objects without changing their classes
可以動態加入一群物件的行為

Requirements Statement

範例:Compiler and AST

image
父親擴充了新的方法,兒子也要增加

重構:

  1. Encapsulate what varies
    image

    分成一個class,但兩個不同的方法
  2. Generalize common features
    image
  3. Program to an interface, not an implementation
    image

Recurrent Problem

image
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

Refactor untestable code to testable code

Untestable Code

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

image
image

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

Steps:

  1. Extract Interface as Seam
    image
  2. Create Stub Class
    image
  3. Program to an interface, not an implementation
    image
  4. Dependency Injection
    image
  • 結果
    image
  • 利用 ILoginManager 作為依賴注入的物件,只要 new StubLoginManager() 作為假資料就變得可以測試
    image

Tip (Stub vs. Mock)

image

  • 因此,單元測試類型除了
    • 驗證回傳值
    • 驗證系統狀態
  • 運用Mock即可增加一種測試類型
    • 驗證互動

clean code

強調可讀性

Meaningful Names

Use Intention-Revealing Names

Choosing good names takes time but saves more than it takes
命名要有意義

image

Avoid Disinformation

容易誤會、過長、太一般性的名稱

int a = l;
if ( O == l )
 a = O1;
else
 l = 01;

Make Meaningful Distinctions

盡量使用 source and destination

public static void copyChars(char a1[], char a2[]) {
 for (int i = 0; i < a1.length; i++) {
  a2[i] = a1[i];
 }
}

是否是一樣的?

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

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

Good Comments

Informative Comments

不得不去解釋

image

Explanation of Intent

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

image

Clarification

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

image

Warning of Consequences

解釋後果

image

TODO Comments

該去寫,但還未寫

Bad Comments

Mumbling

讀完註解,但還是不清楚

image

Redundant Comments

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

image

Mandated Comments

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

image

Noise Comments

image

Don’t Use a Comment When You Can Use a Function or a Variable

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

image

Commented-Out Code

被註解的code,可以刪除就刪除掉。

Nonlocal Information

註解放的跟 code 太遠了

Formatting

長度

image

Vertical Openness Between Concepts

斷空白行

image

Vertical Density

內容的密度高

image

Vertical Distance

呼叫跳來跳去

Variable Declarations

Variables should be declared as close to their usage as possible.

Dependent Functions

互相呼叫的如果可以就放在一起

Conceptual Affinity

概念一樣就放在一起

Horizontal Openness and Density

水平的空白,方便閱讀

image

Horizontal Alignment

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

image

Breaking Indentation

scopes 應隔開
應使用下方的code

image

Objects and Data Structures

Data/Object Anti-Symmetry

不一定都要化為 Object 或切成多型。

Data Abstraction

抽象化,不需要接露實作細節

image

Data Structure

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

image

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
    • 精神:把重要跟不重要的做切割
  • Clean Architecture:獨立且易於維護的軟體系統,將不同部分進行明確的分層,讓系統具備靈活性和可測試性。
    image
    • 獨立於框架
    • 可測試姓
    • 獨立於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
      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:先寫測試

image

寫測試 寫程式 重構

符合 SOLID 原則
有效的測試

修改設計的需求 再去修改設計

專注於需求的修改

透過寫好的測試 保護重構後程式的完整性

先列測項 跟客戶確認好需求
而不是撰寫時再思考需求。

TDD每一步的大小
釐清需求 用測試來記錄下 來告訴程式的正確與否

心得

在該堂課中,我們進行了 FizzBuzz 遊戲 的實作,這是一個經典且簡單的練習,但課堂中最大的收穫不只是完成遊戲本身,而是模擬了面對客戶需求變更時 的情境,讓我學習到如何在有限的時間內保持程式的彈性並快速回應變化。

一開始,我們按照最基本的需求實作 FizzBuzz,例如輸出 1 到 100 的數字,其中 3 的倍數輸出 "Fizz",5 的倍數輸出 "Buzz",同時是 3 和 5 的倍數時輸出 "FizzBuzz"。然而在後續的過程中,老師(或模擬的客戶)陸續提出了新的需求,例如:

  • 增加其他倍數的條件輸出特定文字。

這樣的變更讓我體會到,實務中不可能所有需求一開始就明確,客戶往往會根據他們看到的結果提出新的想法,這也導致說一個系統的設計,必須寫得有"彈性"並且易於"理解"與修改,才能在有限的時間內,去滿足客戶提出的需求。

關於遊戲實作的想法

遊戲實作的問題
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:面對新規則(新需求)時,需要修改現有邏輯。
實作如何改善
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 方法中即可:
    ​​​​rules.add(new Rule(11, "Jazz"));
    ​​​​rules.add(new Rule(13, "Pop"));
    

這樣的設計符合 SOLID 原則,還讓程式碼能夠應對新的需求跟變化,更符合實際工作的情況。

關於單元測試的想法

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);
	}
}

原本的單元測試,需要人工將規則一一列出,所以我萌發了一個想法:

我能否重構單元測試呢?

使用原本通過單元測試但尚未重構的程式碼,作為單元測試,用來測試重構後的程式:

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

  • 基礎(學習):模仿→懂學習
  • 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

  • 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 不僅是一種技術實現工具,更是一種數位能力的表達方式,因此設計的重點應該放在如何清晰地傳達業務能力,而非僅僅參照後端的資料。

更強調跨團隊的協作,將前端、後端和業務人員緊密聯繫在一起,屬於一種團隊上理念與溝通的橋樑,讓開發過程更加敏捷和協作化。