层次结构原则倡导以分类,归并,替换和排序等手法以层次方式组织抽象。面向对象编程语言支持两种形式的层次结构:类层次结构(is-a)和对象层次结构(has-a).下面的逻辑结构图展示了实现层次结构的手法和所对应的层次结构缺陷。
下面详细说明各种层次结构缺陷。
缺失的层次结构 缺陷
【概念定义】使用类型吗或条件逻辑来处理行为变化,表明没有进行有意义的分类,导致缺少层次结构。而且这种行为变化可能在代码中出现多次。
【违反原则】DRY原则,层次原则。
【缺陷实例】代码中大量使用if/else或switch语句,通过判断编码值来决定行为。
【重构建议】1)如果条件判断中调用的语句相同,可以将这些操作抽象出一个接口,使用多态性重构这些处理;2)创建一个新的类层次结构。
【现实考虑】如果系统要和外部进行交互,则很难避免使用条件逻辑。
不必要的层次结构 缺陷
【概念定义】在设计中,要进行有意义的分类,即捕获行为之间的差异,而不是数据方面的共性和差异。如果滥用继承,以子类型代替实例,就会产生此类缺陷。
【违反原则】分类原则
【缺陷实例】对于字体类Font,对每种不同的字体样式创建子类型,如ArialFont,GthoFont等。
【重构建议】将字体作为Font类的一个属性,删除整个字体样式子类层次。
【现实考虑】java.nio.Buffer的层次结构中,子类型都是各种基本类型的Buffer.由于Java的泛型不支持基本类型。所以采用预编译的手法生成子类。
未归并的层次结构 缺陷
【概念定义】类型之间存在相同或类似的代码,层次结构中存在不必要的重复,不要形式包括兄弟类型之间的重复和父子类型之间存在重复。
【违反原则】DRY原则
【缺陷实例】java.text.NumberFormat的两个子类ChoiceFormat和DecimalFormat中都含有同名的方法applyPattern()和toPattern()。但是父类中却不包含这两个方法。
【重构建议】可以将兄弟类型中的相同的方法接口,方法定义或字段上移到父类中;或者利用模板模式提取子类中的共性到父类中。对于上述的缺陷实例,需要在父类中引入和子类同名的两个抽象方法。
【实现考虑】由于Java不支持基本类型的泛型化。所以类AbstractQueuedSynchronizer和AbstractQueuedLongSynchronizer中含有大量的重复的代码,只是把状态的标示由int类型修改成了Long类型。
过宽的层次结构 缺陷
【概念定义】由于归并不充分,缺失中间类型。导致客户代码不得不直接引用中间类型,或类型间存在不必要的重复。在归并过程中,需要保证层次结构规模适中。如果任何类型的子类型超过9个,就可以判断存在这种缺陷。
【违反原则】层次原则
【缺陷实例】java.util.EventObject类有36个子类。这些子类中,有些子类不应该放在同一层次中。
【重构建议】提取超类,引入中间抽象。
凭空想像的层次结构 缺陷
【概念定义】在设计过程中,需要考虑计划内的未来需求。但如果设计人员进行了过度的设计,按照个人的想像进行了需求之外的抽象,会导致这种抽象。
【违反原则】层次结构原则
【缺陷实例】如果一个类型只有一个直接子类,并且之类中包含未被使用的方法。
【重构建议】使用紧缩层次结构方法,将不需要的层次结构删除。
过深的层次结构 缺陷
【概念定义】过度归并时产生大量的非必要的中间父类,有时中间父类还重写了继承自顶层父类的方法。导致设计复杂,难于理解。一般而言,超过6层的层次结构就可以判断为有这种缺陷。
【违反原则】归并原则
【缺陷实例】java.nio.channels.DatagramChannels类所属的类层次结构中,接口继承深度为4,类继承深度为7,存在此类缺陷。
【重构建议】删除不必要的层次结构
【实现考虑】较深的继承层次结构可以提高可重用性,很多使用广泛的框架和库都包含深的层次结构。架构师在设计时要权衡考虑。
叛逆型层次结构 缺陷
【概念定义】子类型拒绝父类提供的方法,一般来说,对于父类型的方法,子类型采取如下方式重写时,会导致这种缺陷。1)引发异常,以禁止调用该方法。2)重写方法不执行任何操作。3)仅仅打印出警告信息。4)想调用者返回一个错误值。也就是说,子类性和父类的实现不一致,导致破坏了子类和父类的is-a关系。
【违反原则】LSP,层次原则
【缺陷实例】java.util.Iterator类中声明了remove()方法。但是由于迭代器的作用是遍历和访问数据,所以实现Iterator类的子类,比如java.util.Scaner类的remove()方法会抛出异常UnsupportedOperationException.
【重构建议】如果父类中的方法只适用于部分子类,则把父类中的方法下移到子类,或引入中间父类解决这个问题。对于迭代器的重构建议,可以创建Iterator的两个抽象子类ReadOnlyIterator和ReadWriteIterator并把remove()方法下移到ReadWriteItertor()中。
【实现考虑】如果为了性能考虑或者为未实现的功能预留的占位符,则违反这种设计是可以接受的。
支离破碎的层次结构 缺陷
【概念定义】父类和子类之间不存在is-a关系,导致替换性遭到破坏,有如下三种形式:1)父类的方法对子类方法适用或相关;2)子类没有拒绝从父类继承来的对自己无关的方法;3)子类显式的拒绝了从父类继承过来的方法。这种缺陷的极端现象是父类和子类的继承关系反转了。
【违反原则】里氏代换原则
【缺陷实例】1)java.utilStack类继承了java.util.Vector类,并没有拒绝相关的方法,导致可以在Stack的中间插入或删除数据。显然这两个类不存在is-a关系;2)java.util.Properties继承了java.util.Hashtable<Object,Object>类,导致可以使用put()和putAll()方法向Properties中插入非String的键值;3)java.util.Date类是java.sql.Date和java.sql.Time的父类。但是java.sql.Date类拒绝了父类中与时间有关的方法,而java.sql.Time类拒绝了所有与日期有关的方法。
【重构建议】使用“委托代替继承”的重构方法,以has-a关系取代is-a关系。
【实现考虑】类适配器模式中,adpater类和adaptee类之间不存在is-a的关系,存在这种缺陷。
多路径层次结构 缺陷
【概念定义】子类型通过多条路径直接或间接的继承父类,导致层次结构中存在多余的继承路径。这种缺陷和层次结构中的菱形层次结构不同,菱形层次结构中的每条路径都不是多余的。
【违反原则】层次原则
【缺陷实例】java.util.concurrent.ConcurrentLinkedQueue类实现了接口java.util.Queue,并扩展了java.util.AbstractQueue类。而AbstractQueue类是Queue的子类。因此ConcurrentLinkedQueue类的继承层次中存在多路径层次结构缺陷。
【重构建议】删除多余的继承路径。
【实现考虑】如果一个设计的决策利大于弊,那么引入一些设计缺陷是可以接受的。
循环的层次结构 缺陷
【概念定义】在层次结构中,父类依赖于子类,包括父类包含子类对象,父类引用子类,父类方位子类的方法或数据成员,会导致这种缺陷。
【违反规则】层次原则
【缺陷实例】AbstractButton类通过SwingUtility类和JRootPane类间接地依赖于JButton类,而JButton类是AbstractButton的子类,导致循环的层次结构缺陷。
【重构建议】1)如果父类中引用子类型是不必要的,就把子类型对象删除;2)如果父类和子类的耦合性非常高,就将父类和子类合并为一个;3)如果父类必须使用子类提供的服务,可以使用状态模式或策略模式。
【实现考虑】如果能够确保循环路径上的类在未来不发生变化,这种缺陷是可以接受的。