重构类关系-Extract Subclass提炼子类六
1.提炼子类
1.1.使用场景
类中的某些特性只被某些(而非全部)实例用到。新建一个子类,将上面所说的那一部分特性移到子类中
使用Extract Subclass (330)的主要动机是:你发现类中的某些行为只被一部分实例用到,其他实例不需要它们。有时候这种行为上的差异是通过类型码区分的,此时你可以使用Replace Type Code with Subclasses (223)或Replace Type Code with State/Strategy (227)。但是,并非一定要出现了类型码才表示需要考虑使用子类。
Extract Class (149)是Extract Subclass (330)之外的另一种选择,两者之间的抉择其实就是委托和继承之间的抉择。
Extract Subclass (330)通常更容易进行,但它也有限制:一旦对象创建完成,你无法再改变与类型相关的行为。但如果使用Extract Class (149),你只需插入另一个组件就可以改变对象的行为。此外,子类只能用以表现一组变化。如果你希望一个类以几种不同的方式变化,就必须使用委托。
1.2.如何做
- 为源类定义一个新的子类。
- 为这个新的子类提供构造函数。
- 简单的做法是:让子类构造函数接受与超类构造函数相同的参数,并通过super调用超类构造函数。
- 如果你希望对用户隐藏子类的存在,可使用Replace Constructor with Factory Method (304)。
- 找出调用超类构造函数的所有地点。如果它们需要的是新建的子类,令它们改而调用新构造函数。
- 如果子类构造函数需要的参数和超类构造函数的参数不同,可以使用Rename Method (273)修改其参数列。如果子类构造函数不需要超类构造函数的某些参数,可以使用Rename Method (273)将它们去除。
- 如果不再需要直接创建超类的实例,就将超类声明为抽象类。
- 逐一使用Push Down Method (328)和Push Down Field (329)将源类的特性移到子类去。
- 和Extract Class (149)不同的是,先处理函数再处理数据,通常会简单一些。当一个public函数被下移到子类后,你可能需要
新定义该函数的调用端的局部变量或参数类型,让它们改而调用子类中的新函数。如果忘记进行这一步骤,编译器会提醒你。 - 找到所有这样的字段:它们所传达的信息如今可由继承体系自身传达(这一类字段通常是boolean变量或类型码)。以Self Encapsulate Field (171)避免直接使用这些字段,然后将它们的取值函数替换为多态常量函数。所有使用这些字段的地方都应该以Replace Conditional with Polymorphism (255)重构。
- 任何函数如果位于源类之外,而又使用了上述字段的访问函数,考虑以Move Method (142)将它移到源类中,然后再使用Replace Conditional with Polymorphism (255)。
- 每次下移之后,编译并测试。
1.3.示例
下面是JobItem 类,用来决定当地修车厂的工作报价
// 工作报价类
class JobItem ...
public JobItem (int unitPrice, int quantity, boolean isLabor, Employee employee) {
_unitPrice = unitPrice;
_quantity = quantity;
_isLabor = isLabor;
_employee = employee;
}
public int getTotalPrice() {
return getUnitPrice() * _quantity;
}
public int getUnitPrice(){
return (_isLabor) ?
_employee.getRate():
_unitPrice;
}
public int getQuantity(){
return _quantity;
}
public Employee getEmployee() {
return _employee;
}
private int _unitPrice;
private int _quantity;
private Employee _employee;
private boolean _isLabor;
// 员工类
class Employee...
public Employee (int rate) {
_rate = rate;
}
public int getRate() {
return _rate;
}
private int _rate;
我要提炼出一个LaborItem子类,因为上述某些行为和数据只在按工时(labor)收费的情况下才需要。首先建立这样一个类:
class LaborItem extends JobItem {
}
我需要为LaborItem提供一个构造函数,因为JobItem没有默认构造函数。我把超类构造函数的参数列复制过来:
public LaborItem (int unitPrice, int quantity, boolean isLabor, Employee employee) {
super (unitPrice, quantity, isLabor, employee);
}
这就足以让新的子类通过编译了。但是这个构造函数会造成混淆:某些参数是LaborItem所需要的,另一些不是。稍后我再来解决这个问题。
下一步是要找出对JobItem构造函数的调用,并从中找出可以改用LaborItem构造函数的地方。因此,下列语句:
JobItem j1 = new JobItem (0, 5, true, kent);
就被修改为
JobItem j1 = new LaborItem (0, 5, true, kent);
此时我尚未修改变量类型,只是修改了构造函数所属的类。之所以这样做,是因为我希望只在必要地点才使用新类型。到目前为止,子类还没有专属接口,因此我还不想宣布任何改变。
现在正是清理构造函数参数列的好时机。我将针对每个构造函数使用Rename Method (273)。首先处理超类构造函数。我要新建一个构造函数,并把旧构造函数声明为protected(不能直接声明为private,因为子类还需要它):
class JobItem...
protected JobItem (int unitPrice, int quantity, boolean isLabor, Employee employee) {
_unitPrice = unitPrice;
_quantity = quantity;
_isLabor = isLabor;
_employee = employee;
}
public JobItem (int unitPrice, int quantity) {
this (unitPrice, quantity, false, null)
}
现在,外部调用应该使用新构造函数
JobItem j2 = new JobItem (10, 15);
编译、测试都通过后,我再使用Rename Method (273)修改子类构造函数
class LaborItem
public LaborItem (int quantity, Employee employee) {
super (0, quantity, true, employee);
}
此时我仍然暂时使用protected的超类构造函数。
现在,我可以将JobItem的特性向下搬移。先从函数开始,我先运用Push Down Method (328)对付getEmployee()函数:
class LaborItem...
public Employee getEmployee() {
return _employee;
}
class JobItem...
protected Employee _employee;
因为_employee字段也将在稍后被下移到LaborItem,所以我现在先将它声明为protected。
将_employee字段声明为protected之后,我可以再次清理构造函数,让_employee只在即将去达的子类中被初始化:
class JobItem...
protected JobItem (int unitPrice, int quantity, boolean isLabor) {
_unitPrice = unitPrice;
_quantity = quantity;
_isLabor = isLabor;
}
class LaborItem ...
public LaborItem (int quantity, Employee employee) {
super (0, quantity, true);
_employee = employee;
}
_isLabor字段所传达的信息,现在已经成为继承体系的内在信息,因此我可以移除这个字段了。最好的方式是:先使用Self Encapsulate Field (171),然后再修改访问函数,改用多态常量函数——这样的函数会在不同的子类实现版本中返回不同的固定值:
class JobItem...
protected boolean isLabor() {
return false;
}
class LaborItem...
protected boolean isLabor() {
return true;
}
然后,我就可以摆脱_isLabor字段了。
现在,我可以观察isLabor()函数的用户,并运用Replace Conditional with Polymorphism (255)重构它们。我找到了下列这样的函数
class JobItem...
public int getUnitPrice(){
return (isLabor()) ?
_employee.getRate():
_unitPrice;
}
将它重构为:
class JobItem...
public int getUnitPrice(){
return _unitPrice;
}
class LaborItem...
public int getUnitPrice(){
return _employee.getRate();
}
当使用某项字段的函数全被下移至子类后,我就可以使用Push Down Field (329)将字段也下移。如果尚无法移动字段,那就表示我需要对函数做更多处理,可能需要实施Push Down Method (328)或Replace Conditional with Polymorphism (255)。
由于只有按零件收费的工作项才会用到_unitPrice字段,所以我可以再次运用Extract Subclass (330)对JobItem提炼出一个子类:PartsItem。完成后,我可以将JobItem声明为抽象类。