范例(Examples)
Java 2拥有一组全新群集(collections)--并非仅仅加入一些新classes,而是完全改变了群集的风格.所以在Java 1.1和Java 2中,封装群集的方式也完全不同.我首先讨论Java 2的方式,因为我认为功能更强大的Java 2 collections会取代Java 1.1 collections的地位.
范例(Examples): 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(Person person) {
Iterator iter = person.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变得臃肿.但是在实际工作经验中,我发现这通常并不成为问题.