head first设计模式笔记:模板方法模式

2053 查看

为什么需要模板方法模式?

有时候我们需要做相似的一类事情,它们的总体步骤差不多,而细节上又有不同。我们希望抽象相同的部分,而一些子步骤又可以定制。甚至可以控制某些步骤是否要做。

比如一个薪酬管理的系统,假设有两种雇员:工人和工程师。

工人有基本工资,有计件工资。

工程师有基本工资,没有计件工资,有职级工资,职级越高工资越高。

所有员工都有加班费。

这时我们就可以在雇员超类中定义一个计算工资的模板方法,把相同的部分抽象,而具体的计算每种工资的子方法由子类完成。

什么是模板方法模式?

《head first设计模式》中是这样定义的:

模板方法模式在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。

具体来说,就是将算法的每个步骤定义成一个方法,再定义一个模板方法按一定次序调用这些方法。子方法可以是抽象的,由子类实现,也可以有默认实现,由子类选择是否要覆盖。

我们来实现一下上文中的工资计算方法:
首先实现Employee基类:

//salary.swift
class Employee {
    var overtimeHours:Int = 0
    let overtimePayUnit:Double = 40

    func basePay() ->Double {
        println("calculate base pay")
        return 2000.0
    }

    func overtimePay()->Double{
        println("calculate overtime pay")
        return Double(overtimeHours) * overtimePayUnit
    }

    func levelPay()->Double{
        return 0.0
    }

    func productPay()->Double{
        return 0.0
    }

    func calcSalary()->Double{
        println("calculate salary:")
        return basePay() + overtimePay() + levelPay() + productPay()
    }
}

然后实现Worker类,覆盖productPay()方法计算计件工资。

class Worker: Employee {
    var productCount:Int = 0
    let payPerProduct:Double = 50.0
    override func productPay() -> Double {
        println("calculate product pay")
        return Double(productCount) * payPerProduct
    }

}

实现Engineer类,覆盖levelPay()方法计算职级工资

class Engineer: Employee {
    var level:Int = 0
    let payPerLevel:Double = 2000.0
    override func levelPay() -> Double {
        println("calculate level pay")
        return Double(level) * payPerLevel
    }
}

运行一下看看:

//main.swift

var workerA = Worker()
workerA.overtimeHours = 15
workerA.productCount = 60

println("salary of workerA: \(workerA.calcSalary())")

var engineerB = Engineer()
engineerB.overtimeHours = 10
engineerB.level = 3
println("salary of engineerB: \(engineerB.calcSalary())")

结果如下:

calculate salary:
calculate base pay
calculate overtime pay
calculate product pay
salary of workerA: 5600.0
calculate salary:
calculate base pay
calculate overtime pay
calculate level pay
salary of engineerB: 8400.0

还是工程师工资高。好好读书是王道啊。

使用钩子控制模板方法的执行流程

有时候不仅仅需要在子类中定制模板方法中的具体步骤,还需要控制执行流程,这时可以使用钩子。

钩子是一个在父类中有默认实现的方法,子类可以选择是否要覆盖。

最简单的应用是实现一个返回布尔值的钩子,作为模板方法中if语句的判断条件,这样就可以通过覆盖钩子返回true或false来控制模板方法中代码的执行路径了。

使用钩子修改一下上文薪资计算的类:

class Employee {
    var overtimeHours:Int = 0
    let overtimePayUnit:Double = 40

    func basePay() ->Double {
        println("calculate base pay")
        return 2000.0
    }

    func overtimePay()->Double{
        println("calculate overtime pay")
        return Double(overtimeHours) * overtimePayUnit
    }

    func needCalcLevelPay()->Bool{
        return false
    }

    func levelPay()->Double{
        println("zero level pay")
        return 0.0
    }

    func needCalcProductPay()->Bool{
        return false
    }

    func productPay()->Double{
        println("zero product pay")
        return 0.0
    }

    func calcSalary()->Double{
        println("calculate salary:")
        var salary = basePay() + overtimePay()
        if needCalcLevelPay() {
            salary += levelPay()
        }
        if needCalcProductPay() {
            salary += productPay()
        }
        return salary
    }
}

class Worker: Employee {
    var productCount:Int = 0
    let payPerProduct:Double = 50.0

    override func needCalcProductPay() -> Bool {
        return true
    }

    override func productPay() -> Double {
        println("calculate product pay")
        return Double(productCount) * payPerProduct
    }

}

class Engineer: Employee {
    var level:Int = 0
    let payPerLevel:Double = 2000.0

    override func needCalcLevelPay() -> Bool {
        return true
    }

    override func levelPay() -> Double {
        println("calculate level pay")
        return Double(level) * payPerLevel
    }
}

执行结果:

calculate salary:
calculate base pay
calculate overtime pay
calculate product pay
salary of workerA: 5600.0
calculate salary:
calculate base pay
calculate overtime pay
calculate level pay
salary of engineerB: 8400.0

和之前一样。

其实这个例子里并不需要钩子,不需要计算某一项薪资,直接返回0即可。但这是因为模板方法中调用的几个方法是完全独立的。如果有的方法有副作用,比如涉及IO、需要用户输入数据,就需要钩子来控制了。

另外这里只是简单地控制一个子步骤是否执行,有时候需要在多个子步骤中选择一个来执行,这时钩子就必不可少了。用代码来描述大概是这样:

class SuperClass {
    func action1(){
        println("action 1")
    }

    func action2(){
        println("action 2")
    }

    func whichAction() -> Int {
        return 0
    }
    func templateMethod() {
        switch whichAction(){
        case 1:
            action1()
        case 2:
            action2()
        default:
            break
        }
    }
}

子类可以通过覆盖whichAction()方法来控制执行action1()还是action2()

(未完待续)