Kent Beck Implementation Pattern Principles(6)-Rate of Change
介紹Kent Beck Principle的最後一個原則: Rate of Change
中文意思為: 改變的頻率,這跟程式有甚麼關係呢?
Implementation Pattern原文 :
A final principle is to put logic or data that changes at the same rate together and separate logic or data that changes at different rates. These rates of change are a form of temporal symmetry.
大意為 : 運算(Logic)跟資料(Data)之間改變的頻率要一致,當不一致時分離兩段改變頻率不一致的程式碼。
聽起來有點抽象,但這在物件導向中是很重要的,我們都知道物件是封裝Logic跟Data並且提供Client Public API讓Client可以不用知道Public API的實作細節(Know What Not How),這樣的好處是當物件內部實作改變時不會影響到其他系統,也就是之前介紹過的Local Consequence原則。
但到底哪些邏輯跟資料要放在一起 ? 那些不用放在一起 ?
這就是Rate of Change原則可以幫助我們決策的地方。
依照Rate of Change舉例 :
- Model和View改變的頻率會不會一樣?
- Domain和Database改變的頻率會不會一樣?
上述兩個問題應該很簡單,
- 因為不一樣所以透過MVC Pattern來解決問題。
- 因為不一樣所以透過ORM Mapper來解決問題。
但其實上述的Pattern都是為了達到Separate Different Rate of Change的效果。格局再放大一點也就是Separation of Concerns。
兩個Pattern要解決的問題都不一樣,但是背後的動機卻都是差不多的。
也就是之前提到過Pattern是針對Specific Context(上述針對顯示和資料儲存問題),而Principle提供Pattern背後的動機,Principle較為General而Pattern較為Specific。
講了這麼多為什麼把改變頻率不一致的邏輯放在一起不好呢 ? 原因如下 :
- 不同的改變頻率代表此物件要滿足多個Client的需求。
- 不同的Client之間會產生Implicit Coupling。
- 當因為Client A的需求修改此物件時,會間接影響到Client B(因為Client B也對此物件有耦合關係),這正是Code Smell Shotgun Surgery。
- 造成修改商業邏輯(Domain)影響資料庫(Persistence),修改畫面(View)影響商業邏輯(Domain)
記得之前在PTT SoftJob看到一則留言,修改Web前端會影響後端MVC框架,雖不知道他們詳細情形,但很可能就是沒有針對改變頻率不一致的程式碼做分割(Separate Different Rate of Change)。
接下來透過例子讓大家更能明白Rate of Change的含意 :
public class Person {
private String firstName;
private String middleName;
private String lastName;
private Money money;
public Person(final String firstName, final String middleName, final String lastName) {
this.firstName = firstName;
this.middleName = middleName;
this.lastName = lastName;
this.money = Money.zero();
}
public Money getMoney() {
return this.money;
}
public void deposit(Money money) {
this.money = money.add(money);
}
public void setFirstName(String firstName) {
if (firstName.length() > 5)
this.firstName = firstName;
throw new RuntimeException("First Name Should Large Than Five");
}
public void setMiddleName(String middleName) {
this.middleName = middleName;
}
public void setLastName(String lastName) {
if (lastName.length() > 3)
this.lastName = lastName;
throw new RuntimeException("First Name Should Large Than Three");
}
}
備註:
- 假設FirstName的長度至少要大於5,LastName的長度至少要大於3。
- 丟Exception只是為了Demo,不是很好的示範。
上述程式相信很多人都曾寫過,但這種程式卻包含很多淺在設計的問題 :
- Code Smell Primitive Obsession。
- 針對姓名的判斷,容易有重複程式碼,違反DRY。
- Person被迫處理複雜的驗證邏輯,導致Cohesion降低。
- firstName、middleName、lastName改變的頻率都相同,沒有適當封裝。
我們提到的Rate of Change,不僅僅適用於邏輯的責任分配,同時也適用於物件屬性(private member field in java)。
物件屬性改變的頻率也要一致,我們從Person物件可以看出,firstName、middleName、lastName改變的頻率明顯和Money屬性變化的頻率不一致。
因此我們透過以下重構手段解決上述的問題
- 針對firstName、middleName、lastName跟相關的邏輯,Extract Class為PersonName。
- 讓PersonName轉變為immutable(即變成Value Object)。
修改後的程式碼如下:
public class Person {
private PersonName name;
private Money money;
public Person(PersonName name) {
this.name = name;
this.money = Money.zero();
}
public Money getMoney() {
return this.money;
}
public void deposit(Money money) {
this.money = money.add(money);
}
public void setFirstName(String firstName) {
this.name = this.name.changeFirstName(firstName);
}
public void setMiddleName(String middleName) {
this.name = this.name.changeMiddleName(middleName);
}
public void setLastName(String lastName) {
this.name = this.name.changeLastName(lastName);
}
private static class PersonName {
private final String firstName;
private final String middleName;
private final String lastName;
static PersonName create(final String firstName, final String middleName, final String lastName) {
if (firstName.length() < 5)
throw new RuntimeException("First Name Should Not Less Than Five");
if (middleName.length() < 3)
throw new RuntimeException("Last Name Should Not Lass Than Three");
return new PersonName(firstName, middleName, lastName);
}
private PersonName(final String firstName, final String middleName, final String lastName) {
this.firstName = firstName;
this.middleName = middleName;
this.lastName = lastName;
}
PersonName changeFirstName(String firstName) {
return PersonName.create(firstName, this.middleName, this.lastName);
}
PersonName changeMiddleName(String middleName) {
return PersonName.create(this.firstName, middleName, this.lastName);
}
PersonName changeLastName(String lastName) {
return PersonName.create(this.firstName, this.middleName, lastName);
}
}
}
註: 為了Demo方便而使用static inner class。
以下有幾個優點:
- 解決了Primitive Obsession Code Smell。
- 把驗證邏輯封裝在PersonName的class static factory method,遵守DRY。
- Person原本的驗證邏輯移到PersonName,增加Person的Cohesion。
- 遵守Same Rate of Change,現在Person Class所有屬性,改變的頻率都一致。
- PersonName只能透過class static factory method產生,而如果驗證不過就無法產生PersonName的Instance,遵守Fail Fast Principle,也遵守了PersonName的Invariant。
- 所有Reference到PersonName的物件,都不用擔心驗證問題,因為只要能夠產生PersonName的Instance,就保證內部資料永遠是正確的(Data Integrity)。
對比之前的Person,現在的Person,更像是High Level Coordinator,只需要分派工作給PersonName Value Object,也不用擔心驗證問題。
看起來更像是在描述Domain的語言,而不是一堆複雜的邏輯跟驗證。
對應到上次提到過的Declarative Expression。描述Simple Fact,而不是Step by Step的Low Level Sequence Operation。
花了兩個多月介紹Kent Beck的Design Principle,相信當遇到無法解決的問題時,先從核心的角度切入,能夠看到更多不一樣的設計思維。
- Local Consequence
- Minimize Repetition
- Symmetry
- Logic And Data Together
- Declarative Expression
- Rate of Change
結論: 透過Kent Beck的Rate of Change找到適當的屬性邊界,並且盡可能將其邊界Model成Value Object,能夠大大提升程式碼的可讀性,跟大大降低不必要的重複。