天天看點

Encapsulate Collection(封裝群集)

有個函數(method)傳回一個群集(collection).

讓這個函數傳回該群集的一個隻讀映件(read-only view),并在這個class中提供[添加/移除](add/remove)群集元素的函數.

Person...

    getCourse() :Set

    setCourse(Set)

==>

Person

   getCourse() : Unmodifiable Set

   addCourse(Course)

   removeCourse(Course)

動機

class常常會使用群集(collection,可能是array,list,set或vector)來儲存一組實體.這樣的class通常也會提供針對該群集[取值/設值函數](getter/setter).

但是,叢集的處理方式應該和其他種類的資料略有不同.取值函數(getter)不該傳回群集自身,因為這将讓使用者得以修改群集内容而群集擁有者卻一無所悉.這也會對使用者暴露過多[對象内部資料結構]的資訊.如果一個取值函數(getter)确實需要傳回多個值,它應該避免使用者直接操作對象内所儲存的群集,并隐藏對象内[與使用者無關]的資料結構.至于如何做到這一點,視你使用的Java版本不同而有所不同.

另外,不應該為這整個群集提供一個設值函數(setter),但應該提供用以為群集添加/移除(add/remove)元素的函數.這樣,群集擁有者(對象)就可以控制群集元素的添加和移除.

如果你做到以上數點,群集(collection)就被很好地封裝起來了,這便可以降低群集擁有者(class)和使用者之間的耦合度.

作法

1. 加入[為群集添加(add),移除(remove)元素]的函數.

2. 将[用以儲存群集]的值域初始化為一個空群集.

3. 編譯.

4. 找出[群集設值函數]的所有調用者.你可以修改那個設值函數,讓它使用上述建立立的[添加/移除元素]函數;也可以直接修改調用端,改讓它們調用上述建立立的[添加/移除元素]函數.

兩種情況下需要用到[群集設值函數];(1)群集為空時;(2)準備将原有群集替換為另一個群集時.

你或許會想運用Rename Method(273)為[群集設值函數]改名,從setXxx()改為initializeXxx()或replaceXxx().

5. 編譯,測試.

6. 找出所有[通過取值函數(getter)獲得群集并修改其内容]的函數.逐一修改這些函數,讓它們改用[添加/移除](add/remove)函數.每次修改後,編譯并測試.

7. 修改完上述所有[通過取值函數(getter)獲得群集并修改群集内容]的函數後,修改取值函數自身,使它傳回該群集的一個隻讀映件(read-only view).

在Java 2中,你可以使用Collection.unmodifiableXxx()得到該叢集的隻讀映件.

在Java 1.1中,你應該傳回群集的一份拷貝.

8. 編譯,測試.

9. 找出取值函數(getter)的所有使用者,從中找出應該存在于[群集之宿主對象(host object)]内的代碼.運用Extract Method(110)和Move Method(142)将這些代碼移到宿主對象去.

如果你使用Java 2,那麼本項重構到此為止.如果你使用Java 1.1,那麼使用者也許會喜歡使用枚舉(enumeration).為了提供這個枚舉,你應該這樣做.

1. 修改現有取值函數(getter)的名字,然後添加一個新取值函數,使其傳回一個枚舉.找出舊取值函數的所有被使用點,将它們都改為使用新取值函數.

如果這一步跨度太大,你可以先使用Rename Method(273)修改原取值函數的名稱;再建立一個新取值函數用以傳回枚舉;最後再修改所有調用者,使其調用新取值函數.

2. 編譯,測試.

Java 2擁有一組全新群集(collections)--并非僅僅加入一些新classes,而是完全改變了群集的風格.是以在Java 1.1和Java 2中,封裝群集的方式也完全不同.我首先讨論Java 2的方式,因為我認為功能更強大的Java 2 collections會取代Java 1.1 collections的地位.

Java 2

假設有個人要去上課.我們用一個簡單的Course來表示[課程]:

class Course...

   public Course(String name, boolean isAdvanced) {...};

   public boolean isAdvanced() {...};

我不關心課程其他細節.我感興趣的是表示[人]的Person:

class Person...

   public Set getCourse() {

      return _courses;

   }

   public void setCourse(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中的群集(collections)建立合适的修改函數(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(273)更明确地展示這個函數的意圖.

   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 counter = 0;

   while(iter.hasNext()){

      Course each = (Course)iter.next();

      if(each.isAdvanced()) cout++;

   } 

因為以上隻使用了屬于Person的資料.首先我使用Extract Method(110)将這段代碼提煉為一個獨立函數:

   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(142)将這個函數搬移到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變得臃腫.但是在實際工作經驗中,我發現這通常并不成為問題.

Java1.1的代碼省略

封裝數組

數組經常被使用,但是我很少使用數組,因為我更喜歡功能更加豐富的群集類。進行封裝時,我常把數組換成其他群集。

這次我們的範例從一個字元串數組開始:

String[] getSkills() {

return _skills;

}

void setSkills (String[] arg) {

_skills = arg;

}

String[] _skills;

同樣地,首先我要提供一個修改函數,由于使用者有可能修改數組中某一特定位置上的值,是以我提供的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]);

}

如果要處理删除元素和參數的數組長度不一緻的情況會比較複雜,是以優先使用群集。

現在我需要修改取值函數的調用者,把下列代碼

kent.getSkills()[1] = "Refactoring";

改為

kent.setSkill(1,"Refactoring");

完成這一系列修改之後,我可以修改取值函數,令它傳回一份數組的拷貝:

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