Summary:两个类之间有双向关联,但其中一个类如今不再需要另一个类的特性。去除不必要的关联。
动机:
双向关联很有用,但你也必须为它付出代价,那就是维护双向连接、确保对象被正确创建和删除而增加的复杂度。而且,由于很多程序员并不习惯使用双向关联,它往往成为错误之源。
大量的双向连接也很容易造成“僵尸对象”:某个对象本来已经该死亡了,却任然保留在系统中,因为对它的引用还没有完全清除。
此外,双向关联也迫使两个类之间有了依赖:对其中任何一个类的修改,都可能引发另一个类的变化。如果这两个类位于不同的包,这种依赖就是包与包之间的相依。过多的跨包依赖会造成紧耦合系统,使得任何一点小改动都可能造成许多无法预知的后果。
只有在真正需要双向关联的时候,才应该使用它。如果发现双向关联不再有存在的价值,就应该去掉其中不必要的一条关联。
做法:
1.找出保存“你想去除的指针”的字段,检查它的每一个用户,判断是否可以去除改指针
à不但要检查直接访问点,也要检查调用这些直接访问点的函数。
à考虑有无可能不通过指针取得被引用对象。如果有可能,你就可以对取值函数使用Substitute Algorithm,从而让客户在没有指针的情况下也可以使用该取值函数
à对于使用该字段的所有函数,考虑将被引用对象作为参数传进去。
2.如果客户使用了取值函数,先运用Self Encapsulate Field将待删除字段自我封装起来,然后使用Substitute Algorithm对付取值函数,令它不再使用该字段,然后编译、测试。
3.如果客户未使用取值函数,那就直接修改待删除字段的所有被引用点:改以其他途径获得该字段所保存的对象。每次修改后,编译并测试。
4.如果已经没有任何函数使用带删除字段,移除所有对该字段的更新逻辑,然后移除该字段
à 如果有许多地方对此字段赋值,先运用Self Encapsulate Field使这些地点改用同一个设值函数。编译、测试。而后将这个设值函数的本体清空。再编译、再测试。如果这些都可行,就可以将此字段和其设值函数,连同对设值函数的所有调用,全部移除。
5.编译,测试。
范例
本例从Change Unidirectional Association to Bidirectional留下的代码开始进行,其中Customer和Order之间有双向关联:
class Order... Customer getCustomer(){ return _customer; } Void setCustomer(Customer arg){ if(_customer !=null){ _customer.friendOrders().remove(this); } _customer = arg; if(_customer !=null){ _customer.friendOrders().add(this); } private Customer _customer; } class Customer ... void addOrder(Order arg){ arg.setCustomer(this); } private Set _orders = new HashSet(); Set friendOrders(){ /** should only be used by Order*/ return _orders; }
后来我们发现,除非先有Customer对象,否则不会存在Order对象。因此我想将从Order到Customer的连接移除掉。
对于本项重构来说,最困难的就是检查可行性。如果我知道本项重构时安全的,那么重构手法自身十分简单。问题在于是否有任何代码依赖_customer字段存在。如果确实有,那么在删除这个字段之后,必须提供替代品。
首先,需要研究所有读取这个字段的函数,以及所有使用这些函数的函数。我能找到另一条途径来提供Customer对象吗--这通常意味着将Customer对象作为参数传递给用户。下面是一个简化例子:
class Order ... double getDiscountedPrice(){ return getGrossPrice() * (1 - _customer.getDiscount()); }
改变为
class Order... double getDiscountedPrice(Customer customer){ return getGrossPrice() * (1 _ customer.getDiscount()); }
如果待改函数式被Customer对象调用的,那么这样的修改方案特别容易实施,因为Customer对象将自己作为参数传给函数很容易。所以下列代码:
class Customer .. double getPriceFor(Order order){ Assert.isTrue(_orders.contains(order)); return order.getDiscountedPrice(); }
变成了:
class Customer ... double getPriceFor(Order order){ Assert.isTrue(_orders.contains(order)); return order.getDiscountedPrice(this); }
另一种做法就是修改取值函数,使其在不使用_customer字段的前提下返回一个Customer对象。如果这行得通,就可以使用Substitute Algorithm修改Order.getCustomer()函数算法。我有可能这样修改代码:
Customer getCustomer(){ Iterator iter = Customer.getInstances().iterator(); while(iter.hasNext()){ Customer each = (Customer) iter.next(); if(each.containsOrder(this)){ return each; } } return null; }
这段代码比较慢,不过确实可行。而且在数据库环境下,如果我需要使用数据库查询语句,这段代码对系统性能的影响可能并不显著。如果Order类中有些函数使用_customer字段,我们可以实施Self Encapsulate Field令它们转而改用上述的getCustomer()函数。
如果我们要保留上述的取值函数,那么Order和Customer的关联从接口上看虽然仍是双向的,但实现上已经是单向关系了。随人移除了反指针,但两个类彼此之间的依赖关系仍然存在。
既然要替换取值函数,那么我们就专注地替换它,其他部分留待以后处理。逐一修改取值函数的调用者,让它们通过其他来源取得Customer对象。每次修改后都编译并测试。实际工作中这一过程往往相当快。如果这个过程让我们觉得很棘手很复杂,我们可以放弃本项重构。
一旦消除了_customer字段的所有读取点。就可以着手处理对此字段赋值的函数了。很简单,只要把这些赋值动作全部移除,再把字段一并删除就行了。由于已经没有任何代码需要这个字段。所以删掉它并不会带来任何影响