设计模式(design pattern):是对软件设计中普遍存在、反复出现的问题所提出的解决方案,这里的问题就是我们应该怎么去写/设计我们的代码,让我们的代码可读性、可扩展性、可重用性、可靠性更好,通过合理的代码设计让我们的程序拥有“高内聚,低耦合”的特性,这就是设计模式要解决的问题。
本质是为了提高软件的可维护性、可扩展性、通用性,并降低软件的复杂度。
1. 创建型模式:提供创建对象的机制,增加已有代码的灵活性和可复用性。
2. 结构型模式:介绍如何将对象和类组装成较大的结构,并同时保持结构的灵活和高效
3. 行为型模式:负责对象间的高效沟通和职责委派
设计模式是前人摸索出来的一种代码设计经验,学习它就像站在巨人的肩膀上,参悟其中的设计理念,从而在实践中写出高质量代码。因此不论是编程新手还是老鸟,都应该去学习设计模式。
1. 代码复用
2. 扩展性
设计原则
原则:面向接口进行开发,而不是面向实现;依赖于抽象类型,而不是具体类型。
简单工厂就是我们首先声明一个类,这个类叫做工厂类,在这个内我们可以声明一个(静态)方法,这个方法会根据参数的值生成相应的对象(我们把这个对象叫“产品”)。
简单工厂的好处是:
代码
package main
import "fmt"
// 对象接口
type BMW interface {
run()
}
// 构建对象1
type BMW730 struct {
}
func (b BMW730) run() {
fmt.Println("BMW730 is running...")
}
// 构建对象2
type BMW840 struct {
}
func (b BMW840) run() {
fmt.Println("BMW840 is running...")
}
// 工厂对象
type Factory struct {
}
func (f Factory)produceBMW(BMW_TYPE string) BMW {
switch BMW_TYPE {
case "BMW730":
return BMW730{}
case "BMW840":
return BMW840{}
default:
return nil
}
}
func main() {
// 生成工厂对象
factory := new(Factory)
// 使用工厂对象生成产品对象
p1 := factory.produceBMW("BMW730")
p1.run()
p2 := factory.produceBMW("BMW840")
p2.run()
}
在工厂方法模式(Factory Methord Pattern)中,工厂父类(在go中为interface)负责定义创建产品对象的公共接口,子工厂类要实现父工厂中定义的接口,每一个工厂子类则负责生成具体种类的产品对象,这样做的目的是将产品类的实例化操作延迟到工厂子类中完成。
工厂方法的好处:
在工厂方法中,客户端不需要知道产品的类名,只需要知道所对应的子工厂类即可,具体的产品对象由具体的子工厂类创建,客户端只需要知道创建具体产品的子工厂类对象就行。
当要有新种类的产品对象出现时,我们只需要要新增生产这个产品的子工厂类(这个类去实现父工厂类中定义的接口),就可以通过这个子工厂类就能生产这个新产品对象了,此时我们没有对代码进行修改,只是对代码进行了扩展(新增了一个子工厂类),因此符合开闭原则。
代码
package main
import "fmt"
// 对象接口
type BMW interface {
run()
}
// 构建对象1
type BMW730 struct {
}
func (b BMW730) run() {
fmt.Println("BMW730 is running...")
}
// 构建对象2
type BMW840 struct {
}
func (b BMW840) run() {
fmt.Println("BMW840 is running...")
}
// 抽象父类工厂
type Factory interface {
produceBMW() BMW
}
// 子类实现父抽象类接口,生成特定种类的产品BMW730
type BMW730Factory struct {
}
func (b BMW730Factory) produceBMW() BMW {
return BMW730{}
}
// 子类实现父抽象类接口,生成特定种类的产品BMW840
type BMW840Factory struct {
}
func (b BMW840Factory) produceBMW() BMW {
return BMW840{}
}
// 通过向参数中传不同的子类,生成不同的产品(某种车)
func get_bmw(bmw Factory) {
car := bmw.produceBMW()
car.run()
}
func main() {
// 生成不同子工厂对象
factory_730 := new(BMW730Factory)
factory_840 := new(BMW840Factory)
// 通过不同子工厂对象,生产不同种类的车
get_bmw(factory_730)
get_bmw(factory_840)
}
抽象工厂(Abstract Factory Pattern)通过提供了一个抽象工厂类,这个抽象工厂类定义了多个接口,每个接口都可以生产一种产品。子类工厂在实现抽象工厂的接口后,可以通过不同的接口生产出不同种类的对象。
代码
package main
import "fmt"
// 构建对象1
type BMW730 struct {
}
func (b BMW730) run() {
fmt.Println("BMW730 is running...")
}
// 构建对象2
type BMW840 struct {
}
func (b BMW840) run() {
fmt.Println("BMW840 is running...")
}
// 抽象工厂
type BMWFactory interface {
produce730BMW() BMW730
produce840BMW() BMW840
}
// 具体工厂子类
type ConcreteBMWFactory struct {
}
func (c ConcreteBMWFactory) produce730BMW() BMW730 {
return BMW730{}
}
func (c ConcreteBMWFactory) produce840BMW() BMW840 {
return BMW840{}
}
func main() {
// 实例化子类工厂,这个工厂可以根据不同的函数生成不同种类的车产品
concreteBMWFactory := ConcreteBMWFactory{}
bmw730 := concreteBMWFactory.produce730BMW()
bmw840 := concreteBMWFactory.produce840BMW()
bmw730.run()
bmw840.run()
}
单例模式(Singleton Pattern)是一种简单的创建模式,有些对象我们往往只需要全局一个,比如全局缓存、数据库连接等。
需要频繁实例化然后销毁的对象
创建对象耗时过多或资源过多,但有经常用到
系统只需要一个实例对象,如果系统要求提供一个唯一的序列号生成器或资源管理器,或者对象消耗资源太大而只允许创建一个对象。
代码
package main
import (
"fmt"
"sync"
)
// 数据库连接对象实例
type DBInstance struct {
}
var (
once sync.Once
dbInstance *DBInstance
)
// 注意初始对象实例过程也可以放到init函数中进行
func NewInstance() *DBInstance {
// 只有第一次才会实例化数据库连接对象
if dbInstance != nil {
once.Do(func() {
dbInstance = &DBInstance{}
})
}
return dbInstance
}
func main() {
for i := 0; i <= 10; i++ {
// 在使用的时候获取数据库实例对象
db := NewInstance()
fmt.Println(db)
}
}
建造者模式(Build Pattern)又叫生成器,将一个复杂对象分解成多个相对简单的部分,之后按步骤创建这个复杂对象的每一个部分。该模式允许你使用相同的创建代码生成不同的对象。
代码
package main
import "fmt"
type Car struct {
engine string
chassis string
body string
}
// 生成器
type CarBuilder struct {
engine string
chassis string
body string
}
func (c *CarBuilder) addChassis(chassis string) {
c.chassis = chassis
}
func (c *CarBuilder) addEngine(engine string) {
c.engine = engine
}
func (c *CarBuilder) addBody(body string) {
c.body = body
}
func (c *CarBuilder) build() Car {
return Car{
engine: c.engine,
chassis: c.chassis,
body: c.body,
}
}
func main() {
carBuilder := &CarBuilder{}
// 按步骤创建对象
carBuilder.addEngine("v12")
carBuilder.addChassis("复合材料")
carBuilder.addBody("镁合金")
// 构建Car对象
car := carBuilder.build()
fmt.Println(car)
}
如果你希望生成一个对象,这个对象与另一个对象完全相同,该如何实现呢?
如果遍历对象的所有成员,将其依次复制到新对象中,会稍显麻烦。
原型模式(Prototype Pattern)将这个克隆新对象过程委派给了被克隆对象(其实就是将这个被克隆对象的类中增加一个克隆函数),被克隆对象叫做原型。
注意:在写克隆函数时,要注意深浅拷贝问题。
package main
import "fmt"
type obj struct {
name *string
}
func (o *obj) Clone() *obj {
new_obj := *o
return &new_obj
}
func main() {
name := "qiliang"
o1 := &obj{name: &name}
o2 := o1.Clone()
// 注意会有浅拷贝问题
*o2.name = "xiaolin"
fmt.Println(*o1.name)
fmt.Println(*o2.name)
}
适配器模式(Adaptor Pattern)说白了就是兼容,假设一开始我们提供了A对象,后期随着业务迭代,有需要在A对象的基础上衍生出不同的需求。如果有很多函数已经在线上调用了A对象,此时再对A对象修改就比较麻烦,也可能会出现问题。甚至更糟糕的情况,你坑你没有程序库的源代码,从而无法进行修改。
此时就可以用一个适配器,它就像一个接口转换器,调用方只需要调用这个适配器,而不需要去关心背后的实现,由适配器接口封装复杂的逻辑转换过程。
package main
import "fmt"
type CM2M struct {
}
func (c CM2M) CMtoM(cm float64) (m float64) {
m = cm / 100
return
}
type M2CM struct {
}
func (c M2CM) MtoCM(m float64) (cm float64) {
cm = m * 100;
return cm
}
// 适配器函数,自动进行cm与m之间的转换
// 有了这个适配器逻辑后我们,不需要再关注转换的逻辑
type AdapterTransLength interface {
transLength(string, float64) float64
}
type TransLengthAdapter struct {
}
func (t TransLengthAdapter)transLength(whickType string, length float64) float64 {
if whickType == "m" {
return M2CM{}.MtoCM(length)
}
return CM2M{}.CMtoM(length)
}
func main() {
transAdapter := TransLengthAdapter{}
ans := transAdapter.transLength("m", 10)
fmt.Println(ans)
}
假设一开始业务需要两种发送消息渠道:sms和email,我们可以分别实现sms和email接口。
之后随着业务迭代,又产生了新的需求,需要提供两种系统发送的方式,systemA和systemB并且这两种系统发送方式都应该支持sms和email渠道
此时至少需要提供4中方法:systemA to sms、systemA to email、systemB to sms、systemB to email
如果在增加新的需求维度,那么类的数量会出现质数增长,这是我们不能接受的
解决方案
其实我们之前是在用继承的想法来看问题,桥接模式则希望将继承关系转变为关联关系,使两个类独立存在,且一个类通过实现接口聚合在另一个类中。
详细说一下:
用一句话总结桥接模式的理念就是:“将抽象与实现接口,将不同的类别的继承关系改为关联关系”
代码
package main
import "fmt"
// 声明消息接口
type SendMessage interface {
send(text, to string)
}
// 声明第一种消息
type sms struct {
}
func (s sms) send(text, to string) {
fmt.Println(fmt.Sprintf("send %s to %s sms", text, to))
}
func NewSms() *sms {
return &sms{}
}
// 声明第二种消息
type email struct {
}
func (e email) send(text, to string) {
fmt.Println(fmt.Sprintf("send %s to %s email", text, to))
}
func NewEmail() *email {
return &email{}
}
// 声明两种发送渠道,这两种发送渠道都要支持sms和email消息(通过组合SendMessage接口来实现
// 本质是因为在组合SendMessage接口后,sms、emali都可以放到systemA中的method字段,当method字段值不同时就可以发送不同的消息)
type systemA struct {
method SendMessage
}
func NewSystemA(method SendMessage) *systemA {
return &systemA{
method: method,
}
}
func (s *systemA) SystemASendMes(text, to string) {
s.method.send("SystemA "+text, to)
}
type systemB struct {
method SendMessage
}
func NewSystemB(method SendMessage) *systemB {
return &systemB{
method: method,
}
}
func (b *systemB) SystemBSendMes(text, to string) {
b.method.send("SystemB " + text, to)
}
func main() {
// 声明要发送的类型
sms := NewSms()
email := NewEmail()
// 在SystemA中发送两种消息
systemA1 := NewSystemA(sms)
systemA2 := NewSystemA(email)
systemA1.SystemASendMes("ni hao !", "qiliang")
systemA2.SystemASendMes("ni hao !", "qiliang")
systemB1 := NewSystemB(sms)
systemB2 := NewSystemB(email)
systemB1.SystemBSendMes("gan xie !", "xiaoming")
systemB2.SystemBSendMes("gan xie !", "xiaoming")
}
有时候我们对一个类进行封装,形成一个新类,这个新类在原类的基础上拥有额外的功能。
例如一个披萨类,你可以在对披萨类进行封装,形成新的类如番茄披萨类和芝士披萨类。此时这个封装就是装饰器模式的核心思想。
简单来说,装饰器模式就是将对象封装到形成一个新对象,这个信息对象功能更丰富(可以理解为:为源对象绑定了新的行为功能)
如果你希望在无需修改对象的情况下使用对象,并且希望对象新增额外的行为,就可以考虑使用装饰器模式。
代码
package main
import "fmt"
type pizza interface {
getPrice() int
}
type basePizza struct {
}
func (p basePizza) getPrice() int {
return 15
}
type tomatoPizza struct {
pizza pizza
}
func (p tomatoPizza) getPrice() int {
return p.pizza.getPrice() + 10
}
type cheesePizza struct {
pizza pizza
}
func (p cheesePizza) getPrice() int {
return p.pizza.getPrice() + 20
}
func main() {
tomPizza := tomatoPizza{}
tomPizza.pizza = basePizza{}
price := tomPizza.getPrice()
fmt.Println(price)
}
如果你需要在访问一个对象时,有一个像“代理”一样的角色,它可以在访问对象之前为你进行缓存检查、权限判断等访问控制,在访问对象之后为你进行结果缓存、日志记录等处理,那么可以考试率使用代码模式(Proxy Pattern)。
会议一下一些web框架的router模块,当客户端访问一个接口时,在最终执行对应的接口之前,router模块会执行一些事前操作,进行权限判断操作,在执行之后会记录日志,这就是典型的代理模式。
代理模式需要一个代理类,其包含执行真正对象所需要的成员变量,并由代理类管理整个声明周期。
代码
package main
import "fmt"
type Subject interface {
ProxyFun() string
}
// 声明代理类
type Proxy struct {
real RealSubject
}
func (p Proxy) ProxyFun() string {
var ans string
// 在低啊用真实对象之前,检查缓存、判断权限等
p.real.PreFun()
p.real.RealFun()
p.real.AfterFun()
// 在调用完操作之后,可以缓存结果、对结果进行处理等(如脱敏)、记录日志等
return ans
}
type RealSubject struct {
}
func (s RealSubject) RealFun() {
fmt.Println("real...")
}
func (s RealSubject) PreFun() {
fmt.Println("Pre...")
}
func (s RealSubject) AfterFun() {
fmt.Println("After...")
}
func main() {
rs := RealSubject{}
proxy := Proxy{real: rs}
proxy.ProxyFun()
}
如果你需要在对一个对象的状态被改变时,其他对象能作为“观察者”被通知,就可以使用观察者模式。
我们将自身状态改变就回通知其他对象的对象称为“发布者”,关注发布者状态变化的对象称为“订阅者”。
代码
package main
import "fmt"
// 发布者-主题
type Subject struct {
observers []Observer
content string
}
func NewSubject() *Subject {
return &Subject{
observers: make([]Observer, 0),
}
}
// 添加订阅者
func (s *Subject) AddObserver(o Observer) {
s.observers = append(s.observers, o)
}
// 通知消费者
func (s *Subject) Notify() {
for _, o := range s.observers {
o.SendMessage(s)
}
}
// 发布消息
func (s *Subject) UpdateContent(content string) {
s.content = content
s.Notify()
}
// 观察者-订阅者接口
type Observer interface {
SendMessage(*Subject)
}
// 订阅者
type Reader struct {
name string
}
func NewReader(name string) *Reader {
return &Reader{
name: name,
}
}
func (r Reader) SendMessage(s *Subject) {
fmt.Println(r.name + " " + s.content)
}
func main() {
subject := NewSubject()
reader1 := NewReader("qiliang")
reader2 := NewReader("xiaolin")
subject.AddObserver(reader1)
subject.AddObserver(reader2)
subject.UpdateContent("ni hao !")
}
假设需要实现一个根据出行方式规划路线导航功能,出行的方式可以选择不行、骑行、开车,最简单的方式就是分别实现这个3种方法,供客户端调用。但是这样做会使客户端选择路线的代码和出行方式耦合了起来,如果新增一种出行的方式,就要修改客户端路线选择方案代码,这不符合开闭原则。
解决办法是使用策略模式(Strategy Pattern),它会将每种出行方案都抽取到一组新的类中,组中的类都会实现一个出行Interface,这个出行Interface约定了出行接口函数。客户端只需要指定指定所需要的出行方案即可。
代码
package main
import "fmt"
// 策略维护对象,包含了路线信息name 和 所用的的策略strategy
type Travel struct {
name string
strategy Strategy
}
func NewTravel(name string, strategy Strategy) *Travel {
return &Travel{
name: name,
strategy: strategy,
}
}
// 执行路线策略
func (p *Travel) getTraffic() {
p.strategy.traffic(p)
}
// 路线策略接口
type Strategy interface {
traffic(*Travel)
}
// 实现走路方式
type walk struct {
}
func (w *walk) traffic(t *Travel) {
fmt.Println(t.name + " walk...")
}
// 实现开车方式
type drive struct {
}
func (w *drive) traffic(t *Travel) {
fmt.Println(t.name + " drive...")
}
func main() {
// 生成走路路线策略
travel1 := NewTravel("qiliang", &walk{})
// 生成开车路线策略
travel2 := NewTravel("xiaolin", &drive{})
// 运行策略
travel1.getTraffic()
travel2.getTraffic()
}
// OutPut
qiliang walk...
xiaolin drive...
模板方法建议将过程/算法分解为一系列步骤(就比如:盖房子这个过程,对盖普通的房子或者盖别墅,它的大致步骤是相同,我们可以简单考虑的将盖房子的过程分为:步骤1 打地基、步骤2 建围墙、步骤3 盖房顶 这个三个基类中的方法)
然后将这些步骤改写成基类中的方法, 当我子类去继承基类时,子类也拥有了这些方法,并且在子类中我们可以重写基类中的某些方法,让子类更适配某个具体的流程(比如这个子类表示的是 “盖别墅类”, 那么我们可以重写“盖别墅类中”的“步骤1 打地基”方法,因为盖别墅打的地基,可能和基类中默认实现的打地基的方式不太一样,通过重写这个基类中的“步骤1 打地基”方法)。
如果以后新增加 “盖小区的”这个过程,我们仍然可以使用 基类中的定义的模板方法(就是盖房子的三个步骤),并对某些方法进行重写,因为我们只重写了部分方法,其它的步骤是用基类的默认方法,这样就减少了我们的代码量工作。
举例:
让我们来考虑一个一次性密码功能 (OTP) 的例子。 将 OTP 传递给用户的方式多种多样 (短信、 邮件等)。 但无论是短信还是邮件, 整个 OTP 流程都是相同的:
后续引入的任何新 OTP 类型都很有可能需要进行相同的上述步骤。
因此, 我们会有这样的一个场景, 其中某个特定操作的步骤是相同的, 但实现方式却可能有所不同。 这正是适合考虑使用模板方法模式的情况。
首先, 我们定义一个由固定数量的方法组成的基础模板算法。 这就是我们的模板方法。 然后我们将实现每一个步骤方法, 但不会改变模板方法。
代码
package main
import "fmt"
// 定义OTP步骤接口
type IOtp interface {
genRandomOTP(int) string
saveOTPCache(string)
getMessage(string) string
sendNotification(string) error
}
// 对IOtp封装一下
type Otp struct {
iOtp IOtp
}
// 相同OTP执行步骤调用
func (o *Otp) genAndSendOTP(otpLength int) error {
otp := o.iOtp.genRandomOTP(otpLength)
o.iOtp.saveOTPCache(otp)
message := o.iOtp.getMessage(otp)
err := o.iOtp.sendNotification(message)
if err != nil {
return err
}
return nil
}
// 定义Sms实现IOtp接口
type Sms struct {
}
func (s *Sms) genRandomOTP(len int) string {
randomOTP := "1234"
fmt.Printf("SMS: generating random otp %s\n", randomOTP)
return randomOTP
}
func (s *Sms) saveOTPCache(otp string) {
fmt.Printf("SMS: saving otp: %s to cache\n", otp)
}
func (s *Sms) getMessage(otp string) string {
return "SMS OTP for login is " + otp
}
func (s *Sms) sendNotification(message string) error {
fmt.Printf("SMS: sending sms: %s\n", message)
return nil
}
// 定义Email实现IOtp接口
type Email struct {
}
func (s *Email) genRandomOTP(len int) string {
randomOTP := "1234"
fmt.Printf("EMAIL: generating random otp %s\n", randomOTP)
return randomOTP
}
func (s *Email) saveOTPCache(otp string) {
fmt.Printf("EMAIL: saving otp: %s to cache\n", otp)
}
func (s *Email) getMessage(otp string) string {
return "EMAIL OTP for login is " + otp
}
func (s *Email) sendNotification(message string) error {
fmt.Printf("EMAIL: sending email: %s\n", message)
return nil
}
func main() {
smsOTP := &Sms{}
o := Otp{
iOtp: smsOTP, // 接口实现多态
}
o.genAndSendOTP(4)
fmt.Println("")
emailOTP := &Email{}
o = Otp{
iOtp: emailOTP, // 接口实现多态
}
o.genAndSendOTP(4)
}
文章参考:
Go 常用设计模式
图解九种常见的设计模式 - SegmentFault 思否
用Go语言实现23种设计模式 - 掘金