Kent Beck Implementation Pattern Principles(6)-Rate of Change

Z-xuan Hong
11 min readFeb 29, 2020

--

介紹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,相信當遇到無法解決的問題時,先從核心的角度切入,能夠看到更多不一樣的設計思維。

  1. Local Consequence
  2. Minimize Repetition
  3. Symmetry
  4. Logic And Data Together
  5. Declarative Expression
  6. Rate of Change

結論: 透過Kent Beck的Rate of Change找到適當的屬性邊界,並且盡可能將其邊界Model成Value Object,能夠大大提升程式碼的可讀性,跟大大降低不必要的重複。

--

--

Z-xuan Hong
Z-xuan Hong

Written by Z-xuan Hong

熱愛軟體開發、物件導向、自動化測試、框架設計,擅長透過軟工幫助企業達到成本最小化、價值最大化。單元測試企業內訓、導入請私訊信箱:t107598042@ntut.org.tw。

No responses yet