如何写出优美的代码(二)
(本文思想基本来自于经典著作《重构》一书)
上一篇 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(); }