如何写出优美的代码(二)

(本文思想基本来自于经典著作《重构》一书)

上一篇 http://www.cnblogs.com/ceys/archive/2012/03/05/2379842.html#commentform

 

 

上一篇文章主要讲了怎么给函数整容。现在我们大家基本上都使用面向对象语言,什么样的“对象”才是优美的呢?

类中的函数、字段应该和该类最紧密相关,如果和另一个类有更多交互,搬移它。搬移字段时,如果很多函数引用了该字段,可以把该字段用get/set方法自我封装起来。这样搬移时只需要修改get/set访问函数。

一个类应该是一个清楚的抽象,处理一些明确的责任。否则建立一个新类,用以表现从旧类中分离出来的责任。提炼类是改善并发程序的一种常用技术,因为提炼后可以给两个类分别加锁。需要注意,提炼出新类,要考虑要不要对用户公开这个新类。如果允许任何用户修改其对象的任何部分,它就成为引用对象;如果不允许任何人不通过对象A修改它的任何部分,则可以将其设为不可修改的,或为它提供不可修改的接口,将A中所有与其相关的函数委托之,从而完全隐藏这个类。

每个对象应该尽可能少了解系统的其它部分,当客户通过一个委托类调用另一个对象,在服务类上建立客户需要的所有函数,隐藏委托关系,减少耦合。然而,如果受托类的功能越来越多,服务类就完全变成了一个中介。很难说隐藏到什么程度是合适的。

当需要为服务类提供一些额外函数,但无法修改这个类时,建立一个新类,使它包含这些额外函数,成为源类的子类或包装类。如果函数较少,直接引入外加函数即可。

子类的工作量比较少,但它有两个问题:第一,必须在对象创建期实施,如果对象创建之后,就不能用了;第二,子类化会产生一个子类对象,如果有其它对象引用了旧对象,就同时又两个对象保存了原数据,如果原数据允许被修改,一个修改动作无法同时改变两份副本。这个时候就要用包装类。包装类需要为原始类的所有函数提供委托函数。 

 

二、重新组织数据

 

面向对象语言有一个有用的特征:允许定义新类型。这样我们可以利用对象,组织数据。

如果有一个数据项,需要与其他数据和行为一起使用才有意义,将数据项变为对象。

如果一个类衍生出许多彼此相等的实例,希望将它们替换为同一个对象,将这个值对象变成引用对象。每个引用对象都代表真实世界中的一个事物,可以以==检查两个对象是否相等。值对象由其所含数值定义,不在意副本存在。这里举个简单例子:

值对象:

class Customer {
	public Customer(String name) {
		_name = name;
	}
	public String getName() {
		return _name;
	}
	private final String _name;
}
class Order {
	public Order(String customerName) {
		_customer = new Customer(customerName);
	}
	//set,get...
	private Customer _customer;
	//other function use Customer...
}

这里,就算多分订单属于同一客户,每个Order对象还是拥有各自的Customer对象。现在把它改成引用对象,即每个客户名称只对应一个Customer对象:

class Customer {
	public static Customer getNamed (String name) {
		return (Customer) _instances.get(name);
	}
	private Customer (String name) {
		_name = name;
	}
	private static Dictionary _instances = new Hashtable();
	static void loadCustomers() {
		new Customer("Gang Li").store();
	}
	private void store() {
		_instances.put(this.getName(), this);
	}
	//...
}
class Order {
	public Order (String customer) {
		_customer = Customer.getNamed(customer);
	}
	//...
}	

我们首先创建工厂函数,以控制Customer对象的创建过程。然后,需要决定如何访问Customer对象,可以利用另一个对象访问,但这里Order类没有一个明显的字段可用于访问,所以可以创建一个对象保存所有Customer对象。这里为了简化,利用Customer里的静态字典保存。让Customer类作为访问点。然后,决定何时创建Customer对象。这里为了简化,在loadCustomer里预先把需要使用的Customer对象创建好。

引用对象可能造成内存区域之间错综复杂的关联。在分布系统和并发系统中,不可变的值对象特别有用,因为无需考虑他们的同步问题。这里的不可变指的是对象自身不能改变,比如money对象。当判断相等时需覆写equals()和hashCode()。(实现hashCode的简单办法是把equals()使用的所有字段安慰异或操作)。

如果两个类都需要使用对方特性,但只有一条单向连接,添加一个反向指针,并使修改函数能同时更新两条连接。以上面的代码为例:

class Customer {
	//客户拥有多份订单
	private Set _orders = new HashSet();
	//添加只包内可见的辅助函数,让Order可以直接访问_orders集合
	Set friendOrders() {
		return _orders;
	}
	//如果需要在Customer中也能修改连接,就让它调用控制函数
	void addOrder(Order arg) {
		arg.setCustomer(this);
	}
class Order...
	void setCustomer(Customer arg) {
		if (_customer != null)
		_customer.friendOrders().remove(this);
		_customer = arg;
		if (_customer != null)
		_customer.friendOrders().add(this);
	}	

如果一个数组,其元素各自代表不同的东西,以对象替换数组

以字面常量取代魔法数

如果有个函数返回一个集合,让这个函数返回该集合的一个只读副本,并在这个类中提供添加/移除集合元素的函数。如果函数返回集合自身,会让用户在拥有者不知情的情况下修改集合修改集合。所以不该为集合提供设置函数。但可以提供为集合增减元素的函数。例如:

class Person{
	//封装集合操作
	public void addCourse (Course arg) {
		_courses.add(arg);
	}
	public void removeCourse (Course arg) {
		_courses.remove(arg);
	}
	public void initializeCourses(Set arg) {
		Assert.isTrue(_courses.isEmpty());
		Iterator iter = arg.iterator();
		while (iter.hasNext()) {
			addCourse((Course) iter.next());
		}
	}
	//确保没有用户通过取值函数修改集合
	public Set getCourses() {
		return Collections.unmodifiableSet(_courses);
	}
	private Set _courses = new HashSet();
}

posted on 2012-03-09 22:19  大俗人  阅读(2803)  评论(0编辑  收藏  举报