欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

软件构造(09):“位置可更改”的三种实现及其比较

程序员文章站 2022-03-10 15:17:44
...


做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这些关键字只是为开发人员的编程提供某些规范,并不是出于安全性考虑。因为使用反射机制可以很容易地绕过这些限制,用安全性来解释很多事情就会显得有些牵强。但是针对我们这门课程,笔者个人认为还是应该接受安全性的解释。

说了这么多,我们比较了“位置可更改”的三种实现:
第一种方案实现使用了蹩脚的委托,导致安全性变差;
第二种什么机制也不用,成为了无情的复读机,导致复用性变差;
第三种使用继承的机制,可以在安全性和复用性之间找到比较好的平衡。

相关标签: 软件构造