### Encapsulate Collection(封裝群集)
有個函數(method)返回一個群集(collection)。
讓這個函數返回該群集的一個只讀映件(read-only view),并在這個class中提供「添加/移除」(add/remove)群集元素的函數。

**動機(Motivation)**
class常常會使用群集(collection,可能是array、list、set或vector)來保存一組實體。這樣的class通常也會提供針對該群集的「取值/設值函數」(getter/setter)。
但是,群集的處理方式應該和其他種類的數據略有不同。取值函數(getter)不該返回群集自身,因為這將讓用戶得以修改群集內容而群集擁有者卻一無所悉。這也會對用戶暴露過多「對象內部數據結構」的信息。如果一個取值函數(getter)確實需要返回多個值,它應該避免用戶直接操作對象內所保存的群集,并隱藏對象內「與用戶無關」的數據結構。至于如何做到這一點,視你使用的版本不同而有所不同。
另外,不應該為這整個群集提供一個設值函數(setter),但應該提供用以為群集添加/移除(add/remove)元素的函數。這樣,群集擁有者(對象)就可以控制群集元素的添加和移除。
如果你做到以上數點,群集(collection)就被很好地封裝起來了,這便可以降低群集擁有者(class)和用戶之間的耦合度。
**作法(Mechanics)**
- 加入「為群集添加(add)、移除(remove)元素」的函數。
- 將「用以保存群集」的值域初始化為一個空群集。
- 編譯。
- 找出「群集設值函數」的所有調用者。你可以修改那個設值函數,讓它使用 上述新建立的「添加/移除元素」函數;也可以直接修改調用端,改讓它們調用上述新建立的「添加/移除元素」函數。
- 兩種情況下需要用到「群集設值函數」:(1) 群集為空時;(2) 準備將原有群集替換為另一個群集時。
- 你或許會想運用Rename Method 為「群集設值函數」改名,從setXxx()改為initialzeXxx()或replaceXxx()。
- 編譯,測試。
- 找出所有「通過取值函數(getter)獲得群集并修改其內容」的函數。逐一修改這些函數,讓它們改用「添加/移除」(add/remove)函數。每次修改后,編譯并測試。
- 修改完上述所有「通過取值函數(getter)獲得群集并修改群集內容」的函數后,修改取值函數自身,使它返回該群集的一個只讀映件(read-only view)。
- 在Java 2中,你可以使用Collection.unmodifiableXxx()得到該群集的只讀映件。
- 在Java 1.1中,你應該返回群集的一份拷貝。
- 編譯,測試。
- 找出取值函數(getter)的所有用戶,從中找出應該存在于「群集之宿主對象(host object)內的代碼。運用 Extract Method 和 Move Method 將這些代碼移到宿主對象去。
如果你使用Java 2,那么本項重構到此為止。如果你使用Java 1.1,那么用戶也許會喜歡使用枚舉(enumeration)。為了提供這個枚舉,你應該這樣做:
- 修改現有取值函數(getter)的名字,然后添加一個新取值函數,使其返回一個枚舉。找出舊取值函數的所有被使用點,將它們都改為使用新取值函數。
- 如果這一步跨度太大,你可以先使用Rename Method 修改原取值函數的名稱;再建立一個新取值函數用以返回枚舉;最后再修改 所有調用者,使其調用新取值函數。
- 編譯,測試。
**范例(Example)**
Java 2擁有一組全新群集(collections)——并非僅僅加入一些新classes,而是完全改變了群集的風格。所以在Java 1.1和Java 2中,封裝群集的方式也完全不同。我首先討論Java 2的方式,因為我認為功能更強大的Java 2 collections會取代Java 1.1 collections 的地位。
**范例(Example):Java 2**
假設有個人要去上課。我們用一個簡單的Course來表示「課程」:
~~~
class Course...
public Course (String name, boolean isAdvanced) {...};
public boolean isAdvanced() {...};
~~~
我不關心課程其他細節。我感興趣的是表示「人」的Person:
~~~
class Person...
public Set getCourses() {
return _courses;
}
public void setCourses(Set arg) {
_courses = arg;
}
private Set _courses;
~~~
有了這個接口,我們就可以這樣為某人添加課程:
~~~
Person kent = new Person();
Set s = new HashSet();
s.add(new Course ("Smalltalk Programming", false));
s.add(new Course ("Appreciating Single Malts", true));
kent.setCourses(s);
Assert.equals (2, kent.getCourses().size());
Course refact = new Course ("Refactoring", true);
kent.getCourses().add(refact);
kent.getCourses().add(new Course ("Brutal Sarcasm", false));
Assert.equals (4, kent.getCourses().size());
kent.getCourses().remove(refact);
Assert.equals (3, kent.getCourses().size());
~~~
如果想了解高級課程,可以這么做:
~~~
Iterator iter = person.getCourses().iterator();
int count = 0;
while (iter.hasNext()) {
Course each = (Course) iter.next();
if (each.isAdvanced()) count ++;
}
~~~
我要做的第一件事就是為Person中的群集(collection)建立合適的修改函數(modifiers,亦即add/remove函數:),如下所示,然后編譯:
~~~
class Person
public void addCourse (Course arg) {
_courses.add(arg);
}
public void removeCourse (Course arg) {
_courses.remove(arg);
}
~~~
如果我像下面這樣初始化_courses值域,我的人生會輕松得多:
~~~
private Set _courses = new HashSet();
~~~
接下來我需要觀察設值函數(setter)的調用者。如果有許多地點大量運用了設值函數,我就需要修改設值函數,令它調用添加/移除(add/remove)函數。這個過程的復雜度取決于設值函數的被使用方式。設值函數的用法有兩種,最簡單的情況就是: 它被用來「對群集進行初始化動作」。換句話說,設值函數被調用之前,_courses 是個空群集。這種情況下我只需修改設值函數,令它調用添加函數(add)就行了 :
~~~
class Person...
public void setCourses(Set arg) {
Assert.isTrue(_courses.isEmpty());
Iterator iter = arg.iterator();
while (iter.hasNext()) {
addCourse((Course) iter.next());
}
}
~~~
修改完畢后,最好以Rename Method 更明確地展示這個函數的意圖。
~~~
public void initializeCourses(Set arg) {
Assert.isTrue(_courses.isEmpty());
Iterator iter = arg.iterator();
while (iter.hasNext()) {
addCourse((Course) iter.next());
}
}
~~~
更普通(譯注:而非上述所言對「空群集」設初值)的情況下,我必須首先以移除函數(remove)將群集中的所有元素全部移除,然后再調用添加函數(add)將 元素一一添加進去。不過我發現這種情況很少出現(晤,愈是普通的情況,愈少出現)。
如果我知道初始化時,除了添加元素,不會再有其他行為,那么我可以不使用循環, 直接調用addAll() 函數:
~~~
public void initializeCourses(Set arg) {
Assert.isTrue(_courses.isEmpty());
_courses.addAll(arg);
}
~~~
我不能僅僅對這個set 賦值,就算原本這個set 是空的也不行。因為萬一用戶在「把set 傳遞給Person 對象」之后又去修改它,會破壞封裝。我必須像上面那樣創建set 的一個拷貝。
如果用戶僅僅只是創建一個set,然后使用設值函數(setter。譯注:目前已改名為initializeCourses()),我可以讓它們直接使用添加/移除(add/remove)函數, 并將設值函數完全移除。于是,以下代碼:
~~~
Person kent = new Person();
Set s = new HashSet();
s.add(new Course ("Smalltalk Programming", false));
s.add(new Course ("Appreciating Single Malts", true));
kent.initializeCourses(s);
~~~
就變成了:
~~~
Person kent = new Person();
kent.addCourse(new Course ("Smalltalk Programming", false));
kent.addCourse(new Course ("Appreciating Single Malts", true));
~~~
接下來我幵始觀察取值函數(getter)的使用情況。首先處理「有人以取值函數修改底部群集(underlying collection)」的情況,例如:
~~~
kent.getCourses().add(new Course ("Brutal Sarcasm", false));
~~~
這種情況下我必須加以改變,使它調用新的修改函數(modifier):
~~~
kent.addCourse(new Course ("Brutal Sarcasm", false));
~~~
修改完所有此類情況之后,我可以讓取值函數(getter)返回一個只讀映件(read-only view),用以確保沒有任何一個用戶能夠通過取值函數(getter)修改群集:
~~~
public Set getCourses() {
return Collections.unmodifiableSet(_courses);
}
~~~
這樣我就完成了對群集的封裝。此后,不通過Person 提供的add/remove 函數,誰也不能修改群集內的元素。
將行為移到這個class中
我擁有了合理的接口。現在開始觀察取值函數(getter)的用戶,從中找出應該屬于Person 的代碼。下面這樣的代碼就應該搬移到Person 去:
~~~
Iterator iter = person.getCourses().iterator();
int count = 0;
while (iter.hasNext()) {
Course each = (Course) iter.next();
if (each.isAdvanced()) count ++;
}
~~~
因為以上只使用了屬于Person的數據。首先我使用 Extract Method 將這段代碼提煉為一個獨立函數:
~~~
int numberOfAdvancedCourses(Person person) {
Iterator iter = person.getCourses().iterator();
int count = 0;
while (iter.hasNext()) {
Course each = (Course) iter.next();
if (each.isAdvanced()) count ++;
}
return count;
}
~~~
然后使用Move Method 將這個函數搬移到Person中:
~~~
class Person...
int numberOfAdvancedCourses() {
Iterator iter = getCourses().iterator();
int count = 0;
while (iter.hasNext()) {
Course each = (Course) iter.next();
if (each.isAdvanced()) count ++;
}
return count;
}
~~~
舉個常見例子,下列代碼:
~~~
kent.getCourses().size()
~~~
可以修改成更具可讀性的樣子,像這樣:
~~~
kent.numberOfCourses()
class Person...
public int numberOfCourses() {
return _courses.size();
}
~~~
數年以前,我曾經擔心將這樣的行為搬移到Person 中會導致Person 變得臃腫。但是在實際工作經驗中,我發現這通常并不成為問題。
**范例: Java 1.1**
在很多地方,Java 1.1的情況和Java 2非常相似。這里我使用同一個范例,不過群集改為vector (譯注:因為vector 屬于Java 1.1,不屬于Java 2):
~~~
class Person...
public Vector getCourses() {
return _courses;
}
public void setCourses(Vector arg) {
_courses = arg;
}
private Vector _courses;
~~~
同樣地,我首先建立修改函數(modifiers:add/remove 函數),并初始化_courses值域,如下所示:
~~~
class Person
public void addCourse(Course arg) {
_courses.addElement(arg);
}
public void removeCourse(Course arg) {
_courses.removeElement(arg);
}
private Vector _courses = new Vector();
~~~
我可以修改setCourses()來初始化這個vector:
~~~
public void initializeCourses(Vector arg) {
Assert.isTrue(_courses.isEmpty());
Enumeration e = arg.elements();
while (e.hasMoreElements()) {
addCourse((Course) e.nextElement());
}
}
~~~
然后,我修改取值函數(getter)調用點,讓它們改用新建的修改函數(modifiers)。 于是下列代碼:
~~~
kent.getCourses().addElement(new Course ("Brutal Sarcasm", false));
~~~
就變成了:
~~~
kent.addCourse(new Course ("Brutal Sarcasm", false));
~~~
最后一步需要有點改變,因為Java 1.1的Vector class并沒有提供「不可修改版」(unmodifiable version):
~~~
class Person...
Vector getCourses() {
return (Vector) _courses.clone();
}
~~~
這樣便完成了群集的封裝。此后,如果不通過Person 提供的函數,誰也不能改變群集的元素。
**范例:封裝數組(Encapsulating Arrays)**
數組(array)很常被使用,特別是對于那些不熟悉群集(collections)的程序員而言。我很少使用數組,因為我更喜歡功能更加豐富的群集類。進行封裝時,我常把數組換成其他群集。
這次我們的范例從一個字符串數組(string array)開始:
~~~
String[] getSkills() {
return _skills;
}
void setSkills (String[] arg) {
_skills = arg;
}
String[] _skills;
~~~
同樣地,首先我要提供一個修改函數(modifier)。由于用戶有可能修改數組中某一特定位置上的值,所以我提供的setSkill()必須能對任何特定位置上的元素賦值:
~~~
void setSkill(int index, String newSkill) {
_skills[index] = newSkill;
}
~~~
如果我需要對整個數組賦值,可以使用下列函數:
~~~
void setSkills (String[] arg) {
_skills = new String[arg.length];
for (int i=0; i < arg.length; i++)
setSkill(i,arg[i]);
}
~~~
如果需要處理「被移除元素」(removed elements),就會有些困難。如果作為引數(argument)的數組和原數組長度不同,情況也會比較復雜。這也是我優先選擇群集的原因之一。
現在,我需要觀察取值函數(getter)的調用者。我可以把下列代碼:
~~~
kent.getSkills()[1] = "Refactoring";
~~~
改成:
~~~
kent.setSkill(1,"Refactoring");
~~~
完成這一系列修改之后,我可以修改取值函數(getter),令它返回一份數組拷貝:
~~~
String[] getSkills() {
String[] result = new String[_skills.length];
System.arraycopy(_skills, 0, result, 0, _skills.length);
return result;
}
~~~
現在,是把數組換成list 的時候了:
~~~
class Person...
String[] getSkills() {
return (String[]) _skills.toArray(new String[0]);
}
void setSkill(int index, String newSkill) {
_skills.set(index,newSkill);
}
List _skills = new ArrayList();
~~~
- 譯序 by 侯捷
- 譯序 by 熊節
- 序言
- 前言
- 章節一 重構,第一個案例
- 起點
- 重構的第一步
- 分解并重組statement()
- 運用多態(Polymorphism)取代與價格相關的條件邏輯
- 結語
- 章節二 重構原則
- 何謂重構
- 為何重構
- 「重構」助你找到臭蟲(bugs)
- 何時重構
- 怎么對經理說?
- 重構的難題
- 重構與設計
- 重構與性能(Performance)
- 重構起源何處?
- 章節三 代碼的壞味道
- Duplicated Code(重復的代碼)
- Long Method(過長函數)
- Large Class(過大類)
- Long Parameter List(過長參數列)
- Divergent Change(發散式變化)
- Shotgun Surgery(散彈式修改)
- Feature Envy(依戀情結)
- Data Clumps(數據泥團)
- Primitive Obsession(基本型別偏執)
- Switch Statements(switch驚悚現身)
- Parallel Inheritance Hierarchies(平行繼承體系)
- Lazy Class(冗贅類)
- Speculative Generality(夸夸其談未來性)
- Temporary Field(令人迷惑的暫時值域)
- Message Chains(過度耦合的消息鏈)
- Middle Man(中間轉手人)
- Inappropriate Intimacy(狎昵關系)
- Alternative Classes with Different Interfaces(異曲同工的類)
- Incomplete Library Class(不完美的程序庫類)
- Data Class(純稚的數據類)
- Refused Bequest(被拒絕的遺贈)
- Comments(過多的注釋)
- 章節四 構筑測試體系
- 自我測試代碼的價值
- JUnit測試框架
- 添加更多測試
- 章節五 重構名錄
- 重構的記錄格式
- 尋找引用點
- 這些重構準則有多成熟
- 章節六 重新組織你的函數
- Extract Method(提煉函數)
- Inline Method(將函數內聯化)
- Inline Temp(將臨時變量內聯化)
- Replace Temp with Query(以查詢取代臨時變量)
- Introduce Explaining Variable(引入解釋性變量)
- Split Temporary Variable(剖解臨時變量)
- Remove Assignments to Parameters(移除對參數的賦值動作)
- Replace Method with Method Object(以函數對象取代函數)
- Substitute Algorithm(替換你的算法)
- 章節七 在對象之間搬移特性
- Move Method(搬移函數)
- Move Field(搬移值域)
- Extract Class(提煉類)
- Inline Class(將類內聯化)
- Hide Delegate(隱藏「委托關系」)
- Remove Middle Man(移除中間人)
- Introduce Foreign Method(引入外加函數)
- Introduce Local Extension(引入本地擴展)
- 章節八 重新組織數據
- Self Encapsulate Field(自封裝值域)
- Replace Data Value with Object(以對象取代數據值)
- Change Value to Reference(將實值對象改為引用對象)
- Replace Array with Object(以對象取代數組)
- Replace Array with Object(以對象取代數組)
- Duplicate Observed Data(復制「被監視數據」)
- Change Unidirectional Association to Bidirectional(將單向關聯改為雙向)
- Change Bidirectional Association to Unidirectional(將雙向關聯改為單向)
- Replace Magic Number with Symbolic Constant(以符號常量/字面常量取代魔法數)
- Encapsulate Field(封裝值域)
- Encapsulate Collection(封裝群集)
- Replace Record with Data Class(以數據類取代記錄)
- Replace Type Code with Class(以類取代型別碼)
- Replace Type Code with Subclasses(以子類取代型別碼)
- Replace Type Code with State/Strategy(以State/strategy 取代型別碼)
- Replace Subclass with Fields(以值域取代子類)
- 章節九 簡化條件表達式
- Decompose Conditional(分解條件式)
- Consolidate Conditional Expression(合并條件式)
- Consolidate Duplicate Conditional Fragments(合并重復的條件片段)
- Remove Control Flag(移除控制標記)
- Replace Nested Conditional with Guard Clauses(以衛語句取代嵌套條件式)
- Replace Conditional with Polymorphism(以多態取代條件式)
- Introduce Null Object(引入Null 對象)
- Introduce Assertion(引入斷言)
- 章節十一 處理概括關系
- Pull Up Field(值域上移)
- Pull Up Method(函數上移)
- Pull Up Constructor Body(構造函數本體上移)
- Push Down Method(函數下移)
- Push Down Field(值域下移)
- Extract Subclass(提煉子類)
- Extract Superclass(提煉超類)
- Extract Interface(提煉接口)
- Collapse Hierarchy(折疊繼承關系)
- Form Template Method(塑造模板函數)
- Replace Inheritance with Delegation(以委托取代繼承)
- Replace Delegation with Inheritance(以繼承取代委托)
- 章節十二 大型重構
- 這場游戲的本質
- Tease Apart Inheritance(梳理并分解繼承體系)
- Convert Procedural Design to Objects(將過程化設計轉化為對象設計)
- Separate Domain from Presentation(將領域和表述/顯示分離)
- Extract Hierarchy(提煉繼承體系)
- 章節十三 重構,復用與現實
- 現實的檢驗
- 為什么開發者不愿意重構他們的程序?
- 現實的檢驗(再論)
- 重構的資源和參考資料
- 從重構聯想到軟件復用和技術傳播
- 結語
- 參考文獻
- 章節十四 重構工具
- 使用工具進行重構
- 重構工具的技術標準(Technical Criteria )
- 重構工具的實用標準(Practical Criteria )
- 小結
- 章節十五 集成
- 參考書目