# Working Effectively with Legacy Code - 3/3
###### tags: `Legacy Code`
## PART III: Dependency-Breaking Techniques
## Chapter 25: Dependency-Breaking Techniques
本章列出了一些 depenency-breaking 的方法,是作者實際上有使用過並有效 decouple class 並且能夠測試,技術上來說所有的方法都是在做 refactor,都不會改變程式行為。這邊的 refactor 方法都是為了能在沒有測試保護的情況下執行,目的是讓 code 能夠變為可測試。
這些方法雖然犯錯的機率小,但並不表示是 100% 安全的,還是要小心。
這些方法不會馬上讓程式的架構變好,甚至可能變得更糟。但他們可以幫助程式能夠被測試,一旦有了測試的保護,就能夠再透過其他方法改善設計。
### Adapt Parameter
Method parameter 是很容易遇到另人頭痛 dependency 的地方。如果參數的 class 是可以修改的,那麼我們可以用之後會介紹的 *Extract Interface* 來解決
但事情不是永遠都很美好的,如果 parameter 是 low level type 或是特別的實作,extract interface 就會變得不切實際或是不可行
> Use Adapt Parameter when you can’t use Extract Interface (362) on a parameter’s class or when a parameter is difficult to fake.
```java=
public class ARMDispatcher
{
public void populate(HttpServletRequest request) {
String [] values
= request.getParameterValues(pageStateName);
if (values != null && values.length > 0)
{
marketBindings.put(pageStateName + getDateStamp(), values[0]);
}
...
}
...
}
```
上面這個例子 populate 需要傳入一個 HttpServletRequest (J2EE Standard interface),我們可以寫一個 class 實作 HttpServletRequest,但從文件上會發現我們需要實作 23 個 method,還不包括 super intefafce。想要用 Extract Interface 也不行,因為無法讓 HttpServletRequest 繼承我們的 interface。
當然網路上可以找到 HttpServletRequest 的測試 library,這樣就可以不用修改 populate method,但這樣還是沒有幫助我們脫離 HttpServletRequest 這個 API 的依賴
我們可以寫一個 wrapper 來達成 (Adapter Parameter)
* 參數改為 ParameterSource interface
```java=
public class ARMDispatcher
{
public void populate(ParameterSource source) {
String values = source.getParameterForName(pageStateName);
if (value != null) {
marketBindings.put(pageStateName + getDateStamp(), value);
}
...
}
}
```
* Fake class
```java=
class FakeParameterSource implements ParameterSource
{
public String value;
public String getParameterForName(String name) {
return value;
}
}
```
* Productive class
```java=
class ServletParameterSource implements ParameterSource
{
private HttpServletRequest request;
public ServletParameterSource(HttpServletRequest request) {
this.request = request;
}
String getParameterValue(String name) {
String [] values = request.getParameterValues(name);
if (values == null || values.length < 1)
return null;
return values[0];
}
}
```
如此一來就可以將 HttpServletRequest 的依賴解開,同時也提供了一個比較限縮抽象層 (原本 HttpServletRequest 有一堆的 method 沒有用到),讓程式能更好懂
> Adapt Parameter is one case in which we don’t *Preserve Signatures*. Use extra care.
新建的 parameter 介面如果和原本的 parameter 差很多是很有風險的,有可能一不小心就產生 bug,要記住我們的目的是要讓 code 能夠測試,而不是讓 code 能夠變得有更好的架構。有了測試保護,才能夠安心的繼續做其他的修改
> Safety first. Once you have tests in place, you can make invasive changes much more confidently.
#### Steps
1. 新增一個 interface,確保介面夠簡單
2. 新增一個 production class 實作該 interface
3. 新增一個 fake class 實作該 interface
4. 寫一個測試並傳入 fake object
5. 該寫原本的 method 使用新的 parameter
6. 執行測試確認修改成功,可以使用 fake object 來測試原本的 method
### Break Out Method Object
class 中的 long method 通常都是很難搞的,有時候光是要能夠放入測試工具中就要花非常大的力氣,這時候就要考慮使用 Break Out Method Object 的技巧。如果是小 method 且沒有使用到 instance data 的話,則可以考慮 Expose Static Method 方法。
簡而言之,Break Out Method Object 就是將 long method 移到一個新的 class,這個 class 會稱為 Method Object 是因為他就只有一個 method 在裡面。使用這個方法後就可以比較簡單的寫測試。原本 long method 的 local variable 也可以轉為 instance variable。
```cpp=
class GDIBrush
{
public:
void draw(vector<point>& renderingRoots,
ColorMatrix& colors,
vector<point>& selection);
...
private:
void drawPoint(int x, int y, COLOR color);
...
};
void GDIBrush::draw(vector<point>& renderingRoots,
ColorMatrix& colors,
vector<point>& selection)
{
for(vector<points>::iterator it = renderingRoots.begin();
it != renderingRoots.end();
++it) {
point p = *it;
...
drawPoint(p.x, p.y, colors[n]);
}
...
}
```
GDIBrush 有一個很長的 function *draw()* 且不容易建立 object 並測試,那麼就讓我們使用 Break Out Method Object 將 draw 搬到新的 class 去。
首先新的 class 我們取名為 Renderer,並建立一個 public constructor,參數是 GDIBrush 的指標和原本 draw function 的所有 arguments,並將所有參數都存入 instance variable 中
```cpp=
class Renderer
{
private:
GDIBrush *brush;
vector<point>& renderingRoots;
ColorMatrix& colors;
vector<point>& selection;
public:
Renderer(GDIBrush *brush,
vector<point>& renderingRoots,
ColorMatrix& colors,
vector<point>& selection)
: brush(brush), renderingRoots(renderingRoots),
colors(colors), selection(selection)
{}
};
```
現在看起來什麼幫助也沒有,我們還是有 GDIBrush 出現,還是無法放到測試工具中,讓我們繼續看下去…
接著新增 draw function 到 Renderer class 中,並且將原本的 function 實作 copy 過來,接著就是 compiler 派上用場的時候了
如果 draw 有用到 GDIBrush 的 instance variable 就會出現 error,我們可以在 GDIBrush 加一個 getter,如果是 instance method (drawPoint) 的話就改成 public。
這時候就可以將原本的 draw 改為下面這樣:
```cpp=
void GDIBrush::draw(vector<point>& renderingRoots,
ColorMatrix &colors,
vector<point>& selection)
{
Renderer renderer(this, renderingRoots, colors, selection);
renderer.draw();
}
```
那麼我們再回到 GDIBrush 的問題,這邊可以用 Extract Interface 方法來解決無法建立 GDIBrush object 的問題,也就是建立一個 interface 讓 GDIBrush 實作他,並且讓 Renderer 使用新建的 interface。
```cpp
class PointRenderer
{
public:
virtual void drawPoint(int x, int y, COLOR color) = 0;
};
class GDIBrush : public PointRenderer
{
public:
void drawPoint(int x, int y, COLOR color);
...
};
class Renderer
{
private:
PointRender *pointRenderer;
vector<point>& renderingRoots;
ColorMatrix& colors;
vector<point>& selection;
public:
Renderer(PointRenderer *renderer,
vector<point>& renderingRoots,
ColorMatrix& colors,
vector<point>& selection)
...
}
```

雖然改到目前這樣可能會覺得 code 不漂亮,而且還把原本 GDIBrush 的 private variable/method 改成了 public,但這不是最後的終點,總有一天你會受不了 GDIBrush 不能測試的情況,繼續 break dependencies 直到 GDIBrush 可以被測試。
> Break Out Method Object has several variations.
>
> In the simplest case, the original method doesn’t use any instance variables or methods from the original class. We don’t need to pass it a reference to the original class.
>
> In other cases, the method only uses data from the original class. At times, it makes sense to put this data into a new data-holding class and pass it as an argument to the method object.
>
> The case that I show in this section is the worst case; we need to use methods on the original class, so we use Extract Interface (362) and start to build up some abstraction between the method object and the original class.
#### Steps
1. 建立新的 class
2. 建立 constructor 並將原本 method 的參數照搬過來 (視情況加上原本 class 的 pointer)
3. 將 constructor 所有參數都存成新 class 的 instance variable
4. 建立一個空 method
5. 將原本 method 的實作 copy 過來並 compile
6. 根據 compile 的結果可能要新增 getter 或是將 private method 改為 public
7. 新的 class 可以 compile 過後,就可以回頭來改舊的 long method
8. 如果需要的話還要搭配 Extract Interface 來解除新 class 對舊 class 的依賴
### Definition Completion
有一些語言允許你在一個地方宣告,然後在另一個地方定義它,最明顯的是C/C++,可以用來幫助我們來解依賴
``` cpp
class CLateBindingDispatchDriver : public CDispatchDriver
{
public:
CLateBindingDispatchDriver ();
virtual ~CLateBindingDispatchDriver ();
ROOTID GetROOTID (int id) const;
void BindName (int id, OLECHAR FAR *name);
private:
CArray<ROOTID, ROOTID&> rootids;
};
```
如果我們希望在測試能用其他的 BindName 來綁定帳號,而非採用原先的方式,則可以透過定義補全的方式。
我們在測試檔案中包含其標頭檔,然後在測試中直接對目標方法提供另一個定義,對不關心的方法直接定義空的內容,或是定義測試要用的感測方法。
``` cpp
#include "LateBindingDispatchDriver.h"
CLateBindingDispatchDriver::CLateBindingDispatchDriver() {}
CLateBindingDispatchDriver::~CLateBindingDispatchDriver() {}
ROOTID GetROOTID (int id) const { return ROOTID(-1); }
void BindName(int id, OLECHAR FAR *name) {}
TEST(AddOrder,BOMTreeCtrl)
{
CLateBindingDispatchDriver driver;
CBOMTreeCtrl ctrl(&driver);
ctrl.AddOrder(COrderFactory::makeDefault());
LONGS_EQUAL(1, ctrl.OrderCount());
}
```
使用這個方法,就意味我們必須要為使用的這些測試建立單獨的可執行檔,否則替換的定義就會和原有的定義在連接期產生衝突。
另外的缺點就是因為有兩組定義,所以維護起來較困難,因此除非是遇上最難應付的狀況,不然是不建議使用。當使用完該技術能夠順利解開初始依賴,建議如果能將原先的重複定義刪掉就盡早刪除。
**Steps**
1. Identify a class with definitions you'd like to replace
2. Verify that the method definitions are in a source file, not a header
3. Include the header in the test source file of the class your are testing
4. Verify that the source files for the class are not part of the build
5. Build to find missing methods
6. Add method definitions to the test source file until you have a complete build
### Encapsulate Global References
在測試解依賴於全域變數的程式碼時,有三種解法:
1. 讓依賴的全域變數在測試期間有不同行為
2. 利用連結器連結到另一個全域變數定義
3. 透過封裝全域方式
``` cpp
bool AGG230_activeframe[AGG230_SIZE];
bool AGG230_suspendedframe[AGG230_SIZE];
void AGGController::suspend_frame()
{
frame_copy(AGG230_suspendedframe,
AGG230_activeframe);
clear(AGG230_activeframe);
flush_frame_buffers();
}
void AGGController::flush_frame_buffers()
{
for (int n = 0; n < AGG230_SIZE; ++n) {
AGG230_activeframe[n] = false;
AGG230_suspendedframe[n] = false;
}
```
為何不使用參數化方法,將兩個陣列傳給 suspend_frame?
因為當 suspend_frame 呼叫了 flush_frame_buffers 時,他又必須將變數傳下去,反而更煩雜
又或是將兩個變數當成建構子的參數傳進來?
是可行,但要檢查看看有沒有又被其他人用到
> 如果若干全域變數總是被綁在一起使用,一起修改,則它們應該可以綁在一起放在同一個類別使用
有點自言自語的時間...
> 命名一個類別的時候,考慮到最終會位於它裡面的方法,當然應該要起一個好名字,但並不一定是完美的,別忘了,你總是可以重新為它命名
> 你想到的類別名稱可能已經被用掉了,這時候可以考慮重新命名給那些使用該名字的實體,而將該名字騰出來
``` cpp
class Frame
{
public:
// declare AGG230_SIZE as a constant
enum { AGG230_SIZE = 256 };
bool AGG230_activeframe[AGG230_SIZE];
bool AGG230_suspendedframe[AGG230_SIZE];
};
// declare global instance of the Frame
Frame frameForAGG230
// Comment out the original declarations
// bool AGG230_activeframe[AGG230_SIZE];
// bool AGG230_suspendedframe[AGG230_SIZE];
```
這時候就可以透過 Compiler 幫你找出誰不存在,然後在這些全域變數上加入 frameForAGG230
``` cpp
void AGGController::suspend_frame()
{
frame_copy(frameForAGG230.AGG230_suspendedframe,
frameForAGG230.AGG230_activeframe);
clear(frameForAGG20.AGG230_activeframe);
flush_frame_buffer();
}
```
接著,我們可以透過 AGGController 的建構子來傳遞 Frame 物件,來達到跟全域變數分離的效果,雖然看起來效果好像不大,但慢慢的就會朝向良性方法修改。
除了全域變數,非成員函數也可以使用封裝全域參照(*Encapsulate Global References*) 來得到更佳的程式碼結構。
``` cpp
void ColumnModel::update()
{
alignRows();
Option resizeWidth = ::GetOption("ResizeWidth");
if (resizeWidth.isTrue()) {
resize();
} else {
resizeToDefault();
}
}
```
建立一個類別包含我們所需的每個自由函數的抽象版本,而產品類別的程式碼就只要呼叫相應的全域函數即可。
``` cpp
class OptionSource
{
public:
virtual ~OptionSource() = 0;
virtual Option GetOption(const string& optionName) = 0;
virtual void SetOption(const string& optionName,
const Option& newOption) = 0;
};
class ProductionOptionSource : public OptionSource
{
public:
Option GetOption(const string& optionName);
void SetOption(const string& optionName, const Option& newOption);
};
Option ProductionOptionSource::GetOption(const string& optionName)
{
::GetOption(optionName);
}
void ProductionOptionSource::SetOption(const string& optionName,
const Option& newOption)
{
::SetOption(optionName, newOption);
}
```
在測試中,則是從 OptionSource 衍生出一個偽類別,可以讓這個偽類別持有map或vector,內含測試中要用到的Option,想方便怎麼使用都可以。
**Steps**
1. Identify the globals that you want to encapsulate
2. Create a class that you want to reference them from
3. Copy the globals into the class. If some of them are variables, handle their initialization in the class
4. Comment out the original declarations of the globals
5. Declare a global instance of the new class
6. *Lean on the Compiler* to find all the unresolved references to the old globals
7. Precede each unresolved reference with the name of the global instance of the new class
8. In places where you want to use fakes, use *Introduce Static Setter*,
*Paramterize Constructor*, *Parameterize Method* or *Replace Global Reference with Getter*
### Expose Static Method
```java=
class RSCWorkflow
{
...
public void validate(Packet packet)
throws InvalidFlowException {
if (packet.getOriginator().equals( "MIA")
|| packet.getLength() > MAX_LENGTH
|| !packet.hasValidCheckSum()) {
throw new InvalidFlowException();
}
...
}
...
}
```
其實 `validate` 看起來更適合放在 `Packet` class ,但搬動這個 method 不是風險最低的選擇,我們無法做到 Preserve Signatures
沒有用到任何 instance variable 與 method,可以改為 static method
```java=
public class RSCWorkflow {
public void validate(Packet packet)
throws InvalidFlowException {
validatePacket(packet);
}
public static void validatePacket(Packet packet)
throws InvalidFlowException {
if (packet.getOriginator() == "MIA"
|| packet.getLength() <= MAX_LENGTH
|| packet.hasValidCheckSum()) {
throw new InvalidFlowException();
}
...
}
...
}
```
有些語言,可以直接將原本 method 改為 static,透過 instance 仍然可以使用,如以下範例
```java=
RSCWorkflow workflow = new RCSWorkflow();
...
// static call that looks like a non-static call
workflow.validatePacket(packet);
```
然而有些語言,這樣做會有 compiler warning,最好還是要讓 code 不要有 warning
如果擔心改為 static 後,之後有人使用而造成 dependency 問題,可以設成 non-public,例如 Java, C# 有 package 或 internal,在 C++ 可以設為 protected 或使用 namespace
Expose Static Method 步驟
1. 對你想要改為 public static 的 method 寫測試
2. 將 method body 抽成 static method,記得要 Preserve Signatures。你需要替 method 取另一個名字,通常可以從參數來想,例如上面範例是 `validate` 收到 `Packet`,抽出來的 method 就可以叫 `validatePacket`
3. Compile
4. 如果有存取 instance data, method 的錯誤,看一下能否把這些也變成 static
### Extract and Override Call
```java=
public class PageLayout
{
private int id = 0;
private List styles;
private StyleTemplate template;
...
protected void rebindStyles() {
styles = StyleMaster.formStyles(template, id);
...
}
...
}
```
要如何切斷與 StyleMaster 的 dependency
一個作法是抽成另一個 method,在 testing subclass 把新 method override 掉
```java=
public class PageLayout
{
private int id = 0;
private List styles;
private StyleTemplate template;
...
protected void rebindStyles() {
styles = formStyles(template, id);
...
}
protected List formStyles(StyleTemplate template,
int id) {
return StyleMaster.formStyles(template, id);
}
...
}
```
Extract and Override Call 很適合用來斷開 global variable 與 static method 的相依
Extract and Override Call 步驟
1. 找到想要抽走的 function call
2. 寫新的 method,使用與要抽走的 function call 相同 signature
3. 將該 function call 複製到新 method,原使用處改用新 method
### Extract and Override Factory Method
在幫 class 加測試時,constructor 內的 object creation 令人生氣
```java=
public class WorkflowEngine
{
public WorkflowEngine () {
Reader reader
= new ModelReader(
AppConfig.getDryConfiguration());
Persister persister
= new XMLStore(
AppConfiguration.getDryConfiguration());
this.tm = new TransactionManager(reader, persister);
...
}
...
}
```
在 constructor 內,new 出 TransactionManager 的 object
採用 Extract and Override Factory Method,可以修改為
```java=
public class WorkflowEngine
{
public WorkflowEngine () {
this.tm = makeTransactionManager();
...
}
protected TransactionManager makeTransactionManager() {
Reader reader
= new ModelReader(
AppConfiguration.getDryConfiguration());
Persister persister
= new XMLStore(
AppConfiguration.getDryConfiguration());
return new TransactionManager(reader, persister);
}
...
}
```
測試時,將 factory method override 掉,回一個 fake TransactionManager 即可
```java=
public class TestWorkflowEngine extends WorkflowEngine
{
protected TransactionManager makeTransactionManager() {
return new FakeTransactionManager();
}
}
```
這個方法有語言限制,在 C++ 無法使用,後面的 Supersede Instance Variable 會有詳細說明
*during construction and destruction, virtual functions aren't virtual.*
在 C++ 可以改用 Supersede Instance Variable (404) 或 Extract and Override Getter (352, 下一則)
Extract and Override Factory Method 步驟
1. 找出 constructor 內的 object creation
2. 將所有相關 code 抽到 factory method
3. 建立 testing subclass 並且 override factory method
### Extract and Override Getter
上則 Extract and Override Factory Method 有語言限制,C++ 無法使用
可以改用 Extract and Override Getter
```cpp=
// WorkflowEngine.h
class WorkflowEngine
{
private:
TransactionManager *tm;
public:
WorkflowEngine ();
...
}
// WorkflowEngine.cpp
WorkflowEngine::WorkflowEngine()
{
Reader *reader
= new ModelReader(
AppConfig.getDryConfiguration());
Persister *persister
= new XMLStore(
AppConfiguration.getDryConfiguration());
tm = new TransactionManager(reader, persister);
...
}
```
修改後
```cpp=
// WorkflowEngine.h
class WorkflowEngine
{
private:
TransactionManager *tm;
protected:
TransactionManager *getTransactionManager() const;
public:
WorkflowEngine ();
...
}
// WorkflowEngine.cpp
WorkflowEngine::WorkflowEngine()
:tm (0)
{
...
}
TransactionManager *getTransactionManager() const
{
if (tm == 0) {
Reader *reader
= new ModelReader(
AppConfig.getDryConfiguration());
Persister *persister
= new XMLStore(
AppConfiguration.getDryConfiguration());
tm = new TransactionManager(reader,persister);
}
return tm;
}
```
這裡我們使用 lazy getter,也常見於 Singleton Design Pattern
```java=
Thing getThing() {
if (thing == null) {
thing = new Thing();
}
return thing;
}
```
測試時,就可以把 getter override
```cpp=
class TestWorkflowEngine : public WorkflowEngine
{
public:
TransactionManager *getTransactionManager()
{ return &transactionManager; }
FakeTransactionManager transactionManager;
};
```
使用 Extract and Override Getter 其中一個缺點,是有機會用到還沒 initialized 的變數,因此要注意,確保 class 內都是用 getter
作者表示,他很少用 Extract and Override Getter,通常都是 Extract and Override Call 就可以解決,也比較簡單。但是當 class 有許多 method 要處理,包一個 getter 就可以處理掉,那也是個好方法
Extract and Override Getter 步驟
1. 找出需要 getter 的 object
2. 將 create object 需要的邏輯全部搬進 getter
3. 所有用到該 object 的地方,換成 getter,並且在所有 constructor 裡將指向該 object 的 reference 設為 null
4. 測試時,把 getter override 掉
### Extract Implementer
Extract Interface 是非常好用的技巧,但是最難的是 naming。通常的 case 是直接使用原本的 name ,如果使用的IDE 支援 rename class功能那會很簡單,
但如果不是使用原本的 name 那你會有幾個方法
- 取一個 foolish name
- 找一個 public methods 包含了所有你想抽的 method ,如果有 取一個新名子 interface
通常會加上"I"到新的interace class name 前面,除非他轉換成code base 中‧
>Naming is a key part of design. If you choose good names, you reinforce understanding
in a system and make it easier to work with. If you choose poor names, you
undermine understanding and make life hellish for the programmers who follow you.
Naming
是設計中最重要的一個部分,好的 names 可以讓你更專注了解系統‧
爛 names 會破壞了解系統而且會建立地獄的工程師人生
當一class name 是一個完美的 interface name,而且手上沒有自動 refector tool.
我會使用 Extract Implementer (本章重點)
來看一個範例
```cpp
// ModelNode.h
class ModelNode
{
private:
list<ModelNode *> m_interiorNodes;
list<ModelNode *> m_exteriorNodes;
double m_weight;
void createSpanningLinks();
public:
void addExteriorNode(ModelNode *newNode);
void addInternalNode(ModelNode *newNode);
void colorize();
...
};
```
第一個步驟複製一份,到 ProductionModelNode
```cpp
// ProductionModelNode.h
class ProductionModeNode
{
private:
list<ModelNode *> m_interiorNodes;
list<ModelNode *> m_exteriorNodes;
double m_weight;
void createSpanningLinks();
public:
void addExteriorNode(ModelNode *newNode);
void addInternalNode(ModelNode *newNode);
void colorize();
...
};
```
下一個步驟,回到原本的 ModelNode,刪掉不是public method的東西,再把所有public method 改成 pure method
``` cpp
// ModelNode.h
class ModelNode
{
public:
virtual void addExteriorNode(ModelNode *newNode) = 0;
virtual void addInternalNode(ModelNode *newNode) = 0;
virtual void colorize() = 0;
...
};
```
然後別忘了還要補上 pure virtal destructor
``` cpp
// ModelNode.h
class ModelNode
{
public:
virtual ~ModelNode () = 0;
virtual void addExteriorNode(ModelNode *newNode) = 0;
virtual void addInternalNode(ModelNode *newNode) = 0;
virtual void colorize() = 0;
...
};
// ModelNode.cpp
ModelNode::~ModelNode() {}
```
回到 ProductionModelNode, inlcude file 和 繼承ModelNode
```cpp
#include "ModelNode.h"
class ProductionModelNode : public ModelNode
{
private:
list<ModelNode *> m_interiorNodes;
list<ModelNode *> m_exteriorNodes;
double m_weight;
void createSpanningLinks();
public:
void addExteriorNode(ModelNode *newNode);
void addInternalNode(ModelNode *newNode);
void colorize();
...
};
```
Extract Implementer 步驟如下
1. 複製 class declaration 到另外一個檔案, 給不一樣的名子,我通常取名會有prefix Production
2. 回到原本的檔案,刪除所有非public mehtod 和 variable
3. rename 所有public method 變成 abstract
4. 檢查所有 include 這interface class 的地方,若有需要可以透過 compiler幫忙(之前章節有講過)
5. 讓 production class implement the new interface
6. Complie 確認所有 method signatures 都有實作
7. Complie 所有系統,找到使用到的地方換成新的 production class
8. Recomplie and test
#### A More Complex Example

對這個例子使用 Extract Implementer ,先 Node 做一次 Extract Implementer 如下

再對 ModelNode 做一次如下

看到這邊,當你有這種階層的class架構時,應該要考慮使用 Extract Interface (下一章節)
### Extract Interface
對許多語言來說,Extract Interface 是一個最安全 dependency-breaking 的技巧,如果出錯,compiler 會馬上告訴你,非常小的機會產生bug ‧
有三種方法可以完成Extract Interface 而且有兩個小陷阱需要去注意.
1. 方法1,自動 refector ,如果夠幸運系統支援選擇method or class 直接 rename,
使用這tool 找到 code 而且改reference 到新的 interface 當中. 好的工具可以省下很多工作.
2. 方法2, 沒有自動工具for interface extraction, 那就按照後面"步驟"完成 (會在下面補充步驟)
3. 方法3, 使用 copy/cut 和 paste 大法,到另外的interface class ,比起前兩個比較不安全,但這還是蠻安全的,
在我們使用方法2之前,還有幾件事情需要討論,
PaydayTransaction 使用一個log TransactionLog 如下圖,

我們也寫了test 如,
```cpp
void testPayday()
{
FakeTransactionLog aLog = new FakeTransactionLog();
Transaction t = new PaydayTransaction(getTestingDatabase(), aLog);
t.run();
assertEquals(getSampleCheck(12),
getTestingDatabase().findCheck(12));
}
```
為了compiler , 我們必須 extract interface for TransactionLog,讓FakeTransactionLog 去實作 interface
First things first: 抽一個interface 全新空的 TransactionRecorder
``` java
interface TransactionRecorder
{
}
public class TransactionLog implements TransactionRecorder
{
...
}
public class FakeTransactionLog implements TransactionRecorder
{
}
```
完成後還要改 PaydayTransaction 去使用TransactionRecorder ,接著為了擺脫 compiler error 一個一個去加 declarations to the TransactionRecorder and empty method definitions to the FakeTransactionLog
例如
```java
public class PaydayTransaction extends Transaction
{
public PaydayTransaction(PayrollDatabase db, TransactionRecorder log) {
super(db, log);
}
public void run() {
for(Iterator it = db.getEmployees(); it.hasNext(); ) {
Employee e = (Employee)it.next();
if (e.isPayday(date)) {
e.pay();
}
}
log.saveTransaction(this);
}
...
}
```
這時候 TransactionRecorder 和 FakeTransactionLog 要去實作 saveTransaction
```java
interface TransactionRecorder
{
void saveTransaction(Transaction transaction);
}
public class FakeTransactionLog implements TransactionRecorder
{
void saveTransaction(Transaction transaction) {}
}
```
看到這你可能會想說還有多一個method `recordError` 還沒implement,你可以去把recordError 抽成 interface,但實際上不用,
現在你只需要抽一些關鍵的public method 就好,因為現在需要基本的test 保護讓你去修改code,更多的test coverage 以後慢慢來.
> Interface Naming
大多數人會對Interface 使用 I當prefix,好處是相當簡單不用思考,壞處是到最後必須要思考method 是interface.
如果想把一個class 變成正規的class 必須要刪掉I, 不然會產生欺騙
#### Step
To Extract Interface, follow these steps:
1. Create a new interface with the name you’d like to use. Don’t add any
methods to it yet. 建立一個新的 interface, 不要加上任何的method
2. Make the class that you are extracting from implement the interface.
This can’t break anything because the interface doesn’t have any methods.
But it is good to compile and run your test just to verify that.
3. Change the place where you want to use the object so that it uses the
interface rather than the original class.
4. Compile the system and introduce a new method declaration on the
interface for each method use that the compiler reports as an error.
### Introduce Instance Delegator 引入實例委託
One of common reason to use static methods is to create utility classes, because it is hard to find a common abstraction for a set of methods. For example Math class in the Java JDK.
Sometimes you may face some situation that is difficult to depend on these static methods in a test. What do you do in this case?
``` cpp
public class BankingServices
{
public static void updateAccountBalance(int userID, Money amount) {
...
}
}
public class SomeClass
{
public void someMethod() {
BankingServices.updateAccountBalance(id, sum);
}
}
```
we can add an instance method to the clase and have it delegate to the static method
``` cpp
public class BankingServices
{
public static void updateAccountBalance(int userID, Money amount) {
...
}
public void updateBalance(int userID, Money amount) {
updateAccountBalance(userID, amount);
}
}
public class SomeClass
{
public void someMethod(BankingServices services) {
services.updateAccountBalance(id, sum);
}
}
// It's fine to do an additional refactoring step that
// we need to create the BankingServices object.
```
當我們有了物件接縫後,就可以利用它來嵌入測試用的行爲了。
這個做法其實還蠻直觀的,但當遇到 Utility 時,就會覺得很不自然,尤其是這個僅有兩個實例方法卻硬要將簡單功能轉發給相應的靜態方法就更加怪異。
但這個技術因為可以引入物件接縫,能夠使你在測試時替換另一種行為,隨著時間推移,你就會發現每個對該類別的方法呼叫都變成實例轉發時,那我們就可以將者些靜態方法的內容就轉移到相應的實例方法中,並刪除所有的靜態方法了。
**Step**
1. Identify a static method that is problematic to use in a test.
2. Create an instance method for the method on the class. Remember to Preserve Signatures. Make the instance method delegate to the static method.
3. Find places where the static methods are used in the class you have under test. Use Parameterize Method or another dependency-breaking technique to supply an instance to the location where the static method call was made.
### Introduce Static Setter
我們常常發現,阻饒我們將一塊程式碼放進測試控制工具的最大敵人常常就是全域變數,因為當你要測試一組類別時,發現有一些全域變數要被設置成某些狀態才能使用。
引入一個偽物件並用它來進行感測是非常直接的做法,常見的 Singleton 便會透過替換單例的方式來讓測試更順利進行。
首先必須在 singleton 中加入一個靜態設置的方法(static setter),以便用來替換單件實例,接著讓建構子設為受保護的,之後就可以對單例類別進行子類別話,建立一個全新物件並且串遞給那個靜態設置的方法。
雖然說這個做法會打破 Singleton 的保護,但是限制存取目的是用於防止錯誤,而寫測試也是為了防止錯誤,為此強硬一些是可以接受的。
``` cpp
// ExternalRouter class is one singleton and provide a getter for a dispatcher
void MessageRouter::route(Message *message) {
Dispatcher *dispatcher = ExternalRouter::instance()->getDispatcher();
if (dispatcher != NULL)
dispatcher->sendMessage(message);
}
class ExternalRouter
{
private:
static ExternalRouter *_instance;
public:
static ExternalRouter *instance();
...
};
ExternalRouter *ExternalRouter::_instance = 0;
ExternalRouter *ExternalRouter::instance()
{
if (_instance == 0) {
_instance = new ExternalRouter;
}
return _instance;
}
```
想要換入我們自己測試用的 Dispatcher,我們可以把提供該 Dispatcher 的 ExternalRouter 換掉。
因為 ExternalRouter 的單例物件是在 instance() 方法被第一次呼叫建出來,所以要換掉我們自己的 Router 物件,則要修改 instance() 的 return value。
``` cpp
void ExternalRouter::setTestingInstance(ExternalRouter *newInstance)
{
delete _instance;
_instance = newInstance;
}
```
當然我們必須假定我們能夠建立一個新的實例,如果建構子是 private 就無法順利建立,所以我們要將建構子改為 protected,就可以透過子類別化這個單例類別來實作感測,並將實例傳給 setTestingInstance
最後我們建立子類別 TestingExternalRouter 並且複寫 getDispatcher() 來返回我們想要的偽物件。
``` cpp
class TestingExternalRouter : public ExternalRouter
{
public:
virtual void Dispatcher *getDispatcher() const {
return new FakeDispatcher;
}
};
```
這看起來有點誇張,因為我們只是要換入測試用物件就多建了一個衍生類別,也是可以透過一個 Boolean 在類別中來決定返回的物件是要給產品使用還是給測試,但一般而言我們喜歡將產品和測試的程式碼分的清清楚楚。
除了單例物件,還有另一種全域變數是為全域工廠,他們是在每次靜態方法被呼叫時,產生一個全新的物件,要如何換入我們自己的物件呢?
``` java
public class RouteFactory
{
static Router makeRouter() {
return new EWNRouter();
}
}
```
``` java
interface RouterServer
{
Router makeRouter();
}
public class RouterFactory implements RouteServer
{
static Router makeRouter() {
return new server.makeRouter();
}
static setServer(RouterServer server) {
this.server = server;
}
staic RouterServer server = new RouterServer() {
public RouterServer makeRouter() {
return new EWNRouter();
}
}
}
// testing code
protected void setUp() {
RouterServer.setServer(new RouterServer() {
public RouterServer makeRouter() {
return new FakeRouter();
}
});
}
```
有一點需要注意,因為靜態設置的方法對所有測試來說都是程式狀態的修改,故在 xUnit 測試框架下,都會使用 tearDown 的方式來將狀態復位。一般可以在 setUp 和 tearDown 做一樣的事情,來確保系統處於一個乾淨的狀態。
Step:
1. Decrease the protection of the constructor so that you can make a fake by subclassing the singleton.
2. Add a static setter to the singleton class. Make sure the setter destroys the singleton instance before setting the new object.
3. If you need access to private or protected methods in the singleton to set it up properly for testing, consider subclassing it.
### Link Substitution
物件導向讓我們可以容易地抽換 class
然而 procedural language 像 C 無法這樣換
這時可以用 Link Substitution
建立一個 library,使用和想要替換的 function 相同 signature
如果是要 sensing,可以利用檔案、global variable 或其他方式
如下例,建立一個 global list 來記錄這個 function 被呼叫以及其參數
```c=
void account_deposit(int amount)
{
struct Call *call =
(struct Call *)calloc(1, sizeof (struct Call));
call->type = ACC_DEPOSIT;
call->arg0 = amount;
append(g_calls, call);
}
```
用於測試時,可以在最後檢查 global list,看這個 function 是否如預期呼叫的順序
這個方法最適合用於外部 library,最好 fake 的 function 是 pure data sink,也就是呼叫後不在乎回傳值,例如 graphics library
Link Substitution 也可以用於 Java,建立有相同名稱與 method 的 class,修改 classpath 就可以呼叫到自己寫的
Link Substitution 步驟
1. 找出想要替換的 function 或 class
2. 寫出取代用的版本
3. 調整 build 設定,讓它會跑到取代用版本
### Parameterize Constructor
如果在 constructor 有建立 object 的話,最容易替換 object 的方法,是將建立 object 搬出 class,用參數傳進去
```java=
public class MailChecker
{
public MailChecker (int checkPeriodSeconds) {
this.receiver = new MailReceiver();
this.checkPeriodSeconds = checkPeriodSeconds;
}
...
}
```
```java=
public class MailChecker
{
public MailChecker (MailReceiver receiver,
int checkPeriodSeconds) {
this.receiver = receiver;
this.checkPeriodSeconds = checkPeriodSeconds;
}
...
}
```
人們不常想到這個方法,是因為這會讓所有 client 都需要多傳一個參數
其實可以多寫一個 constructor 來解決這問題
```java=
public class MailChecker
{
public MailChecker (int checkPeriodSeconds) {
this(new MailReceiver(), checkPeriodSeconds);
}
public MailChecker (MailReceiver receiver,
int checkPeriodSeconds) {
this.receiver = receiver;
this.checkPeriodSeconds = checkPeriodSeconds;
}
...
}
```
略過兩頁 step by step 改 code 方法
這個方法有什麼缺點呢?這會讓 client 多一個可傳參數的 constructor,有機會讓 dependency 變複雜,但作者覺得不是什麼大問題
在支援 default arguments 的語言,可以更簡單做到 Parameterize Constructor
```cpp=
class AssemblyPoint
{
public:
AssemblyPoint(EquipmentDispatcher *dispatcher
= new EquipmentDispatcher);
...
};
```
在 C++,這個作法有個缺點,就是 class declaration 必須 include `EquipmentDispatcher` 的 header,因為這個原因,作者不常使用 default arguments
Parameterize Constructor 步驟
1. 找出要改參數的 constructor,先複製一份
2. 將要取代的 object 加入 constructor 參數,移除 object creation 並增加從參數 assign 到 instance variable
3. 如果你使用的語言,可以在 constructor 呼叫 constructor,將舊的 constructor 內容移掉並改為呼叫新的 constructor。如果使用的語言無法在 constructor 呼叫 constructor,需要把重複的地方,抽到一支新的 method
### Parameterize Method
類似上面,在 method 中有 create object,要抽換的話,一樣是改從用參數取進去
```cpp=
void TestCase::run() {
delete m_result;
m_result = new TestResult;
try {
setUp();
runTest(m_result);
}
catch (exception& e) {
result->addFailure(e, this);
}
tearDown();
}
```
替換 TestResult
```cpp=
void TestCase::run(TestResult *result) {
delete m_result;
m_result = result;
try {
setUp();
runTest(m_result);
}
catch (exception& e) {
result->addFailure(e, this);
}
tearDown();
}
```
要維持原本的 signature,可以加
```cpp=
void TestCase::run() {
run(new TestResult);
}
```
在 C++, Java, C# 以及許多語言,method 可以取相同名稱,只要 signature 不同
但有時候會令人困惑,另一個作法是將參數放到新 method 的名稱,例如上面的例子,新 method 命名為 runWithTestResult(TestResult)
Parameterize Method 步驟
1. 找出想要修改的 method,複製一份
2. 將要取代的 object 加入 method 參數,移除 object creation 並增加從參數 assign 到 variable
3. 刪除舊的 method 內容並改為呼叫新的 method,用原本的 object creation expression 作為參數
### Primitivize Parameter
當要把 class 放到測試工具裡實在是太耗工夫時,可以選擇這樣的方法
現在有一個音樂合成的工具,將音軌存為多個 event sequence,我們要加上一個 `bool Sequence::hasGapFor(Sequence& pattern)` 的方法,來判斷一個 sequence 中是否有空間可以塞入另一個 sequence。
假設 class Sequence 和 class Event 的依賴都很悲劇,無法將 class Sequence 放到測試工具中
考慮到我們要新增的功能,其實並不需要知道完整的資訊,只需要知道每個 event 的時間長度即可,我們只要在 class 外新寫一個 method 透過時間長短來算出結果
從測試開始寫:
```cpp=
TEST(hasGapFor, Sequence)
{
vector<unsigned int> baseSequence;
baseSequence.push_back(1);
baseSequence.push_back(0);
baseSequence.push_back(0);
vector<unsigned int> pattern;
pattern.push_back(1);
pattern.push_back(2);
CHECK(SequenceHasGapFor(baseSequence, pattern));
}
```
* SequenceHasGapFor 並不屬於任何的 class
* 參數簡化為基本類別
這時候修改原本的 class Sequence,將 `SequenceHasGapFor` 需要的資訊抓出來後透過他來幫我們完成計算
```cpp=
bool Sequence::hasGapFor(Sequence& pattern) const
{
vector<unsigned int> baseRepresentation = getDurationsCopy();
vector<unsigned int> patternRepresentation = pattern.getDurationsCopy();
return SequenceHasGapFor(baseRepresentation, patternRepresentation);
}
```
當然要加上新的 method `getDurationsCopy`
```cpp=
vector<unsigned int> Sequence::getDurationsCopy() const
{
vector<unsigned int> result;
for (vector<Event>::iterator it = events.begin();
it != events.end(); ++it) {
result.push_back(it->duration);
}
return result;
}
```
到目前為止新的功能就完成而且也有測試保護,但是有以下幾個問題:
1. 暴露 Sequence 的內部表示 (representation)
2. 將部份實作放到自由函數,變得更難理解
3. 新寫的 getDurationsCopy() 無法測試
4. 系統中存在重複的資料
5. 並沒有將問題改善,複雜的依賴還是存在
作者表示他不愛用,除非沒有其他選擇,也只用過一次,而且當時是有信心後面有時間可以為原本的 class 寫測試的情況下,等到那時候,就可以將自由函數放回 class 中
#### 技術總結
1. 寫一個自由函數來實作想做的事,建立一個 intermediate representation,讓該函數使用他來完成任務
2. 在舊的 class 實作這個 intermediate representation,並將任務轉發給自由函數
### Pull Up Feature
有時候你需要修改一個 class 中的某一群 method,但是造成問題的依賴和你修改的那群 method 並沒關係,這時候可以使用 `Expose Static Method` 或是 `Expose Static Method` 多次來達成解依賴,或是也有這兩種方法不適用的時候。
這種時候可以選擇 `Pull Up Feature` 將這一群 method 往上抽到 abstract class,這時候就可以繼承這個 absract class 並放到測試工具中。
```java=
public class Scheduler
{
private List items;
public void updateScheduleItem(ScheduleItem item)
throws SchedulingException {
try {
validate(item);
}
catch (ConflictException e) {
throw new SchedulingException(e);
}
...
}
private void validate(ScheduleItem item)
throws ConflictException {
// make calls to a database
...
}
public int getDeadtime() {
int result = 0;
for (Iterator it = items.iterator(); it.hasNext(); ) {
ScheduleItem item = (ScheduleItem)it.next();
if (item.getType() != ScheduleItem.TRANSIENT
&& notShared(item)) {
result += item.getSetupTime() + clockTime();
}
if (item.getType() != ScheduleItem.TRANSIENT) {
result += item.finishingTime();
} else {
result += getStandardFinish(item);
}
}
return result;
}
}
```
假設我們要修改 `getDeadtime` 且不用去動到 `updateScheduleItem`,但是有用到非 static 的 method 讓我們不好用 `Expose Static Method`,method 的長度也不長所以 `Break Out Method Object` 也不是很適合,這時候就試試 `Pull Up Feature`
```java=
public class Scheduler extends SchedulingServices
{
public void updateScheduleItem(ScheduleItem item)
throws SchedulingException {
...
}
private void validate(ScheduleItem item)
throws ConflictException {
// make calls to the database
...
}
...
}
```
```java=
public abstract class SchedulingServices
{
protected List items;
protected boolean notShared(ScheduleItem item) {
...
}
protected int getClockTime() {
...
}
protected int getStandardFinish(ScheduleItem item) {
...
}
public int getDeadtime() {
int result = 0;
for (Iterator it = items.iterator(); it.hasNext(); ) {
ScheduleItem item = (ScheduleItem)it.next();
if (item.getType() != ScheduleItem.TRANSIENT
&& notShared(item)) {
result += item.getSetupTime() + clockTime();
}
if (item.getType() != ScheduleItem.TRANSIENT) {
result += item.finishingTime();
} else {
result += getStandardFinish(item);
}
}
return result;
}
...
}
```
這時候就可以建立 subclass 來做測試
```java=
public class TestingSchedulingServices extends SchedulingServices
{
public TestingSchedulingServices() {
}
public void addItem(ScheduleItem item) {
items.add(item);
}
}
```
我們只是為了測試而產生了這樣的 class,這樣將一組的 feature 分散到兩個 class 中有時候會產生一個困惑,搞不清楚各個 class 的責任,但先有測試才能往下走
#### 技術總結
1. 找到想要往上抽的 method
2. 建立 abstract superclass
3. copy 第 1 步 method 到 superclass
4. 確認可以 compile
5. 建立新的 subclass 並測試
### Push Down Dependency
如果 class 中有問題的依賴只有少數 method 用到的話,可以考慮使用後面的 `Subclass and Override Method` 方法,反之依賴如果很複雜,你可能就要使用多次 `Extract Interface` 來解開依賴,這時候 Push Down Dependency 是一個新的選擇
直接上例子
```cpp+
class OffMarketTradeValidator : public TradeValidator
{
private:
Trade& trade;
bool flag;
void showMessage() { // UI related method
int status = AfxMessageBox(makeMessage(), MB_ABORTRETRYIGNORE);
if (status == IDRETRY) {
SubmitDialog dlg(this,
"Press okay if this is a valid trade");
dlg.DoModal();
if (dlg.wasSubmitted()) {
g_dispatcher.undoLastSubmission();
flag = true;
}
}
else if (status == IDABORT) {
flag = false;
}
}
public:
OffMarketTradeValidator(Trade& trade)
: trade(trade), flag(false) {}
bool isValid() const {
if (inRange(trade.getDate())
&& validDestination(trade.destination)
&& inHours(trade)) {
flag = true;
}
showMessage();
return flag;
}
...
};
```
如果我們要對 `isValid` 寫測試,但不想將 UI 相關的 library 也放到測試工具中的話,就可以試試 `Push Down Dependency` 改成下面這樣,把 UI 相關的 code 向下移到 subclass 去
```cpp=
class OffMarketTradeValidator : public TradeValidator
{
protected:
Trade& trade;
bool flag;
virtual void showMessage() = 0;
public:
OffMarketTradeValidator(Trade& trade)
: trade(trade), flag(false) {}
bool isValid() const {
if (inRange(trade.getDate())
&& validDestination(trade.destination)
&& inHours(trade) {
flag = true;
}
showMessage();
return flag;
}
...
};
class WindowsOffMarketTradeValidator
: public OffMarketTradeValidator
{
protected:
virtual void showMessage() {
int status = AfxMessageBox(makeMessage(),
MB_ABORTRETRYIGNORE);
if (status == IDRETRY) {
SubmitDialog dlg(this,
"Press okay if this is a valid trade");
dlg.DoModal();
if (dlg.wasSubmitted()) {
g_dispatcher.undoLastSubmission();
flag = true;
}
} else if (status == IDABORT) {
flag = false;
}
}
...
};
```
當 UI 相關的程式碼已經被放到新到 subclass 中之後,我們就可以寫一個新的 subclass 來執行測試
```cpp=
class TestingOffMarketTradeValidator
: public OffMarketTradeValidator
{
protected:
virtual void showMessage() {}
};
```
雖然這樣寫不是一個很好的辦法,但他有效的幫助我們可以開始測試,一旦有了測試保護,我們就可以開始整理 showMessage 中的 UI 以及 retry 邏輯,將 UI 的部份移到另一個 class 去,將 retry 邏輯搬回 OffMarketTradeValidator,來解開這個繼承的關係
#### 技術總結
1. 將想測試但有依賴問題的 class 直接放到測試工具中
2. 透過 compiler 找出有問題的地方
3. 將有問題的地方移到新的 subclass,記得取個好名字
4. instance variable 也要搬,注意 signature,將原本 class 的 method 改為 protected 並且將 class 改為 abstract
5. 建立 testing subclass,將第 1 步的測試改為 new 這個 subclass
6. 確認可以 compile
### Replace Function with Function Pointer
當你想 break dependency, 在非OO的語言你沒有太多的選擇,
可以可以使用 Link Substitution (377) or Definition Completion (337),對小的問題會有點殺雞用牛刀,
這提供另外的方法 Replace Function with Function Pointer 是給支援 function point的語言 C
一些人害怕這是不安全的,因為會汙染內容,去call 一個random 的 momory。
其他人,覺得這是有用的工具,但 uesd with care.
複習一下 function point 的使用
``` cpp
struct base_operations
{
double (*project)(double,double);
double (*maximize)(double,double);
};
double default_projection(double first, double second) {
return second;
}
double maximize(double first, double second) {
return first + second;
}
void init_ops(struct base_operations *operations) {
operations->project = default_projection;
operations->maximize = default_maximize;
}
void run_tesselation(struct node *base,struct base_operations *operations) {
double value = operations->project(base.first, base.second);
...
}
```
想像一個情況,你有個 network application ,對一個Online DB 做溝通
```cpp
void db_store(struct receive_record *record,struct time_stamp receive_time);
struct receive_record * db_retrieve(time_stamp search_time);
```
該怎做呢? 先找到他的 declaration, 先複製一份改一樣的名子
``` cpp
// db.h
void db_store(struct receive_record *record,struct time_stamp receive_time);
void (*db_store)(struct receive_record *record,struct time_stamp receive_time);
```
把原本的function name 修改
``` cpp
// db.h
void db_store_production(struct receive_record *record,struct time_stamp receive_time);
void (*db_store)(struct receive_record *record, struct time_stamp receive_time);
```
在程式的一開始要先init
``` cpp
// main.c
extern void db_store_production(struct receive_record *record,struct time_stamp receive_time);
void initializeEnvironment() {
db_store = db_store_production;
...
}
int main(int ac, char **av) {
initializeEnvironment();
...
}
```
在 definition 地方 rename
```cpp
// db.c
void db_store_production(struct receive_record *record, struct time_stamp receive_time) {
...
}
```
現在開始 compile 和 test
>Replace Function with Function Pointer is a good way to break dependencies. One of
the nice things about it is that it happens completely at compile time, so it has minimal
impact on your build system. However, if you are using this technique in C, consider
upgrading to C++ so that you can take advantage of all of the other seams that
C++ provides you. At the time of this writing, many C compilers offer switches to
allow you to do mixed C and C++ compilation. When you use this feature, you can
migrate your C project to C++ slowly, taking only the files that you care to break
dependencies in first.
Replace Function with Function Pointer 是一個好方法去 break dependencies. 一個最好的優點是可以在 compile time 完成,最小化影響整個程式,
如果你使用 C ,考慮升級成 C++, 可獲得更多C++的優點,現在你可以寫 C 和 C++ 的混和,慢慢的 migrate 到 c++
#### Step
To use Replace Function with Function Pointer, do the following:
1. 找到 declarations
2. Create function pointers with the same names before each function declaration
3. Rename the original function declarations so that their names are not the same as the function pointers you’ve just declared.
4. Initialize the pointers to the addresses of the old functions in a C file.
5. Run a build to find the bodies of the old functions. Rename them to the new function names.
### Replace Gloabl Reference with Getter
Global variables 一個痛,當你想要 讓一些code 獨立拆開,
``` java
public class RegisterSale
{
public void addItem(Barcode code) {
Item newItem = Inventory.getInventory().itemForBarcode(code);
items.add(newItem);
}
...
}
```
然後作者在這邊爆氣了, 使用global variable Inventory.getInventory()
第一步首先寫 getter
```java
public class RegisterSale
{
public void addItem(Barcode code) {
Item newItem = Inventory.getInventory().itemForBarcode(code);
items.add(newItem);
}
protected Inventory getInventory() {
return Inventory.getInventory();
}
...
}
```
取代原本使用到的地方
```java
public class RegisterSale
{
public void addItem(Barcode code) {
Item newItem = getInventory().itemForBarcode(code);
items.add(newItem);
}
protected Inventory getInventory() {
return Inventory.getInventory();
}
...
}
```
Inventory is a singleton, we have to make its constructor protected rather than
private.
接下來就使用 subclass 方法寫test
```java
class TestingRegisterSale extends RegisterSale
{
Inventory inventory = new FakeInventory();
protected Inventory getInventory() {
return inventory;
}
}
```
#### Step
1. Identify the global reference that you want to replace.
2. Write a getter for the global reference. Make sure that the access protection
of the method is loose enough for you to be able to override the getter
in a subclass.
3. Replace references to the global with calls to the getter.
4. Create a testing subclass and override the getter.
### Subclass and Override Method
Subclass and Override Method 是一個最核心的技術在 OO的語言中,
其他章節說的方法其實,都是這個方法的變形,
```java
class MessageForwarder
{
private Message createForwardMessage(Session session,Message message)
throws MessagingException, IOException {
MimeMessage forward = new MimeMessage (session);
forward.setFrom (getFromAddress (message));
forward.setReplyTo (new Address [] {new InternetAddress (listAddress) });
forward.addRecipients (Message.RecipientType.TO,listAddress);
forward.addRecipients (Message.RecipientType.BCC,getMailListAddresses ());
forward.setSubject (transformedSubject (message.getSubject ()));
forward.setSentDate (message.getSentDate ());
forward.addHeader (LOOP_HEADER, listAddress);
buildForwardContent(message, forward);
return forward;
}
...
}
```
假設我們想寫test 但不要 MimeMessage dependency .
session class 在測試的時候沒有 real session,
那我們把 createForwardMessage override 成 protected
```java
class TestingMessageForwarder extends MessageForwarder
{
protected Message createForwardMessage(Session session,Message message) {
Message forward = new FakeMessage(message);
return forward;
}
...
}
```
講完了,In production code,我們做的修改只有把 private to protected
作者就開始碎碎念,
Subclass and Override Method 是一個powerful 的方法,
對作者來說 programming 是視覺化的,有很多種方法 UML之類的,
作者自己使用的方法 paper view , 可以分析不同的片段,
那些可以 extract 或replace,如果可以我就貼一張紙在上面,
寫test 我就可以看貼的紙,去修改

paper view 可以幫助 Subclass and Override Method
Steps
To Subclass and Override Method, do the following:
1. Identify the dependencies that you want to separate or the place where
you want to sense. Try to find the smallest set of methods that you can
override to achieve your goals.
2. Make each method overridable. The way to do this varies among programming
languages. In C++, the methods have to be made virtual if
they aren’t already. In Java, the methods need to be made non-final. In
many .NET languages, you explicitly have to make the method overridable
also.
3. If your language requires it, adjust the visibility of the methods that you
will override to so that they can be overridden in a subclass. In Java and
C#, methods must at least have protected visibility to be overridden in
subclasses. In C++, methods can remain private and still be overridden in
subclasses.
4. Create a subclass that overrides the methods. Verify that you are able to
build it in your test harness.
### Supersede Instance Variable
> Object creation in constructors can be problematic, particularly when it is hard to depend upon those objects in a test.
在大部分的情況, 我們會用 *Extract and Override Factory Method (350)* 來解開, 若 language 不支援 virtual function calls in constructors 的話,`Supersede Instance Variable` 就會你一個選擇。
Example: the virtual function problem in C++
```cpp
class Pager
{
public:
Pager() {
reset();
formConnection();
}
virtual void formConnection() {
assert(state == READY);
// nasty code that talks to hardware here
...
}
void sendMessage(const std::string& address,
const std::string& message) {
formConnction();
...
}
...
}
```
馬上根據之前經驗,可以用 *Subclass and Override Method(401)* 來寫 Test, 立馬就來改改看:
```cpp
class TestingPager : public Pager
{
public:
virtual void formConnection() {
}
}
TEST(messaging, Pager)
{
TestingPager pager;
pager.sendMessage("5551212",
"Hey, wanna go to a party? XXXOOO");
LONGS_EQUAL(OKAY, pager.getStatus());
}
```
在 C++ 的世界內,我們常用 override a virtual funciton 來改變 derived class 的行為,但是這邊有一個例外,C++ 不允許 override 在 constructor 呼叫到的 virtual function。繼續看下去之前,我們詳細的看一下到底發生什麼事情
> 1. Memory for derived is allocated.
>2. The Derived() constructor is called
> 3. The compiler looks to see if we’ve asked for a particular Base class constructor. We have! So it calls Pager()
> 4. The base class constructor initialization list, which does nothing in this case
> 5. The base class constructor body executes, which calls `reset()` and `formConnection()`
> 6. The base class constructor returns
> 7. The derived class constructor initialization list, which does nothing
> 8. The derived class constructor body executes, which does nothing
> 9. The derived class constructor returns
因此在這個順序理面當 init TestingPager 的時候,呼叫到 Pager constructor 就會呼叫 `formConnection`, 在這個時間點就會是 `Pager::formConnection` 並非我們想的 `TestingPager::formConnection`
在下面的例子會更明顯
```cpp
class A
{
public:
A() {
someMethod();
}
virtual void someMethod() {}
}
class B : public A
{
C *c;
public:
B() {
c = new C;
}
virtual void someMethod() {
c.doSomething();
}
}
```
其他 language (Java) 因為允許 call overridden methods in constructor 其結果和 C++ 不同,好顯在 C++ 理面, 針對 replacing behavior in constructor 問題有其他的作法。
例如可用 *Extract and Override Getter(352)* 來解除 dependency,
```cpp
BlendingPen::BlendingPen()
{
setName("BlendingPen");
m_param = ParameterFactory::createParameter(
"cm", "Fade", "Aspect Alter");
m_param->addChoice("blend");
m_param->addChoice("add");
m_param->addChoice("filter");
setParamByName("cm", "blend");
}
```
這邊可以看到 constructor 內呼叫了 createParameter 的 Factory, 我們可以藉由 *Introduce Static Setting(372)* 來換掉 m_param,但作者說這個作法比較暴力。
如果不介意加點 method 的話,可以考慮這章節提到的方法, *Supersede Instance Variable*, 但使用前需要確定在呼叫 function 前,這替換的 function 有被呼叫到。
```cpp
void BlendingPen::supersedeParameter(Parameter* newParameter)
{
delete m_param;
m_param = newParameter;
}
```
乍看 *Spuersede Instance Variable* 看起來有點遜, 但在 c++ 若是考慮 *Parameterize Constructor(379)* 的話又有點不好,因為把整個邏輯都綁在了 construcotr.
在 construction call overridden method 的 language 理面,可以直接使用 *Extract and Override Factory Method(350)* 會是比較好的選擇。
Ref: [C++ Constructors and initialization of derived classes](https://www.learncpp.com/cpp-tutorial/114-constructors-and-initialization-of-derived-classes/)
> Generally, it's poor practice to provide setters that change the base objects that an object uses. Those setters allow clients to drastically change the behavior of an object during its lifetime. When someone can make those changes, you have to know the history of that object to understand what happens when you call one of its methods.
> When you don't have setters, code is easier to understand.
>
#### Step
To *Supersede Instance Variable* follow these steps:
1. Identify the insntace variable that you want to supersede.
2. Create a method named supersedeXXX, where XXX is the name of the variable you want to supersede.
3. In the method, write whatever code you need to so that you destroy the previous instace of the variable and set it to the new value. If the variable is a reference, verify that there aren't any other references in the class to the object it points to. If there are, you might have additional work to do in the superceding method to make sure that replacing the object is safe and has the right effect.
### Template Redefinition
```cpp
// AsyncReceptionPort.h
class AsyncReceptionPort
{
private:
CSocket m_socket;
Packet m_packet;
int m_segementSize;
...
public:
AsyncReceptionPort();
void Run();
...
}
// AsyncReceptionPort.cpp
void AsyncReceptionPort::Run() {
for(int n = 0; n < m_segmentSize; ++n) {
int bufferSize = m_bufferMax;
if (n = m_segmentSize -1)
bufferSize = m_remainingSize;
m_socket.receive(m_receiveBuffer, bufferSize);
m_packet.mark();
m_packet.appeend(m_receiveBuffer, bufferSize);
m_packet.pack();
}
m_packet.finalize();
}
```
```cpp
// AsyncReceptionPort.h
template<typename SOCKET> class AsyncReceptionPortImpl
{
private:
SOCKET m_socket;
Packet m_packet;
int m_segementSize;
...
public:
AsyncReceptionPort();
void Run();
...
}
// AsyncReceptionPort.cpp
template<typename SOCKET>
void AsyncReceptionPort<SOCKET>::Run() {
for(int n = 0; n < m_segmentSize; ++n) {
int bufferSize = m_bufferMax;
if (n = m_segmentSize -1)
bufferSize = m_remainingSize;
m_socket.receive(m_receiveBuffer, bufferSize);
m_packet.mark();
m_packet.appeend(m_receiveBuffer, bufferSize);
m_packet.pack();
}
m_packet.finalize();
}
typedef AsyncReceptionPortImpl<CSocket> AsyncReceptionPort;
```
```cpp
// TestAsyncReceptionPort.cpp
#include "AsyncReceptionPort.h"
class FakeSocket
{
public:
receive(char*, int size) {...}
}
TEST(Run, AsyncReceptionPort)
{
AsyncReceptionPortImpl<FakeSocket> port;
...
}
```
在這個方法當中,最棒的就是我們可以用 `typedef` 來避免改變我們 code base 的 references, 不然我們就必須找出所有用到 `AsyncRecepitonPort` 並把它改成 `AsyncReceptionPortImpl<CSocket>`, 如果沒有類似 `typedef` 可以用的話,可以利用之前提到過的 *Lean on the Compiler(315)* 來找出所有要改的地方。
> *Template Redefinition* in C++ has one primary disadvantage. Code that was in implementation files moves to headers when you templatize it. This can increase the dpenecies in systems. Users of the template then are forced to recompile whenever the template code is changed.
> 作者個人偏好使用 inheritance-based techniques 來解依賴, 但是當原本的 code 就是 templatized 的話 *Template Redefinition* 也是很好用的。
>
> ```cpp
> template<typename ArcContact> class CollaborationManager
> {
>...
>ContactManager<ArcContact> m_contactManager;
> ...
> }
> ```
> 如果需要解開 m_contactManager 的依賴的話,可以利用 template 參數化的方式處理
> ```cpp
> template<typename ArcContactManager> class CollaborationManager
> {
>...
>ArcContactManager m_contactManager;
> ...
> }
> ```
#### Step
To *Template Redefinition* in C++. That could be different in other languages support generics, but the description gives a flavor of the technique.
1. Identify the insntace variable that you want to supersede.
2. Create a method named supersedeXXX, where XXX is the name of the variable you want to supersede.
3. In the method, write whatever code you need to so that you destroy the previous instace of the variable and set it to the new value. If the variable is a reference, verify that there aren't any other references in the class to the object it points to. If there are, you might have additional work to do in the superceding method to make sure that replacing the object is safe and has the right effect.
### Text Redefinition
在一些 interperted languages 可以在執行之前,把需要的部分直接 redefine 就好,下面以 Ruby 為例
```ruby
# Account.rb
class Account
def report_deposit(value)
...
end
def deposit(value)
@balance += value
report_deposit(value)
end
def withdraw(value)
@balance -= value
end
end
```
```ruby
# AccountTest.rb
require "runit/testcase"
require "Account"
class Account
def report_deposit(value)
end
end
# tests start here
class AccountTest < RUNIT::TestCase
...
end
```
在 Ruby 不需要 redefine 整個 class, 如果之前 Account 沒有 report_deposit 的話,就會把這 method 加上,如果有 define 過的話,就直接把他換掉。
> Text Redefinition in Ruby has one downside. The new method replaces the old one until the program ends. This can cause some trouble if you forget that a particular method has been redefined by a previous test.
> C & C++ 也可以利用 preprocessor 來進行 Text redefinition 可參考 *Preprocessing Seam(33)* in chapter 4.
#### Step
To use *Text Redefinition* in Ruby, follow these steps.
1. Identify a class with definitions that you want to replace.
2. Add a require clause with the name of the module that contains that class to the top of the test source file.
3. Provide alternative definitions at the top of the test source file for each method that you want to replace.
---
[Working Effectively with Legacy Code 1/3](https://hackmd.io/@ZGt0WcJQQ_enG8iTXTGNWw/BytJvDO0B)
[Working Effectively with Legacy Code 2/3](https://hackmd.io/@ZGt0WcJQQ_enG8iTXTGNWw/HkSXF4fXU)
Working Effectively with Legacy Code 3/3