• GO 常用设计模式


     

    设计模式简介


    什么是设计模式

    设计模式(design pattern):是对软件设计普遍存在、反复出现的问题所提出的解决方案,这里的问题就是我们应该怎么去写/设计我们的代码,让我们的代码可读性、可扩展性、可重用性、可靠性更好,通过合理的代码设计让我们的程序拥有“高内聚,低耦合”的特性,这就是设计模式要解决的问题。

    本质是为了提高软件的可维护性、可扩展性、通用性,并降低软件的复杂度。

    设计模式分类

    1. 创建型模式:提供创建对象的机制,增加已有代码的灵活性和可复用性。

    2. 结构型模式:介绍如何将对象和类组装成较大的结构,并同时保持结构的灵活和高效

    3. 行为型模式:负责对象间的高效沟通和职责委派

    我们为什么要学

    设计模式是前人摸索出来的一种代码设计经验,学习它就像站在巨人的肩膀上,参悟其中的设计理念,从而在实践中写出高质量代码。因此不论是编程新手还是老鸟,都应该去学习设计模式。

    软件设计原则

    1. 代码复用

    2. 扩展性

    设计原则

    封装变化的内容

    面向接口开发,而不是面向实现

    原则:面向接口进行开发,而不是面向实现;依赖于抽象类型,而不是具体类型。

    组合优于继承

    SOLID原则

    一. 单一职责原则(Single Responsibility Principle)

    二. 开闭原则(Open/Closed Principle)

    三. 里氏替换原则(Liskov Substitution Principle)

    四. 接口隔离原则(Interface Segregation Principle)

    五. 依赖倒置原则(Dependency Inversion Principle)

    创建模式

    简单工厂

    简单工厂就是我们首先声明一个类,这个类叫做工厂类,在这个内我们可以声明一个(静态)方法,这个方法会根据参数的值生成相应的对象(我们把这个对象叫“产品”)。

    简单工厂的好处是:

    1. 使用者可以直接获得一个构造好的对象(根据我们在工厂类内方法的传参值),而不需要关心这个对象的构造的过程。
    2. 当我们新增一个对象(产品)时,我们只需要修改这个工厂类内的方法就行了,这样降低了所需对象(产品)与我们使用对象的代码逻辑的耦合,符合开闭原则。

    代码

    1. package main
    2. import "fmt"
    3. // 对象接口
    4. type BMW interface {
    5. run()
    6. }
    7. // 构建对象1
    8. type BMW730 struct {
    9. }
    10. func (b BMW730) run() {
    11. fmt.Println("BMW730 is running...")
    12. }
    13. // 构建对象2
    14. type BMW840 struct {
    15. }
    16. func (b BMW840) run() {
    17. fmt.Println("BMW840 is running...")
    18. }
    19. // 工厂对象
    20. type Factory struct {
    21. }
    22. func (f Factory)produceBMW(BMW_TYPE string) BMW {
    23. switch BMW_TYPE {
    24. case "BMW730":
    25. return BMW730{}
    26. case "BMW840":
    27. return BMW840{}
    28. default:
    29. return nil
    30. }
    31. }
    32. func main() {
    33. // 生成工厂对象
    34. factory := new(Factory)
    35. // 使用工厂对象生成产品对象
    36. p1 := factory.produceBMW("BMW730")
    37. p1.run()
    38. p2 := factory.produceBMW("BMW840")
    39. p2.run()
    40. }

    工厂方法

    工厂方法模式(Factory Methord Pattern)中,工厂父类(在go中为interface)负责定义创建产品对象的公共接口,子工厂类要实现父工厂中定义的接口,每一个工厂子类则负责生成具体种类的产品对象,这样做的目的是将产品类的实例化操作延迟到工厂子类中完成。

    工厂方法的好处:

    在工厂方法中,客户端不需要知道产品的类名,只需要知道所对应的子工厂类即可,具体的产品对象由具体的子工厂类创建,客户端只需要知道创建具体产品的子工厂类对象就行。

    当要有新种类的产品对象出现时,我们只需要要新增生产这个产品的子工厂类(这个类去实现父工厂类中定义的接口),就可以通过这个子工厂类就能生产这个新产品对象了,此时我们没有对代码进行修改,只是对代码进行了扩展(新增了一个子工厂类),因此符合开闭原则。

    代码

    1. package main
    2. import "fmt"
    3. // 对象接口
    4. type BMW interface {
    5. run()
    6. }
    7. // 构建对象1
    8. type BMW730 struct {
    9. }
    10. func (b BMW730) run() {
    11. fmt.Println("BMW730 is running...")
    12. }
    13. // 构建对象2
    14. type BMW840 struct {
    15. }
    16. func (b BMW840) run() {
    17. fmt.Println("BMW840 is running...")
    18. }
    19. // 抽象父类工厂
    20. type Factory interface {
    21. produceBMW() BMW
    22. }
    23. // 子类实现父抽象类接口,生成特定种类的产品BMW730
    24. type BMW730Factory struct {
    25. }
    26. func (b BMW730Factory) produceBMW() BMW {
    27. return BMW730{}
    28. }
    29. // 子类实现父抽象类接口,生成特定种类的产品BMW840
    30. type BMW840Factory struct {
    31. }
    32. func (b BMW840Factory) produceBMW() BMW {
    33. return BMW840{}
    34. }
    35. // 通过向参数中传不同的子类,生成不同的产品(某种车)
    36. func get_bmw(bmw Factory) {
    37. car := bmw.produceBMW()
    38. car.run()
    39. }
    40. func main() {
    41. // 生成不同子工厂对象
    42. factory_730 := new(BMW730Factory)
    43. factory_840 := new(BMW840Factory)
    44. // 通过不同子工厂对象,生产不同种类的车
    45. get_bmw(factory_730)
    46. get_bmw(factory_840)
    47. }

    抽象工厂

    抽象工厂(Abstract Factory Pattern)通过提供了一个抽象工厂类,这个抽象工厂类定义了多个接口,每个接口都可以生产一种产品。子类工厂在实现抽象工厂的接口后,可以通过不同的接口生产出不同种类的对象。

    代码

    1. package main
    2. import "fmt"
    3. // 构建对象1
    4. type BMW730 struct {
    5. }
    6. func (b BMW730) run() {
    7. fmt.Println("BMW730 is running...")
    8. }
    9. // 构建对象2
    10. type BMW840 struct {
    11. }
    12. func (b BMW840) run() {
    13. fmt.Println("BMW840 is running...")
    14. }
    15. // 抽象工厂
    16. type BMWFactory interface {
    17. produce730BMW() BMW730
    18. produce840BMW() BMW840
    19. }
    20. // 具体工厂子类
    21. type ConcreteBMWFactory struct {
    22. }
    23. func (c ConcreteBMWFactory) produce730BMW() BMW730 {
    24. return BMW730{}
    25. }
    26. func (c ConcreteBMWFactory) produce840BMW() BMW840 {
    27. return BMW840{}
    28. }
    29. func main() {
    30. // 实例化子类工厂,这个工厂可以根据不同的函数生成不同种类的车产品
    31. concreteBMWFactory := ConcreteBMWFactory{}
    32. bmw730 := concreteBMWFactory.produce730BMW()
    33. bmw840 := concreteBMWFactory.produce840BMW()
    34. bmw730.run()
    35. bmw840.run()
    36. }

    单例模式

    单例模式(Singleton Pattern)是一种简单的创建模式,有些对象我们往往只需要全局一个,比如全局缓存、数据库连接等。

    使用场景

    需要频繁实例化然后销毁的对象

    创建对象耗时过多或资源过多,但有经常用到

    系统只需要一个实例对象,如果系统要求提供一个唯一的序列号生成器或资源管理器,或者对象消耗资源太大而只允许创建一个对象。

    代码

    1. package main
    2. import (
    3. "fmt"
    4. "sync"
    5. )
    6. // 数据库连接对象实例
    7. type DBInstance struct {
    8. }
    9. var (
    10. once sync.Once
    11. dbInstance *DBInstance
    12. )
    13. // 注意初始对象实例过程也可以放到init函数中进行
    14. func NewInstance() *DBInstance {
    15. // 只有第一次才会实例化数据库连接对象
    16. if dbInstance != nil {
    17. once.Do(func() {
    18. dbInstance = &DBInstance{}
    19. })
    20. }
    21. return dbInstance
    22. }
    23. func main() {
    24. for i := 0; i <= 10; i++ {
    25. // 在使用的时候获取数据库实例对象
    26. db := NewInstance()
    27. fmt.Println(db)
    28. }
    29. }

    建造者模式

    建造者模式(Build Pattern)又叫生成器,将一个复杂对象分解成多个相对简单的部分,之后按步骤创建这个复杂对象的每一个部分。该模式允许你使用相同的创建代码生成不同的对象。

    代码

    1. package main
    2. import "fmt"
    3. type Car struct {
    4. engine string
    5. chassis string
    6. body string
    7. }
    8. // 生成器
    9. type CarBuilder struct {
    10. engine string
    11. chassis string
    12. body string
    13. }
    14. func (c *CarBuilder) addChassis(chassis string) {
    15. c.chassis = chassis
    16. }
    17. func (c *CarBuilder) addEngine(engine string) {
    18. c.engine = engine
    19. }
    20. func (c *CarBuilder) addBody(body string) {
    21. c.body = body
    22. }
    23. func (c *CarBuilder) build() Car {
    24. return Car{
    25. engine: c.engine,
    26. chassis: c.chassis,
    27. body: c.body,
    28. }
    29. }
    30. func main() {
    31. carBuilder := &CarBuilder{}
    32. // 按步骤创建对象
    33. carBuilder.addEngine("v12")
    34. carBuilder.addChassis("复合材料")
    35. carBuilder.addBody("镁合金")
    36. // 构建Car对象
    37. car := carBuilder.build()
    38. fmt.Println(car)
    39. }

    原型模式

    如果你希望生成一个对象,这个对象与另一个对象完全相同,该如何实现呢?

    如果遍历对象的所有成员,将其依次复制到新对象中,会稍显麻烦。

    原型模式(Prototype Pattern)将这个克隆新对象过程委派给了被克隆对象(其实就是将这个被克隆对象的类中增加一个克隆函数),被克隆对象叫做原型。

    注意:在写克隆函数时,要注意深浅拷贝问题。

    1. package main
    2. import "fmt"
    3. type obj struct {
    4. name *string
    5. }
    6. func (o *obj) Clone() *obj {
    7. new_obj := *o
    8. return &new_obj
    9. }
    10. func main() {
    11. name := "qiliang"
    12. o1 := &obj{name: &name}
    13. o2 := o1.Clone()
    14. // 注意会有浅拷贝问题
    15. *o2.name = "xiaolin"
    16. fmt.Println(*o1.name)
    17. fmt.Println(*o2.name)
    18. }

    结构型模式


    适配器模式

    适配器模式(Adaptor Pattern)说白了就是兼容,假设一开始我们提供了A对象,后期随着业务迭代,有需要在A对象的基础上衍生出不同的需求。如果有很多函数已经在线上调用了A对象,此时再对A对象修改就比较麻烦,也可能会出现问题。甚至更糟糕的情况,你坑你没有程序库的源代码,从而无法进行修改。

    此时就可以用一个适配器,它就像一个接口转换器,调用方只需要调用这个适配器,而不需要去关心背后的实现,由适配器接口封装复杂的逻辑转换过程。

    1. package main
    2. import "fmt"
    3. type CM2M struct {
    4. }
    5. func (c CM2M) CMtoM(cm float64) (m float64) {
    6. m = cm / 100
    7. return
    8. }
    9. type M2CM struct {
    10. }
    11. func (c M2CM) MtoCM(m float64) (cm float64) {
    12. cm = m * 100;
    13. return cm
    14. }
    15. // 适配器函数,自动进行cm与m之间的转换
    16. // 有了这个适配器逻辑后我们,不需要再关注转换的逻辑
    17. type AdapterTransLength interface {
    18. transLength(string, float64) float64
    19. }
    20. type TransLengthAdapter struct {
    21. }
    22. func (t TransLengthAdapter)transLength(whickType string, length float64) float64 {
    23. if whickType == "m" {
    24. return M2CM{}.MtoCM(length)
    25. }
    26. return CM2M{}.CMtoM(length)
    27. }
    28. func main() {
    29. transAdapter := TransLengthAdapter{}
    30. ans := transAdapter.transLength("m", 10)
    31. fmt.Println(ans)
    32. }

    桥接模式

    假设一开始业务需要两种发送消息渠道:sms和email,我们可以分别实现sms和email接口。

    之后随着业务迭代,又产生了新的需求,需要提供两种系统发送的方式,systemA和systemB并且这两种系统发送方式都应该支持sms和email渠道

    此时至少需要提供4中方法:systemA to sms、systemA to email、systemB to sms、systemB to email

    如果在增加新的需求维度,那么类的数量会出现质数增长,这是我们不能接受的

    解决方案

    其实我们之前是在用继承的想法来看问题,桥接模式则希望将继承关系转变为关联关系,使两个类独立存在,且一个类通过实现接口聚合在另一个类中。

    详细说一下:

    1. 桥接模式需要将抽象和实现区分开;
    2. 桥接模式需要将“渠道”和“学习通发送方式”这两种类别区分开;
    3. 最后再“系统发送方式”的雷利调用“渠道”的抽象接口,使他们从继承关系转变为关联关系。

    用一句话总结桥接模式的理念就是:“将抽象与实现接口,将不同的类别的继承关系改为关联关系

    代码

    1. package main
    2. import "fmt"
    3. // 声明消息接口
    4. type SendMessage interface {
    5. send(text, to string)
    6. }
    7. // 声明第一种消息
    8. type sms struct {
    9. }
    10. func (s sms) send(text, to string) {
    11. fmt.Println(fmt.Sprintf("send %s to %s sms", text, to))
    12. }
    13. func NewSms() *sms {
    14. return &sms{}
    15. }
    16. // 声明第二种消息
    17. type email struct {
    18. }
    19. func (e email) send(text, to string) {
    20. fmt.Println(fmt.Sprintf("send %s to %s email", text, to))
    21. }
    22. func NewEmail() *email {
    23. return &email{}
    24. }
    25. // 声明两种发送渠道,这两种发送渠道都要支持sms和email消息(通过组合SendMessage接口来实现
    26. // 本质是因为在组合SendMessage接口后,sms、emali都可以放到systemA中的method字段,当method字段值不同时就可以发送不同的消息)
    27. type systemA struct {
    28. method SendMessage
    29. }
    30. func NewSystemA(method SendMessage) *systemA {
    31. return &systemA{
    32. method: method,
    33. }
    34. }
    35. func (s *systemA) SystemASendMes(text, to string) {
    36. s.method.send("SystemA "+text, to)
    37. }
    38. type systemB struct {
    39. method SendMessage
    40. }
    41. func NewSystemB(method SendMessage) *systemB {
    42. return &systemB{
    43. method: method,
    44. }
    45. }
    46. func (b *systemB) SystemBSendMes(text, to string) {
    47. b.method.send("SystemB " + text, to)
    48. }
    49. func main() {
    50. // 声明要发送的类型
    51. sms := NewSms()
    52. email := NewEmail()
    53. // 在SystemA中发送两种消息
    54. systemA1 := NewSystemA(sms)
    55. systemA2 := NewSystemA(email)
    56. systemA1.SystemASendMes("ni hao !", "qiliang")
    57. systemA2.SystemASendMes("ni hao !", "qiliang")
    58. systemB1 := NewSystemB(sms)
    59. systemB2 := NewSystemB(email)
    60. systemB1.SystemBSendMes("gan xie !", "xiaoming")
    61. systemB2.SystemBSendMes("gan xie !", "xiaoming")
    62. }

    装饰器模式

    有时候我们对一个类进行封装,形成一个新类,这个新类在原类的基础上拥有额外的功能。

    例如一个披萨类,你可以在对披萨类进行封装,形成新的类如番茄披萨类和芝士披萨类。此时这个封装就是装饰器模式的核心思想。

    简单来说,装饰器模式就是将对象封装到形成一个新对象,这个信息对象功能更丰富(可以理解为:为源对象绑定了新的行为功能)

    如果你希望在无需修改对象的情况下使用对象,并且希望对象新增额外的行为,就可以考虑使用装饰器模式。

    代码

    1. package main
    2. import "fmt"
    3. type pizza interface {
    4. getPrice() int
    5. }
    6. type basePizza struct {
    7. }
    8. func (p basePizza) getPrice() int {
    9. return 15
    10. }
    11. type tomatoPizza struct {
    12. pizza pizza
    13. }
    14. func (p tomatoPizza) getPrice() int {
    15. return p.pizza.getPrice() + 10
    16. }
    17. type cheesePizza struct {
    18. pizza pizza
    19. }
    20. func (p cheesePizza) getPrice() int {
    21. return p.pizza.getPrice() + 20
    22. }
    23. func main() {
    24. tomPizza := tomatoPizza{}
    25. tomPizza.pizza = basePizza{}
    26. price := tomPizza.getPrice()
    27. fmt.Println(price)
    28. }

    代理模式

    如果你需要在访问一个对象时,有一个像“代理”一样的角色,它可以在访问对象之前为你进行缓存检查、权限判断等访问控制,在访问对象之后为你进行结果缓存、日志记录等处理,那么可以考试率使用代码模式(Proxy Pattern)。

    会议一下一些web框架的router模块,当客户端访问一个接口时,在最终执行对应的接口之前,router模块会执行一些事前操作,进行权限判断操作,在执行之后会记录日志,这就是典型的代理模式。

    代理模式需要一个代理类,其包含执行真正对象所需要的成员变量,并由代理类管理整个声明周期。

    代码

    1. package main
    2. import "fmt"
    3. type Subject interface {
    4. ProxyFun() string
    5. }
    6. // 声明代理类
    7. type Proxy struct {
    8. real RealSubject
    9. }
    10. func (p Proxy) ProxyFun() string {
    11. var ans string
    12. // 在低啊用真实对象之前,检查缓存、判断权限等
    13. p.real.PreFun()
    14. p.real.RealFun()
    15. p.real.AfterFun()
    16. // 在调用完操作之后,可以缓存结果、对结果进行处理等(如脱敏)、记录日志等
    17. return ans
    18. }
    19. type RealSubject struct {
    20. }
    21. func (s RealSubject) RealFun() {
    22. fmt.Println("real...")
    23. }
    24. func (s RealSubject) PreFun() {
    25. fmt.Println("Pre...")
    26. }
    27. func (s RealSubject) AfterFun() {
    28. fmt.Println("After...")
    29. }
    30. func main() {
    31. rs := RealSubject{}
    32. proxy := Proxy{real: rs}
    33. proxy.ProxyFun()
    34. }

    行为行模式

    观察者模式

    如果你需要在对一个对象的状态被改变时,其他对象能作为“观察者”被通知,就可以使用观察者模式。

    我们将自身状态改变就回通知其他对象的对象称为“发布者”,关注发布者状态变化的对象称为“订阅者”。

    代码

    1. package main
    2. import "fmt"
    3. // 发布者-主题
    4. type Subject struct {
    5. observers []Observer
    6. content string
    7. }
    8. func NewSubject() *Subject {
    9. return &Subject{
    10. observers: make([]Observer, 0),
    11. }
    12. }
    13. // 添加订阅者
    14. func (s *Subject) AddObserver(o Observer) {
    15. s.observers = append(s.observers, o)
    16. }
    17. // 通知消费者
    18. func (s *Subject) Notify() {
    19. for _, o := range s.observers {
    20. o.SendMessage(s)
    21. }
    22. }
    23. // 发布消息
    24. func (s *Subject) UpdateContent(content string) {
    25. s.content = content
    26. s.Notify()
    27. }
    28. // 观察者-订阅者接口
    29. type Observer interface {
    30. SendMessage(*Subject)
    31. }
    32. // 订阅者
    33. type Reader struct {
    34. name string
    35. }
    36. func NewReader(name string) *Reader {
    37. return &Reader{
    38. name: name,
    39. }
    40. }
    41. func (r Reader) SendMessage(s *Subject) {
    42. fmt.Println(r.name + " " + s.content)
    43. }
    44. func main() {
    45. subject := NewSubject()
    46. reader1 := NewReader("qiliang")
    47. reader2 := NewReader("xiaolin")
    48. subject.AddObserver(reader1)
    49. subject.AddObserver(reader2)
    50. subject.UpdateContent("ni hao !")
    51. }

    策略模式

    假设需要实现一个根据出行方式规划路线导航功能,出行的方式可以选择不行、骑行、开车,最简单的方式就是分别实现这个3种方法,供客户端调用。但是这样做会使客户端选择路线的代码和出行方式耦合了起来,如果新增一种出行的方式,就要修改客户端路线选择方案代码,这不符合开闭原则。

    解决办法是使用策略模式(Strategy Pattern),它会将每种出行方案都抽取到一组新的类中,组中的类都会实现一个出行Interface,这个出行Interface约定了出行接口函数。客户端只需要指定指定所需要的出行方案即可。

    代码

    1. package main
    2. import "fmt"
    3. // 策略维护对象,包含了路线信息name 和 所用的的策略strategy
    4. type Travel struct {
    5. name string
    6. strategy Strategy
    7. }
    8. func NewTravel(name string, strategy Strategy) *Travel {
    9. return &Travel{
    10. name: name,
    11. strategy: strategy,
    12. }
    13. }
    14. // 执行路线策略
    15. func (p *Travel) getTraffic() {
    16. p.strategy.traffic(p)
    17. }
    18. // 路线策略接口
    19. type Strategy interface {
    20. traffic(*Travel)
    21. }
    22. // 实现走路方式
    23. type walk struct {
    24. }
    25. func (w *walk) traffic(t *Travel) {
    26. fmt.Println(t.name + " walk...")
    27. }
    28. // 实现开车方式
    29. type drive struct {
    30. }
    31. func (w *drive) traffic(t *Travel) {
    32. fmt.Println(t.name + " drive...")
    33. }
    34. func main() {
    35. // 生成走路路线策略
    36. travel1 := NewTravel("qiliang", &walk{})
    37. // 生成开车路线策略
    38. travel2 := NewTravel("xiaolin", &drive{})
    39. // 运行策略
    40. travel1.getTraffic()
    41. travel2.getTraffic()
    42. }
    43. // OutPut
    44. qiliang walk...
    45. xiaolin drive...

    模板方法模式 

    模板方法建议将过程/算法分解为一系列步骤(就比如:盖房子这个过程,对盖普通的房子或者盖别墅,它的大致步骤是相同,我们可以简单考虑的将盖房子的过程分为:步骤1 打地基、步骤2 建围墙、步骤3 盖房顶 这个三个基类中的方法)

    然后将这些步骤改写成基类中的方法, 当我子类去继承基类时,子类也拥有了这些方法,并且在子类中我们可以重写基类中的某些方法,让子类更适配某个具体的流程(比如这个子类表示的是 “盖别墅类”, 那么我们可以重写“盖别墅类中”的“步骤1 打地基”方法,因为盖别墅打的地基,可能和基类中默认实现的打地基的方式不太一样,通过重写这个基类中的“步骤1 打地基”方法)。

    如果以后新增加 “盖小区的”这个过程,我们仍然可以使用 基类中的定义的模板方法(就是盖房子的三个步骤),并对某些方法进行重写,因为我们只重写了部分方法,其它的步骤是用基类的默认方法,这样就减少了我们的代码量工作。

    举例:

    让我们来考虑一个一次性密码功能 (OTP) 的例子。 将 OTP 传递给用户的方式多种多样 (短信、 邮件等)。 但无论是短信还是邮件, 整个 OTP 流程都是相同的:

    1. 生成随机的 n 位数字。
    2. 在缓存中保存这组数字以便进行后续验证。
    3. 准备内容。
    4. 发送通知。

    后续引入的任何新 OTP 类型都很有可能需要进行相同的上述步骤。

    因此, 我们会有这样的一个场景, 其中某个特定操作的步骤是相同的, 但实现方式却可能有所不同。 这正是适合考虑使用模板方法模式的情况。

    首先, 我们定义一个由固定数量的方法组成的基础模板算法。 这就是我们的模板方法。 然后我们将实现每一个步骤方法, 但不会改变模板方法。

    代码

    1. package main
    2. import "fmt"
    3. // 定义OTP步骤接口
    4. type IOtp interface {
    5. genRandomOTP(int) string
    6. saveOTPCache(string)
    7. getMessage(string) string
    8. sendNotification(string) error
    9. }
    10. // 对IOtp封装一下
    11. type Otp struct {
    12. iOtp IOtp
    13. }
    14. // 相同OTP执行步骤调用
    15. func (o *Otp) genAndSendOTP(otpLength int) error {
    16. otp := o.iOtp.genRandomOTP(otpLength)
    17. o.iOtp.saveOTPCache(otp)
    18. message := o.iOtp.getMessage(otp)
    19. err := o.iOtp.sendNotification(message)
    20. if err != nil {
    21. return err
    22. }
    23. return nil
    24. }
    25. // 定义Sms实现IOtp接口
    26. type Sms struct {
    27. }
    28. func (s *Sms) genRandomOTP(len int) string {
    29. randomOTP := "1234"
    30. fmt.Printf("SMS: generating random otp %s\n", randomOTP)
    31. return randomOTP
    32. }
    33. func (s *Sms) saveOTPCache(otp string) {
    34. fmt.Printf("SMS: saving otp: %s to cache\n", otp)
    35. }
    36. func (s *Sms) getMessage(otp string) string {
    37. return "SMS OTP for login is " + otp
    38. }
    39. func (s *Sms) sendNotification(message string) error {
    40. fmt.Printf("SMS: sending sms: %s\n", message)
    41. return nil
    42. }
    43. // 定义Email实现IOtp接口
    44. type Email struct {
    45. }
    46. func (s *Email) genRandomOTP(len int) string {
    47. randomOTP := "1234"
    48. fmt.Printf("EMAIL: generating random otp %s\n", randomOTP)
    49. return randomOTP
    50. }
    51. func (s *Email) saveOTPCache(otp string) {
    52. fmt.Printf("EMAIL: saving otp: %s to cache\n", otp)
    53. }
    54. func (s *Email) getMessage(otp string) string {
    55. return "EMAIL OTP for login is " + otp
    56. }
    57. func (s *Email) sendNotification(message string) error {
    58. fmt.Printf("EMAIL: sending email: %s\n", message)
    59. return nil
    60. }
    61. func main() {
    62. smsOTP := &Sms{}
    63. o := Otp{
    64. iOtp: smsOTP, // 接口实现多态
    65. }
    66. o.genAndSendOTP(4)
    67. fmt.Println("")
    68. emailOTP := &Email{}
    69. o = Otp{
    70. iOtp: emailOTP, // 接口实现多态
    71. }
    72. o.genAndSendOTP(4)
    73. }

    文章参考:

    Go 常用设计模式 

    图解九种常见的设计模式 - SegmentFault 思否

    用Go语言实现23种设计模式 - 掘金

  • 相关阅读:
    零拷贝并非万能解决方案:重新定义数据传输的效率极限
    PHP笔记 28 29 30 31
    被 CSDN,伤透了心
    【手把手带你学JavaSE系列】String类(上篇)
    leetcode -658--找到 K 个最接近的元素
    【计组】总线系统
    FPGA纯verilog实现8路视频拼接显示,提供工程源码和技术支持
    汇编-EQU伪指令(数值替换)
    【Linux】进程地址空间
    【0】数学的魅力
  • 原文地址:https://blog.csdn.net/qq_34261446/article/details/126777112