一、抽象类与接口
有的朋友可能会问一个问题,不是说在Java中应该尽量面向接口编程吗,为何模板方法的模板是采用的抽象方法呢?
要回答这个问题,要首先搞清楚抽象类和接口的关系:
接口是一种特殊的抽象类,所有接口中的属性自动是常量,也就是public final static的,而所有接口中的方法必须是抽象的
抽象类,简单点说是用abstract修饰的类。这里要特别注意的是抽象类和抽象方法的关系,记住两句话:
抽象类不一定包含抽象方法;有抽象方法的类一定是抽象类
抽象类和接口相比较,最大的特点就在于抽象类里面是可以有具体的实现方法的,而接口中所有的方法都是没有具体的实现的。
因此,虽然Java编程中倡导大家“面向接口编程”,并不是说就不再使用抽象类了,那么什么时候使用抽象类呢?
通常在“既要约束子类的行为,又要为子类提供公共功能”的时候使用抽象类。
按照这个原则来思考模板方法模式的实现,模板方法模式需要固定定义算法的骨架,这个骨架应该只有一份,算是一个公共的行为,但是里面具体的步骤的实现又可能是各不相同的,恰好符合选择抽象类的原则。
把模板实现成为抽象类,为所有的子类提供了公共的功能,就是定义了具体的算法骨架;同时在模板里面把需要由子类扩展的具体步骤的算法定义成为抽象方法,要求子类去实现这些方法,这就约束了子类的行为。
因此综合考虑,用抽象类来实现模板是一个很好的选择。
二、模式的功能
模板方法的功能在于固定算法骨架,而让具体算法实现可扩展。
这在实际应用中非常广泛,尤其是在设计框架级功能的时候非常有用。框架定义好了算法的步骤,在合适的点让开发人员进行扩展,实现具体的算法。比如在DAO实现中,设计通用的增删改查功能,这个后面会给大家示例。
模板方法还额外提供了一个好处,就是可以控制子类的扩展。因为在父类里面定义好了算法的步骤,只是在某几个固定的点才会调用到被子类实现的方法,因此也就只允许在这几个点来扩展功能,这些个可以被子类覆盖以扩展功能的方法通常被称为“钩子”方法,后面也会给大家示例。
三、变与不变程序设计的一个很重要的思考点就是“变与不变”,也就是分析程序中哪些功能是可变的,哪些功能是不变的,然后把不变的部分抽象出来,进行公共的实现,把变化的部分分离出去,用接口来封装隔离,或者是用抽象类来约束子类行为。
模板方法模式很好的体现了这一点。模板类实现的就是不变的方法和算法的骨架,而需要变化的地方,都通过抽象方法,把具体实现延迟到子类去了,而且还通过父类的定义来约束了子类的行为,从而使系统能有更好的复用性和扩展性。
四、好莱坞法则
什么是好莱坞法则呢?简单点说,就是“不要找我们,我们会联系你”。
模板方法模式很好的体现了这一点,做为父类的模板会在需要的时候,调用子类相应的方法,也就是由父类来找子类,而不是让子类来找父类。
这其实也是一种反向的控制结构,按照通常的思路,是子类找父类才对,也就是应该是子类来调用父类的方法,因为父类根本就不知道子类,而子类是知道父类的,但是在模板方法模式里面,是父类来找子类,所以是一种反向的控制结构。
五、单一职责原则SRP(Single Responsibility Principle)
所谓单一职责原则,指的就是:一个类应该仅有一个引起它变化的原因。
这里变化的原因就是所说的“职责”,如果一个类有多个引起它变化的原因,那么也就意味着这个类有多个职责,再进一步说,就是把多个职责耦合在一起了。
这会造成职责的相互影响,可能一个职责的变化,会影响到其它职责的实现,甚至引起其它职责跟着变化,这种设计是很脆弱的。
这个原则看起来是最简单和最好理解的,但是实际上是很难完全做到的,难点在于如何区分这个“职责”,这是个没有标准量化的东西,哪些算职责,到底这个职责有多大的粒度,这个职责如何细化等等。因此,在实际开发中,这个原则也是最容易违反的。
六、 开放-关闭原则OCP(Open-Closed Principle)
所谓开放-关闭原则,指的就是:一个类应该对扩展开放,对修改关闭。一般也被简称为开闭原则,开闭原则是设计中非常核心的一个原则。
开闭原则要求的是,类的行为是可以扩展的,而且是在不修改已有的代码的情况下进行扩展,也不必改动已有的源代码或者二进制代码。
看起来好像是矛盾的,怎么样才能实现呢?
实现开闭原则的关键就在于合理的抽象,分离出变化与不变化的部分,为变化的部分预留下可扩展的方式,比如:钩子方法、或是动态组合对象等等。
这个原则看起来也很简单,但事实上,一个系统要全部做到遵守开闭原则,几乎是不可能的,也没这个必要。适度的抽象可以提高系统的灵活性,使其可扩展、可维护,但是过度的抽象,会大大增加系统的复杂程度。应该在需要改变的地方应用开闭原则就可以了,而不用到处使用,从而陷入过度设计。
七、 里氏替换原则LSP(Liskov Substitution Principle)
所谓里氏替换原则,指的就是:子类型必须能够替换掉它们的父类型。这很明显是一种多态的使用情况,它可以避免在多态的应用中,出现某些隐蔽的错误。
事实上,当一个类继承了另外一个类,那么子类就拥有了父类中可以继承下来的属性和操作,理论上来说,此时使用子类型去替换掉父类型,应该不会引起原来使用父类型的程序出现错误。
但是,很不幸的是,在某些情况下是会出现问题的,比如:如果子类型覆盖了父类型的某些方法,或者是子类型修改了父类型某些属性的值,那么原来使用父类型的程序就可能会出现错误,因为在运行期间,从表面上看,它调用的是父类型的方法,需要的是父类型方法实现的功能,但是实际运行调用的确是子类型覆盖实现的方法,而该方法跟父类型的方法并不一样,那就会导致错误的产生。
从另外一个角度来说,里氏替换原则是实现开闭的主要原则之一,开闭原则要求对扩展开放,扩展的一个实现手段就是使用继承,而里氏替换原则是保证子类型能够正确替换父类型,只有能正确替换,才能实现扩展,否则扩展了也会出现错误。
八、 依赖倒置原则DIP(Dependence Inversion Principle)
所谓依赖倒置原则,指的就是:要依赖于抽象,不要依赖于具体类。要做到依赖倒置,典型的应该做到:
- 高层模块不应该依赖于底层模块,二者都应该依赖于抽象
- 抽象不应该依赖于具体实现,具体实现应该依赖于抽象
很多人觉得,层次化调用的时候,应该是高层调用“底层所拥有的接口”,这是一种典型的误解。事实上,一般高层模块包含对业务功能的处理和业务策略选择,应该被重用,应该是高层模块去影响底层的具体实现。
因此,这个底层的接口,应该是由高层提出的,然后由底层实现的,也就是说底层的接口的所有权在高层模块,因此是一种所有权的倒置。
倒置接口所有权,这就是著名的Hollywood(好莱坞)原则:不要找我们,我们会联系你。
九、接口隔离原则ISP(Interface Segregation Principle)
所谓接口隔离原则,指的就是:不应该强迫客户依赖于他们不用的方法。
这个原则用来处理那些比较“庞大”的接口,这种接口通常会有较多的操作声明,涉及到很多的职责。客户在使用这样的接口的时候,通常会有很多它不需要的方法,这些方法对于客户来讲,就是一种接口污染,相当于强迫用户在一大堆“垃圾方法”里面去寻找他需要的方法。
因此,这样的接口应该被分离,应该按照不同的客户需要来分离成为针对客户的接口,这样的接口里面,只包含客户需要的操作声明,这样既方便了客户的使用,也可以避免因误用接口而导致的错误。
分离接口的方式,除了直接进行代码分离之外,还可以使用委托来分离接口,在能够支持多重继承的语言里面,还可以采用多重继承的方式进行分离。
十、最少知识原则(Least Knowledge Principle)
所谓最少知识原则,指的就是:只和你的朋友谈话。
这个原则用来指导我们在设计系统的时候,应该尽量减少对象之间的交互,对象只和自己的朋友谈话,也就是只和自己的朋友交互,从而松散类之间的耦合。通过松散类之间的耦合来降低类之间的相互依赖,这样在修改系统的某一个部分时候,就不会影响其它的部分,从而使得系统具有更好的可维护性。
那么究竟哪些对象才能被当作朋友呢?最少知识原则提供了一些指导:
- 当前对象本身
- 通过方法的参数传递进来的对象
- 当前对象所创建的对象
- 当前对象的实例变量所引用的对象
- 方法内所创建或实例化的对象
总之,最少知识原则要求我们的方法调用,必须保持在一定的界限范围之内,尽量减少对象的依赖关系。
十一、 其它原则
除了上面提到的这些原则,还有一些大家都熟知的原则,比如:
- 面向接口编程
- 优先使用组合,而非继承
当然也还有很多大家不是很熟悉的原则,比如:
- 一个类需要的数据应该隐藏在类的内部
- 类之间应该零耦合,或者只有传导耦合,换句话说,类之间要么没有关系,要么只使用另一个类的接口提供的操作
- 在水平方向上尽可能统一的分布系统功能