搞懂策略模式和模板方法模式

最近听到身边同学在讨论什么是策略模式?我突然想到:就是和模板方法模式很像的那个。那模板方法模式又是什么呢?除此以外,它们两个到底怎么像了,又有什么区别呢?所以想用这篇文章来讨论下,怎样去区分模板方法模式和策略模式。

首先在设计模式中,策略模式和模板方法模式都是行为设计模式,它们旨在提高代码的可维护性、灵活性和复用性。为了便于理解我们先给出代码示例。

场景示例

先说一下两种设计模式的大致情况:

模板方法模式,算法的骨架是固定的,但某些步骤的具体实现可以在子类中进行扩展。算法的执行是由父类的模板方法触发的,子类可以通过扩展来影响某些步骤的具体实现。父类中的抽象方法由子类实现,子类执行的结果会影响父类的结果,这导致一种反向的控制结构。

策略模式,策略可以相对独立地变化,客户端可以灵活地选择和切换不同的策略。客户端通常主动选择并设置具体的策略对象。策略模式提供了管理相关的算法族的办法,并使得算法可独立于使用它的客户变化。

模板方法模式

接口开发是后端程序员日常最主要的工作之一,不管是Web接口开发还是Rpc接口开发,似乎都逃不过需求分析、编码和测试这些阶段,我们不妨就把这些作为我们开发任何系统的模板,于是就有了开发系统这个流程的抽象:

type Coding interface {
    Meeting()  // 讨论需求
    GenerateCode() // 编码
    Test() // 测试
}

然后进行实现,首先是开发Rpc接口:

type RpcApi struct {
}

func (r *RpcApi) Meeting() {
    fmt.Println("meeting")
}

func (r *RpcApi) GenerateCode() {
    fmt.Println("coding java")
}

func (r *RpcApi) Test() {
    fmt.Println("testing rpc api")
}

开发web接口:

type WebApi struct {
}

func (w *WebApi) Meeting() {
    fmt.Println("meeting")
}

func (w *WebApi) GenerateCode() {
    fmt.Println("coding go")
}

func (w *WebApi) Test() {
    fmt.Println("testing http api")
}

测试:

func main() {
    api := &RpcApi{}
    api.Meeting()
    api.GenerateCode()
    api.Test()
}

策略模式

策略模式顾名思义,可以执行不同的策略,比如我们去超市购物,到了收银台,可以选择两种方式进行支付,分别是现金和银行卡,于是我们就可以把支付方式和要支付的钱抽象出来,于是就有了下面的PaymentStrategy接口和PaymentContext作为执行对象。

package main

type PaymentStrategy interface {
    Pay(amount float64) error
}

type PaymentContext struct {
    amount   float64
    strategy PaymentStrategy
}

func NewPaymentContext(amount float64, strategy PaymentStrategy) *PaymentContext {
    return &PaymentContext{
        amount:   amount,
        strategy: strategy,
    }
}

func (p *PaymentContext) Pay() error {
    return p.strategy.Pay(p.amount)
}

然后实现接口,模拟银行卡支付

package main

import "fmt"

type BankPay struct {
    name       string
    cardNumber string
}

func (c *BankPay) Pay(amount float64) error {
    fmt.Printf("Paying %0.2f using BankPay card:%s, name:%s\n", amount, c.cardNumber, c.name)
    return nil
}

实现接口,模拟现金支付

package main

import "fmt"

type CashPay struct {
    name string
}

func (c *CashPay) Pay(amount float64) error {
    fmt.Printf("Paying %0.2f using CashPay name:%s\n", amount, c.name)
    return nil
}

最后我们测试一下

package main

import "fmt"

func main() {
    cash := &CashPay{
        name: "John ",
    }

    context1 := NewPaymentContext(100.0, cash)
    if err := context1.Pay(); err != nil {
        fmt.Println("Payment failed:", err)
    }
}

两种设计模式的异同

先了解两个设计模式的结构与组成:

模板方法模式,主要包含抽象类(AbstractClass)、模板方法(Template Method)和具体实现步骤的方法。

1)抽象类负责给出一个算法的轮廓和骨架,模板方法定义了一套算法的骨架,按某种顺序调用其包含的基本方法。

2)基本方法包括抽象方法(在抽象类中声明,由具体子类实现)、具体方法(在抽象类中已经实现,在具体子类中可以继承或重写它)和钩子方法(在抽象类中已经实现,包括用于判断的逻辑方法和需要子类重写的空方法两种)。

策略模式,主要包含环境类(Context)、策略接口(Strategy)和具体策略实现类(ConcreteStrategy)。

1)策略接口定义了一个算法的家族,具体策略实现类则包装了相关的算法和行为。

2)环境类持有一个策略类的引用,最终给客户端调用。

相同点

1)都旨在封装算法或行为: 模板方法模式 将算法的骨架与具体实现分离, 策略模式 将一系列相关的算法封装成一个个策略类。

2)都遵循开闭原则:通过继承或组合的方式,可以在不修改原有代码的情况下扩展新的算法实现,在一定程度上提高了代码的可维护性和复用性。

不同点

1)目的不同: 策略模式 目的是选择算法或行为,它定义了一组算法,将每个算法封装起来,并使它们可以互换。 模板方法模式 目的是定义一个算法的整体结构,它定义一个算法的骨架,允许子类在不改变算法结构的情况下重新定义算法的某些特定步骤。

2)实现方式不同: 策略模式 是通过不同的策略类实现的,它们实现相同的接口,可以在运行时自由切换。 模板方法模式 的结构在基类中定义,子类实现某些部分,在编译时就确定了算法的结构,子类可以扩展。

两者各自的适用场景

策略模式和模板方法模式都是行为设计模式,它们在不同的场景下有着各自的优势和适用性。

策略模式的使用场景

1) 算法选择:当有多种算法可以实现同一个功能,并且这些算法在未来可能会发生变化时,可以使用策略模式。这样可以将算法的实现封装起来,使客户端可以独立于其使用的算法而变化。

2) 行为选择:除了算法,策略模式还可以用于封装一系列相关的行为,使这些行为可以互换。例如,一个电商系统可能有多种支付方式(如信用卡支付、支付宝支付等),这些支付方式可以被实现为不同的策略类。

3) 条件逻辑简化:如果应用程序中存在大量的条件分支语句,这些分支语句根据不同的条件执行不同的操作,那么可以使用策略模式将这些操作封装成独立的策略类,从而简化条件逻辑。

4) 策略扩展:当需要添加新的算法或行为时,只需要创建新的策略类,而不需要修改现有的代码。这符合开闭原则,即对扩展开放,对修改关闭。

模板方法模式的使用场景

1) 算法框架:当有一个算法的整体框架,但其中的某些步骤的具体实现可能会因情况而异时,可以使用模板方法模式。这样可以将算法的整体框架封装在基类(或抽象类)中,而将具体步骤的实现留给子类。

2) 代码复用:如果多个类有相似的算法或行为,并且这些算法或行为中有一些是公共的,而另一些是各不相同的,那么可以使用模板方法模式将这些公共部分提取到基类中,从而实现代码复用。

3) 控制子类行为:有时需要确保子类按照某种特定的方式来实现某些方法,而不想在子类中直接实现这些方法。这时可以在基类中定义模板方法,并在模板方法中调用这些需要子类实现的方法。这样,子类就必须按照基类定义的方式来实现这些方法。

4) 钩子方法:模板方法模式还允许在算法框架中设置钩子方法(hook method),这些钩子方法在基类中可以有默认实现,也可以没有实现,留给子类来覆盖。钩子方法提供了一种灵活的机制,允许子类在不影响算法整体框架的情况下,对算法的某些步骤进行定制。

综上所述,策略模式和模板方法模式在不同的场景下有着各自的优势和适用性。选择哪种模式取决于具体的需求和上下文环境。

原文阅读