軟件構造(09):“位置可更改”的三種實現及其比較


做Lab3的時候,碰到了一個問題:在“單個位置”的接口和實現類已經開發出來的前提下,“位置可更改”應該怎麼實現。筆者將在此列舉三種方法,從安全性和複用性的角度對它們進行比較。

實現“位置可更改”的基礎

在開發“位置可更改”功能之前,筆者已經完成了SingleLocationEntry接口及其實現類SingleLocationEntryImpl。但是考慮到並不是所有使用單個位置的計劃項的位置都可更改,筆者只實現了最基本的操作,讓它們管理單個Location,並能夠執行返回Location的功能:

public interface SingleLocationEntry {
	
	/**
	 * 獲取計劃項使用的位置
	 * 
	 * @return 計劃項使用的位置
	 */
	public Location getLocation();
}
public class SingleLocationEntryImpl implements SingleLocationEntry{

	private final Location location;
	
	/**
	 * 創建表示計劃項使用單個位置的對象
	 * 
	 * @param location 計劃項使用的位置
	 */
	public SingleLocationEntryImpl(Location location) {
		this.location = location;
		checkRep();
	}
	
	/**
	 * 檢查RI是否被滿足
	 */
	private void checkRep() {
		assert location != null;
	}
	
	@Override
	public Location getLocation() {
		Location newLocation = this.location;
		checkRep();
		return newLocation;
	}
}

問題何在

當開發到CourseEntry的時候,發現需要實現更改位置的功能。直接修改原有的這個SingleLocationEntryImpl類,強行在裏面加上changeLocation方法顯然是不合理的,因爲這一套表示計劃項特性的ADT是面向可複用開發的,如果客戶只想使用單個位置,但不想要位置可更改的功能,我們就不應該強迫用戶去接受這個冗餘的接口,然後在使用的過程中去unable一些不需要的方法。

所以我設計了一個新的接口LocationChangeableEntry,裏面只定義了一個changeLocation方法:

public interface LocationChangeableEntry {

	/**
	 * 更改計劃項使用的位置
	 * 
	 * @param location 計劃項使用的新位置
	 */
	public void changeLocation(Location location);
}

再設計一個新的類SingleLocationChangeableEntryImpl,通過某種方式整合原有的SingleLocationEntryImpl和新設計的接口LocationChangeableEntry,讓它兼有getLocation和changeLocation方法。
新的類需要實現LocationChangeableEntry,這是毋庸置疑的。但關鍵是新的類應該以何種方式與原有的SingleLocationEntryImpl交互?

方案1:蹩腳的委託

最開始考慮的方案比較理想化,筆者嘗試使用一種類似於Decorator模式的結構,在SingleLocationChangeableEntryImpl中包含一個SingleLocationEntryImpl類型的成員變量,命名爲singleLocationEntry。getLocation方法委託給這個成員變量來實現,changeLocation方法直接訪問並修改SingleLocationEntryImpl內部的Location對象。

public class SingleLocationChangeableEntryImpl implements SingleLocationEntry, LocationChangeableEntry {

	private final SingleLocationEntryImpl singleLocationEntry;
	
	/**
	 * 創建表示計劃項使用單個位置,且該位置可更改的對象
	 * 
	 * @param singleLocationEntry 計劃項使用的單位置管理器
	 */
	public SingleLocationChangeableEntryImpl(SingleLocationEntryImpl singleLocationEntry) {
		this.singleLocationEntry = singleLocationEntry;
		checkRep();
	}
	
	/**
	 * 檢查RI是否被滿足
	 */
	private void checkRep() {
		assert singleLocationEntry!=null;
	}
	
	@Override
	public Location getLocation() {
		checkRep();
		return singleLocationEntry.getLocation();
	}
	
	@Override
	public void changeLocation(Location location) {
		singleLocationEntry.location = location;
		checkRep();
	}
}

很可惜,這種方法從安全性角度就說不過去了。讓外部的類直接訪問SingleLocationEntryImpl內部的Location對象肯定是不行的,可能原本客戶並不想讓這個位置被修改,但是有人惡意地用一個SingleLocationChangeableEntryImpl來包裝這個SingleLocationEntryImpl對象,“關門打狗,甕中捉鱉”,從而達到修改位置的邪惡目的,我們肯定不能允許這種事情發生。

方案2:無情的復讀機

所以問題的關鍵是什麼?這個Location對象一定要被SingleLocationChangeableEntryImpl自身擁有!擅自修改別人的成員變量會產生安全威脅,只有當這個Location被自身擁有時,修改起來才安心。

那麼,從頭開始寫一遍SingleLocationChangeableEntryImpl,實現getLocation和changeLocation方法如何?

public class SingleLocationChangeableEntryImpl implements SingleLocationEntry, LocationChangeableEntry {

	private Location location;
	/**
	 * 創建表示計劃項使用單個位置,且該位置可更改的對象
	 * 
	 * @param location 計劃項使用的位置
	 */
	public SingleLocationChangeableEntryImpl(Location location) {
		this.location = location;
		checkRep();
	}
	
	/**
	 * 檢查RI是否被滿足
	 */
	private void checkRep() {
		assert location!=null;
	}
	
	@Override
	public Location getLocation() {
		checkRep();
		return location;
	}

	@Override
	public void changeLocation(Location location) {
		this.location = location;
		checkRep();
	}	
}

也不好!明明SingleLocationEntryImpl都已經有了getLocation方法,如果不去試着複用,在這裏重複造輪子,當復讀機,顯然造成了複用性變差。

方案3:繼承

綜合考慮安全性和複用性,最終採用了以下的方案:讓SingleLocationChangeableEntryImpl繼承SingleLocationEntryImpl類,並實現LocationChangeableEntry接口。

public class SingleLocationChangeableEntryImpl extends SingleLocationEntryImpl implements LocationChangeableEntry {

	/**
	 * 創建表示計劃項使用單個位置,且該位置可更改的對象
	 * 
	 * @param location 計劃項使用的位置
	 */
	public SingleLocationChangeableEntryImpl(Location location) {
		super(location);
		checkRep();
	}
	
	/**
	 * 檢查RI是否被滿足
	 */
	private void checkRep() {
		assert super.location!=null;
	}
	
	@Override
	public void changeLocation(Location location) {
		super.location = location;
		checkRep();
	}
}

通過這種方法,SingleLocationChangeableEntryImpl擁有了自己的Location。雖然這個Location是從父類繼承的,但是我們達到了這樣的目的:父類的這個Location對象只有繼承它的SingleLocationChangeableEntryImpl對象可以訪問。
這樣SingleLocationChangeableEntryImpl與SingleLocationEntryImpl之間不存在依賴注入的關係,外界根本沒有辦法訪問SingleLocationEntryImpl的對象,更不用提修改它的內容了。

注意這裏有一個很多人一直以來存在的誤解,認爲使用繼承的方式,SingleLocationEntryImpl類的Location域就不得不改爲private的,因此是不安全的,其實不然。
根據上面的討論,首先對子類可見一定是安全的,因爲創建子類不會對外產生對父類的引用,因此同一個包內的其他類或是其他子類的對象是沒有辦法通過子類來破壞父類對象內部表示的。其次,對於同一個包內的類可見,只要規範包內的類的行爲即可。

事實上,筆者個人認爲private,final這些關鍵字只是爲開發人員的編程提供某些規範,並不是出於安全性考慮。因爲使用反射機制可以很容易地繞過這些限制,用安全性來解釋很多事情就會顯得有些牽強。但是針對我們這門課程,筆者個人認爲還是應該接受安全性的解釋。

說了這麼多,我們比較了“位置可更改”的三種實現:
第一種方案實現使用了蹩腳的委託,導致安全性變差;
第二種什麼機制也不用,成爲了無情的復讀機,導致複用性變差;
第三種使用繼承的機制,可以在安全性和複用性之間找到比較好的平衡。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章