为什么要有设计模式
主要有以下几个原因:
- 解决重复问题:设计模式基于大量的软件开发实践经验,提供了丰富的、可复用的解决方案,来解决项目中频繁出现的问题。
- 提供通用术语:设计模式提供了一套公共词汇,开发者可以利用这些解决方案在项目中进行讨论,使得开发者间能准确理解思考的问题和解决方法。
- 易理解和维护:设计模式通常将复杂的设计问题分解为相互独立、互不影响的子问题,从而降低系统复杂度,使得软件更容易被理解和维护。
- 提升代码质量:设计模式通常利用面向对象设计原则,如封装、抽象、继承和多态等,从而提升了代码的质量,在扩展性、灵活性和可重用性上都有很大提升。
- 加速开发进程:当您遇到一个常见的设计问题时,如果您知道有一个设计模式已经解决了此类问题,那么你可以直接应用这个设计模式,省去自己设计和验证的过程,从而提升开发效率。
设计模式分类
市面上目前主要有23种设计模式,这么多设计模式必然会让我们眼花缭乱,这么多设计模式我该从哪儿下手,如何学习?我们可以简单的按照特性对设计模式分类,分类有助于我们更快的理解,强化我们的记忆。
主要可分为三个类别:创建型、结构型和行为型
创建型模式
创建型模式关注对象的创建机制,尤其是在标准的对象创建生硬的方式可能带来问题的时候。
- 工厂方法模式 (Factory Method)
- 抽象工厂模式 (Abstract Factory)
- 创建者模式 (Builder)
- 原型模式 (Prototype)
- 单例模式 (Singleton)
结构型模式
结构型模式关注对象的合成,或者各个对象如何共同形成一个大的结构。
- 适配器模式 (Adapter)
- 桥接模式 (Bridge)
- 组合模式 (Composite)
- 装饰模式 (Decorator)
- 外观模式 (Facade)
- 享元模式 (Flyweight)
- 代理模式 (Proxy)
行为型模式
行为型模式关注对象的责任分配。它们处理对象间的交互,这些交互包括信息交流与处理链等。
- 责任链模式 (Chain of Responsibility)
- 命令模式 (Command)
- 解释器模式 (Interpreter)
- 迭代器模式 (Iterator)
- 中介者模式 (Mediator)
- 备忘录模式 (Memento)
- 观察者模式 (Observer)
- 状态模式 (State)
- 策略模式 (Strategy)
- 模板方法模式 (Template Method)
- 访问者模式 (Visitor)
这样分类之后是不是更加清晰了呢,总结来说就是以下三点:
创建型模式关注对象的创建机制,结构型模式关注对象的组合构建,行为模式关注责任分配,大任务拆解、流水线执行等等。
工厂模式
工厂模式是一种创建型设计模式,提供了一种封装对象创建过程的方式。在工厂模式中,客户端通过调用一个工厂方法(或根据需要通过构造函数、属性等方式)来创建对象,而不是直接使用 new 关键字来实例化对象。这样可以抽象出实例化具体类的过程,提高系统的灵活性和可扩展性。
使用场景
- 当一个类不能预见必须创建哪种类的对象时。
- 当一个类希望由其子类来指定创建对象时。
- 当一个类将创建对象的责任委托给多个帮助类中的任何一个,而且你希望将哪个帮助类是代理者这个信息局部化时。
优点
- 在系统中,如果某个客户端需要请求某种服务,直接与具体的服务交互存在很多问题,如程序的耦合度高,缺乏灵活性等。工厂模式可以解决这些问题。
- 可以封装对象的创建细节,使用户只需要关心如何使用,不需要知道具体创建过程和细节,屏蔽产品类
缺点
- 增加了代码的复杂性;
- 增加了系统的抽象性。
示例
package main
import "fmt"
// Animal 接口
type Animal interface {
Speak() string
}
// Dog 结构体
type Dog struct {}
func (d Dog) Speak() string {
return "Woof!"
}
// Cat 结构体
type Cat struct {}
func (c Cat) Speak() string {
return "Meow!"
}
// AnimalFactory 结构体
type AnimalFactory struct{}
// AnimalFactory 的方法 CreateAnimal
func (af AnimalFactory) CreateAnimal(t string) Animal {
switch t {
case "dog":
return Dog{}
case "cat":
return Cat{}
default:
return nil
}
}
func main() {
animalFactory := new(AnimalFactory)
dog := animalFactory.CreateAnimal("dog")
fmt.Println(dog.Speak()) // 输出: Woof!
cat := animalFactory.CreateAnimal("cat")
fmt.Println(cat.Speak()) // 输出: Meow!
}
在此示例中,当需要添加新的动物类型时,只需创建新的实现 Animal 接口的结构体,然后在 "CreateAnimal" 方法中添加相应的创建逻辑即可,主函数的代码不需要做任何修改。这就保证了对象创建过程的灵活和解耦。
抽象工厂模式
抽象工厂模式是一种创建型设计模式,它提供一个接口来创建一系列相关或相互依赖的对象,而无需指定其具体类。
结构
主要由以下几个部分组成:
- 抽象工厂(Abstract Factory):声明了一组方法,这些方法用于产生不同种类的抽象产品。
- 具体工厂(Concrete Factory):实现抽象工厂中的抽象方法,用于生成具体的产品。
- 抽象产品(Abstract Product):为一类产品声明接口。
- 具体产品(Concrete Product):是抽象产品的多种类型的实现。
使用场景
抽象工厂模式主要应用于如下场景:
- 一个系统要独立与它的产品如何被创建、组成和表示时。
- 系统要由多个产品系列中的一个配置时。
- 强调一系列相关的产品对象(属于同一个产品族)一起使用创建对象需要大量重复的代码时。
优点和缺点
优点
- 符合开闭原则:如果需要增加产品族,只需要增加相应的具体工厂,不需要修改原有系统。
- 客户端代码与具体的产品类解耦,只依赖于产品抽象接口。
缺点
- 扩展新的产品族(即增加新的具体工厂)可能比较困难,除非所有产品类都继承自同一抽象类。
示例
package main
import "fmt"
// 抽象产品:食物
type Food interface {
Eat()
}
// 具体产品:牛肉面
type BeefNoodle struct {
}
func (n BeefNoodle) Eat() {
fmt.Println("eat noodle")
}
// 具体产品:烧饼
type ShaoBing struct {
}
func (s ShaoBing) Eat() {
fmt.Println("eat shaobing")
}
// 抽象工厂:早餐店
type BreakfastFactory interface {
Cook() Food
}
// 具体工厂:面馆
type NoodleHouse struct {
}
func (n NoodleHouse) Cook() Food {
return BeefNoodle{}
}
// 具体工厂:烧饼铺
type ShaobingStore struct {
}
func (s ShaobingStore) Cook() Food {
return ShaoBing{}
}
func main() {
var f1 BreakfastFactory = &NoodleHouse{}
f1.Cook().Eat()
var f2 BreakfastFactory = &ShaobingStore{}
f2.Cook().Eat()
}
在这个例子中,面馆(NoodleHouse)和烧饼铺(ShaobingStore)是两个实现了早餐店(BreakfastFactory)接口的具体工厂,烧饼(ShaoBing)和牛肉面(BeefNoodle)是他们分别生产的具体产品。主函数中通过这两个工厂的Cook方法,分别找到了具体的产品实例,调用其Eat方法。无论增加多少种食物或者早餐店,对于客户端的代码调用都没有影响,同时也扩展了系统的一些函数,符合面向对象设计原则。
创建者模式
建造者模式是一种创建型设计模式,用于构建复杂对象,可以按需进行部分构建,并允许通过相同的构建过程创建不同的产品。
使用场景
主要应用于创建复杂的对象的场景,例如:
- 当需要创建的对象具有复杂的内部结构时。
- 当算法与需要构建的对象独立时。
- 当构建过程必须允许被构造的对象有不同的表示形式时。
优点
- 可以创建复杂对象的复制品。
- 在对象构造过程的最后步骤,才返回对象,这意味着一个创建过程无效(可能由于参数错误等)不会返回一个无效对象。
- 通常用于创建组合模式类,仅在对象创建完成后,才会组合成树形结构。
- 提供更好的控制构建过程。
缺点
- 类图中包含很多类,增加了系统复杂度。
- 如果初始化简单,可能不适合使用 Builder。
示例
package main
import (
"fmt"
)
type Phone struct {
Make string
Model string
Platform string
Battery int
}
type PhoneBuilder struct {
phone *Phone
}
func NewPhoneBuilder() *PhoneBuilder {
return &PhoneBuilder{&Phone{}}
}
func (pb *PhoneBuilder) SetMake(make string) *PhoneBuilder {
pb.phone.Make = make
return pb
}
func (pb *PhoneBuilder) SetModel(model string) *PhoneBuilder {
pb.phone.Model = model
return pb
}
func (pb *PhoneBuilder) SetPlatform(platform string) *PhoneBuilder {
pb.phone.Platform = platform
return pb
}
func (pb *PhoneBuilder) SetBattery(battery int) *PhoneBuilder {
pb.phone.Battery = battery
return pb
}
func (pb *PhoneBuilder) Build() *Phone {
return pb.phone
}
func main() {
builder := NewPhoneBuilder()
iphone := builder.
SetMake("Apple").
SetModel("iPhone 12").
SetPlatform("iOS").
SetBattery(2815).
Build()
fmt.Printf("Phone: %+v\n", iphone)
galaxy := builder.
SetMake("Samsung").
SetModel("Galaxy S21").
SetPlatform("Android").
SetBattery(5000).
Build()
fmt.Printf("Phone: %+v\n", galaxy)
}
首先定义了一个新的产品“Phone”和对应的PhoneBuilder
。Phone
包含了制造商, 型号, 平台和电池容量这几个字段。我们对PhoneBuilder
增加了SetMake
、SetModel
、SetPlatform
和SetBattery
几个方法,每个方法设置一个对应的属性,并返回当前的Builder实例以支持链式操作。最后我们提供了一个Build
方法,用于返回当前构建的Phone
实例。
在main
函数中,我们创建了两个不同的Phone实例:一个是iPhone,另一个是Galaxy。其中每个实例的属性都是通过链式操作设置的。
原型模式
原型模式就是从一个对象再创建另外一个可定制的对象,而且不需要知道任何创建的细节。
使用场景
原型模式主要用在以下场景:
- 当一个系统的类的实例对其所创建的对象类型有更深入的理解,且希望创建一个相似对象时。
- 当一个系统要保存某个实例的状态,以便在需要的时候恢复(即实现备份和恢复)。
优点
- 原型模式在创建对象的实例过程中非常高效,特别是当创建新的对象实例成本较高时。
- 可以在运行期间添加和删除产品。
- 对象的复制是通过应用层进行控制,并不需要调用初始化过程。
缺点
- 对象的构造过程中可能会对外有一些副作用,但是使用原型模式创建新的对象时并不能得到任何提示信息。
- 对于复杂的对象,在复制过程中需要对其内部属性进行较深的复制,以保障新对象和原对象间的独立性,这个复制过程可能会相当复杂。(深拷贝和浅拷贝问题)
示例
假设我们正在编写一个应用程序,需要处理大量的员工数据。为了效率,我们希望复制已有的员工数据,然后根据需要进行调整。这样,不需要从头开始创建新的实例,减少了对象初始化的复杂性
package main
import "fmt"
// 定义一个 Clone 接口,所有具备克隆功能的结构体都应该实现这个接口。
type Cloner interface {
Clone() Cloner
}
// Employee 是一个我们将要使用的员工数据,实现了 Cloner 接口。
type Employee struct {
Name string
Age int
Salary float32
}
// Clone 实现了 Cloner 接口的方法,用来拷贝 Employee 数组自身。
func (e *Employee) Clone() Cloner {
clone := *e // 复制 Employee 然后将其返回。
return &clone
}
func main() {
// 创建一个具体的原型对象。
prototype := &Employee{
Name: "Tom",
Age: 30,
Salary: 5000,
}
// 通过原型对象创建新对象,然后对新对象进行修改
employee1 := prototype.Clone().(*Employee) // Clone Employee
employee1.Name = "Jerry" // 只需要修改 Name
fmt.Printf("Employee1: %+v\n", employee1) //Output: Employee1: &{Name:Jerry Age:30 Salary:5000}
employee2 := prototype.Clone().(*Employee) // Clone Employee
employee2.Age = 35 //只需要修改 Age
fmt.Printf("Employee2: %+v\n", employee2) //Output: Employee2: &{Name:Tom Age:35 Salary:5000}
}
单例模式
单例模式是一种创建型设计模式,总是返回同一个实例以确保一个类只有一个实例,并为其提供一个全局访问点。
使用场景
单例模式主要用在以下场景:
- 当一个类在系统中只应该存在一个实例时,如一个系统中可以存在多个打印任务,但是只能有一个正在工作的任务。
- 当你需要控制确切的创建时机和全局访问点。
优点
- 确保一个类只有一个实例,避免出现多个相同实例引发的问题。
- 提供了全局访问点。
缺点
- 单例模式可能会隐藏不好的设计,如程序的各个部分都知道它,可能会增加耦合。
- 在多线程环境中需要特别注意线程安全问题。
示例
package main
import (
"fmt"
"sync"
)
// singleton 是我们需要确保只有一个实例的结构体。
type singleton struct {
}
var instance *singleton
var once sync.Once
// GetInstance 用于获取 singleton 实例,
// 使用 sync.Once 来确保全局只会创建一个 singleton 实例。
func GetInstance() *singleton {
once.Do(func() {
instance = &singleton{}
})
return instance
}
func main() {
// 通过GetInstance方法获取单例
instance := GetInstance()
fmt.Printf("Instance: %+v\n", instance)
anotherInstance := GetInstance()
fmt.Printf("Another Instance: %+v\n", anotherInstance)
if instance == anotherInstance {
fmt.Println("Instances are same.") // 输出:Instances are same.
}
}
我们使用 sync.Once
来确认 singleton
的全局实例只被创建一次。在 main
函数中,我们首先获取 singleton
的实例,然后再次获取实例,并比较这两个实例。由于我们使用了单例模式,GetInstance
返回的总是同一个实例。
适配器模式(Adapter Pattern)
适配器模式(也称为包装模式)是一种结构设计模式,让你能够使不能共同工作的对象能够共同工作,因为这些对象可能具有不同的接口,而适配器可以将其中一个对象的接口转化为另一种接口。
适配器模式的主要目标是确保在现有的类不能被修改的情况下,让不兼容的接口能够一起工作。这种模式是在代码无法或不希望因接口不兼容而进行大量修改的情况下使用的。
适配器种类
适配器可以分为两种:类适配器和对象适配器。
- 类适配器模式: 这种适配器继承了 Adaptee(被适配者) 类和 Target(目标) 类或接口。它主要通过多重继承来实现适配器功能。然而,由于 Go 语言并不支持多重继承,对于 Go 语言来说,我们只能使用对象适配器。
- 对象适配器模式: 这种适配器包含了一个 Adaptee 类的实例。它实现了 Target(目标) 接口,与 Target 交互的调用会被委派给 Adaptee。
代码结构
在适配器模式中,通常有以下四类角色:
- Target(目标): 这是我们期望的接口,客户端使用这个接口与程序交互。
- Client(请求者): 它通过目标接口与适配器交互,而客户端并不知道适配器和被适配者的存在。
- Adaptee(被适配者): 它是我们需要适应的接口,被适配者可能有一些复杂功能,但其接口与我们的期望接口不匹配。
- Adapter(适配器): 它将被适配者的接口转换为目标接口。
客户端始终通过目标接口与适配器交互,适配器则负责将这些交互转发给被适配者。这样,客户端可以避免直接与被适配者交互。
优点和缺点
优点:
- 它可以让任何两个没有关联的类一起运行。
- 提高了类的复用。
- 增加了类的透明度。
缺点:
过多地使用适配器,会让系统非常零乱,不容易整体进行把握。比如,明明看到调用的是A的接口,其实底层被适配成了B的实现,一个系统的复杂性由此大大增加。
示例
想象一个音频播放器,它可以播放 MP3 格式的音频,我们需要为其增加播放 MP4 和 FLAC 音频格式的能力,而现有的播放器并没有这些功能,因为某些原因我们不能或者不想再原有的音频播放器升级。这就是 Adapter 模式真正需要发挥作用的地方。
package main
import "fmt"
// MediaPlayer 是原始的接口,我们的 AudioPlayer 实现了 MediaPlayer 接口
type MediaPlayer interface {
Play(audioType string, filename string)
}
// AdvancedMediaPlayer 是一个更高级的音频播放器,它能播放 vlc 和 mp4
type AdvancedMediaPlayer interface {
PlayVLC(filename string)
PlayMP4(filename string)
}
// VLCPlayer 实现了 AdvancedMediaPlayer 中的 PlayVLC
type VLCPlayer struct{}
func (v *VLCPlayer) PlayVLC(filename string) {
fmt.Printf("Playing vlc file. Name: %s\n", filename)
}
func (v *VLCPlayer) PlayMP4(filename string) {
// 什么都不做
}
// MP4Player 实现了 AdvancedMediaPlayer 中的 PlayMP4
type MP4Player struct{}
func (m *MP4Player) PlayVLC(filename string) {
// 什么都不做
}
func (m *MP4Player) PlayMP4(filename string) {
fmt.Printf("Playing mp4 file. Name: %s\n", filename)
}
// MediaAdapter 功能的适配器
type MediaAdapter struct {
advancedMusicPlayer AdvancedMediaPlayer
}
func (m *MediaAdapter) MediaAdapter(audioType string) {
if audioType == "vlc" {
m.advancedMusicPlayer = &VLCPlayer{}
} else if audioType == "mp4" {
m.advancedMusicPlayer = &MP4Player{}
}
}
func (m *MediaAdapter) Play(audioType string, filename string) {
if audioType == "vlc" {
m.advancedMusicPlayer.PlayVLC(filename)
} else if audioType == "mp4" {
m.advancedMusicPlayer.PlayMP4(filename)
}
}
// AudioPlayer 媒体播放器包含一个 MediaAdapter
type AudioPlayer struct {
mediaAdapter *MediaAdapter
}
func (a *AudioPlayer) Play(audioType string, filename string) {
// 播放 mp3 音乐文件的内置支持
if audioType == "mp3" {
fmt.Printf("Playing mp3 file. Name: %s\n", filename)
} else if audioType == "vlc" || audioType == "mp4" {
a.mediaAdapter = &MediaAdapter{}
a.mediaAdapter.MediaAdapter(audioType)
a.mediaAdapter.Play(audioType, filename)
} else {
fmt.Printf("Invalid media. %s format not supported", audioType)
}
}
func main() {
audioPlayer := &AudioPlayer{}
// AudioPlayer 可以播放 mp3
audioPlayer.Play("mp3", "beyond the horizon.mp3")
// AudioPlayer 可以播放 mp4 via MediaAdapter
audioPlayer.Play("mp4", "alone.mp4")
// AudioPlayer 可以播放 vlc via MediaAdapter
audioPlayer.Play("vlc", "far far away.vlc")
// AudioPlayer 无法播放除 mp3/mp4/vlc 以外的格式音频
audioPlayer.Play("avi", "mind me.avi")
}
以上代码显示了适配器如何允许两个不兼容的接口在一起工作。在这个例子中,AudioPlayer
可以使用 MediaPlayer
接口播放 MP3 音频,但它也想支持 MP4 和 VLC 音频,为此我们使用了 MediaAdapter
来适配 AdvancedMediaPlayer
。
桥接模式 (Bridge)
桥接模式(Bridge Pattern
)是一种结构型设计模式,它将抽象与其实现部分分离开来,以便二者可以独立变化。这种模式的主要目标是避免抽象和实现部分的紧密耦合,这使得它们可以独立进行变化。
使用场景
桥接模式通常在以下场景中使用:
- 当一个类存在多个独立变化的维度,并且每一个维度都需要进行扩展的时候。
- 当一个系统不希望使用继承或因多层次继承导致系统类的个数急剧增加时,可以采用桥接模式来降低系统的复杂度。
- 当一个系统需要在构件的抽象化角色和具体化角色之间增加更多的灵活性时。
优点
1.抽象与实现分离: 这是桥接模式的核心优点,它将抽象与实现者进行分离。
2.优秀的可扩展性: 可以通过聚合、继承等方式对系统进行很好的扩展,符合开闭原则。
3.灵活性提高: 由于抽象和实现是独立变化的,所以系统具有比较好的灵活性,可以单独改变抽象部分或实现部分。
缺点
由于使用了聚合关联关系、且需要设计非常多类使得系统变得复杂。由此,理解和设计相对困难。
示例
package main
import "fmt"
// 遥控器抽象
type RemoteControl interface {
PressOnButton()
PressOffButton()
}
// 设备实现
type Device interface {
On()
Off()
}
// TV 结构体作为设备的实现
type TV struct {
IsRunning bool
}
func (tv *TV) On() {
tv.IsRunning = true
fmt.Println("Turning TV on")
}
func (tv *TV) Off() {
tv.IsRunning = false
fmt.Println("Turning TV off")
}
// 音响结构体作为另一种设备的实现
type SoundSystem struct {
IsRunning bool
}
func (ss *SoundSystem) On() {
ss.IsRunning = true
fmt.Println("Turning SoundSystem on")
}
func (ss *SoundSystem) Off() {
ss.IsRunning = false
fmt.Println("Turning SoundSystem off")
}
// BasicRemote 结构体是遥控器的实现
type BasicRemote struct {
device Device
}
func (br *BasicRemote) PressOnButton() {
fmt.Println("Pressing on button...")
br.device.On()
}
func (br *BasicRemote) PressOffButton() {
fmt.Println("Pressing off button...")
br.device.Off()
}
func main() {
tv := &TV{}
remote := &BasicRemote{device: tv}
remote.PressOnButton()
remote.PressOffButton()
ss := &SoundSystem{}
remote.device = ss
remote.PressOnButton()
remote.PressOffButton()
}
在此代码中,遥控器是抽象部分,设备(例如电视、音响)是实现部分。我们让遥控器包含一个设备接口,这样我们就可以使用同一种遥控器模型去控制所有设备。这就是桥接模式的特点,允许我们独立地改变或升级所有遥控器和设备类型,而不需要修改任何遥控器或设备。
组合模式 (Composite)
组合模式是一种结构型设计模式, 你可以使用它将对象组合成树状结构, 并且能像使用独立对象一样使用它们。如果应用的核心模型能用树状结构表示, 在应用中使用组合模式才有价值。
组合模式主要包括以下三个角色:
- 组件(Component) :所有组合中的对象都共享的接口,它有些方法是所有类都共同具有的。
- 叶子(Leaf) :叶子对象,没有子节点。
- 复合(Composite) : 定义有子部件的那些部件的行为,它存储子部件,并且实现Component的接口。
适用场景
- 在想表示对象的部分-整体层次结构时。
- 你希望用户在使用单一对象和组合对象时保持一致性。
优点:
- 你可以更方便地使用递归结构。
- 开放/闭合原则。你可以在不动现有代码的情况下新增新元素,从而使得对象结构可以灵活地演变。
缺点:
- 为设计通用的组件接口可能会有一些困难。
示例
我们以文件系统为例。文件系统由目录和文件组成。每个目录可以包含其他目录或文件,每个文件是终端节点(叶子节点)。这是一个自然的递归结构,可以很自然地应用组合模式。
在这个模型中:
- Component - 文件和目录可以共享的接口,例如获取名称,获取大小等。
- Leaf - 文件,它是终端节点,没有子节点。
- Composite - 目录,它维护子节点(可以是其他目录或文件),并实现Component接口。
package main
import (
"fmt"
"strings"
)
// Component : File or Folder
type Component interface {
Search(string)
}
// File : Leaf
type File struct {
name string
}
func (f *File) Search(keyword string) {
if strings.Contains(f.name, keyword) {
fmt.Println(f.name)
}
}
// Folder : Composite
type Folder struct {
name string
children []Component
}
func (f *Folder) Search(keyword string) {
if strings.Contains(f.name, keyword) {
fmt.Println(f.name)
}
for _, component := range f.children {
component.Search(keyword)
}
}
func (f *Folder) Add(c Component) {
f.children = append(f.children, c)
}
func main() {
file1 := &File{"File1"}
file2 := &File{"File2"}
file3 := &File{"File3"}
folder1 := &Folder{
name: "Folder1",
children: []Component{},
}
folder1.Add(file1)
folder2 := &Folder{
name: "Folder2",
children: []Component{},
}
folder2.Add(file2)
folder2.Add(file3)
folder := &Folder{
name: "Folder",
children: []Component{},
}
folder.Add(folder1)
folder.Add(folder2)
folder.Add(&File{"File4"})
folder.Search("Folder")
}
在此代码中,Search
方法在Composite(文件夹)中被递归执行,以搜索整个树形结构。对于Leaf(文件),Search
方法搜索单一对象。如果搜索的关键词在文件夹或文件的名称中出现,就会打印出它的名称。
装饰模式 (Decorator)
装饰模式是一种结构型设计模式, 允许你通过将对象放入包含行为的特殊封装对象中来为原对象绑定新的行为。
主要角色包括:
- 抽象组件(Interface Component) : 定义一个对象接口,可以给这些对象动态地添加职责。
- 具体组件(Concrete Component) : 定义了一个具体对象,也可以给这个对象添加一些职责。
- 装饰器(Decorator) : 持有一个指向某个组件对象的引用,并定义一个与原接口一致的接口。
- 具体装饰器(Concrete Dcorator) : 负责给组件对象“贴上”附加行为。
适用场景
- 在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责。 -ProcessRequest 处理链的构建。
优点
- 是对继承的有力补充。可以通过装饰模式在运行时刻动态扩展一个对象的功能,而且可以根据需要以最小的粒度添加具体功能。
- 装饰模式提供了比继承更加灵活的替代方案。
- 可以用多个不同的具体装饰类装饰同一对象,得到的结果也不尽相同。
缺点
- 装饰模式会产生更多的对象,增加了系统的复杂性。
- 多层装饰比较复杂。
示例
结合发送通知的场景,装饰模式对于实现各种消息通知(例如,邮件,短信,即时消息等)非常有用。
在这个示例中,我们有一个基本的通知接口,然后为每种类型的通知创建一个装饰器,每个装饰器都会增加一些额外的行为,并在执行其主要任务之前和之后加入一些细节。
package main
import "fmt"
// Notifier 是任何通知都需要实现的基础接口
type Notifier interface {
SendNotification()
}
// 具体的基础通知
type BasicNotifier struct{}
func (bn BasicNotifier) SendNotification() {
fmt.Println("发送一个基本通知...")
}
// 为通知添加Email通知的装饰器
type EmailNotifierDecorator struct {
Notifier // 继承了 Notifier 接口
}
func (en EmailNotifierDecorator) SendNotification() {
fmt.Println("准备发送Email通知...")
en.Notifier.SendNotification() // 在执行“发送通知”行为前后,添加了一些额外的行为
fmt.Println("Email通知发送完成!")
}
// 为通知添加短信通知的装饰器
type SMSNotifierDecorator struct {
Notifier // 继承了 Notifier 接口
}
func (sn SMSNotifierDecorator) SendNotification() {
fmt.Println("准备发送短信通知...")
sn.Notifier.SendNotification() // 在执行“发送通知”行为前后,添加了一些额外的行为
fmt.Println("短信通知发送完成!")
}
func main() {
basicNotifier := BasicNotifier{} // 基础通知
emailNotifier := EmailNotifierDecorator{
Notifier: basicNotifier, // 这里装饰器里面包装了一个基础通知
}
smsNotifier := SMSNotifierDecorator{
Notifier: emailNotifier, // 这里短信装饰器里面包装了一个email装饰器,email装饰器里面包装了一个基础通知
}
// 先发送短信通知,然后发送Email通知,最后发送基础通知
smsNotifier.SendNotification()
}
外观模式 (Facade)
外观模式是一种结构型设计模式,它提供了一个统一的接口用来访问子系统中的一群接口。外观模式定义了一个更高层次的接口,这个接口使得子系统更容易使用。
组成部分:
- Facade(外观角色) : 知道哪些子系统类负责处理请求,将客户的请求代理给合适的子系统对象。
- SubSystem(子系统角色) : 实现子系统的功能,处理Facade对象指派的任务。没有Facade的任何信息,不用关心Facade的存在。
适用场景
假设你必须在代码中使用某个复杂的库或框架中的众多对象。 正常情况下, 你需要负责所有对象的初始化工作、 管理其依赖关系并按正确的顺序执行方法等。最终, 程序中类的业务逻辑将与第三方类的实现细节紧密耦合, 使得理解和维护代码的工作很难进行。
如果你的程序需要与包含几十种功能的复杂库整合, 但只需使用其中非常少的功能, 那么使用外观模式会非常方便,
优点
- 减少系统相互依赖,简化了系统的访问接口。
- 提高了灵活性和安全性。
缺点
- 不符合开闭原则,如果改动很大,还是要修改Facade类的代码。
示例
package main
import "fmt"
// 简化的计算机系统,包括CPU、内存、硬盘
type CPU struct{}
func (c *CPU) Start() {
fmt.Println("CPU started")
}
type Memory struct{}
func (m *Memory) Start() {
fmt.Println("Memory started")
}
type Disk struct{}
func (d *Disk) Start() {
fmt.Println("Disk started")
}
// Computer类,代表计算机外观,接受一次性开机的指令,然后逐个启动各个设备
type Computer struct {
cpu *CPU
memory *Memory
disk *Disk
}
func NewComputer() *Computer {
return &Computer{
cpu: &CPU{},
memory: &Memory{},
disk: &Disk{},
}
}
func (c *Computer) Start() {
c.cpu.Start()
c.memory.Start()
c.disk.Start()
}
func main() {
computer := NewComputer()
computer.Start()
}
在这个例子中,我们有一个复杂的电脑启动过程,包括启动CPU、内存和硬盘,这个过程被我们封装在了Computer的Start中。通过外观模式,客户端可以很方便的启动整个系统,而不需要关心具体的启动过程。
享元模式 (Flyweight)
享元模式是一种结构型设计模式, 它摒弃了在每个对象中保存所有数据的方式, 通过共享多个对象所共有的相同状态, 让你能在有限的内存容量中载入更多对象。
享元模式可分成两种状态:
- 内部状态:在享元对象的内部且不会随环境改变而改变的共享部分,也就是可以共享出来的信息,存储在享元对象内部并且可以被一个系统中的所有对象所访问。
- 外部状态:在享元对象的外部且会随环境改变而改变的部分。外部状态必须由客户端存储,当使用享元对象时,再由客户端传入享元对象。这里的外部状态独立于享元对象,它是享元对象得以依赖的一个标记。
使用场景
享元模式主要用于减少创建对象的数量,以降低内存占用和提高性能。以下情况应该考虑使用享元模式:
- 程序中使用了大量相似对象,造成了大量内存消耗。
- 对象的大多数状态都可以变为外部状态,如果剥离了外部状态,那么可以用相对较少的共享对象取代很多对象。
优点
- 程序在运行过程中需要创建大量相同或相似的对象时,可以大大减少应用程序创建的对象,节省内存。
缺点
- 复杂化了系统结构,需要分离内部状态和外部状态,外部状态具有固化特性应该由客户端控制,而且客户端需要花更多时间处理享元对象。
示例
package main
import "fmt"
// 树类型
type TreeType struct {
Name string
Color string
}
// 森林
type Forest struct {
Trees []*Tree
}
func NewForest() *Forest {
return &Forest{}
}
// 添加一组同种类的树
func (f *Forest) PlantTrees(number int, typeName string, color string) {
treeType := &TreeType{Name: typeName, Color: color}
for i := 0; i < number; i++ {
tree := &Tree{X: i, Y: i, Type: treeType}
f.Trees = append(f.Trees, tree)
}
}
// 树
type Tree struct {
X, Y int
Type *TreeType
}
// 显示森林信息
func (f *Forest) Draw() {
for _, tree := range f.Trees{
fmt.Printf("Planted a %s tree at %d, %d\n", tree.Type.Name, tree.X, tree.Y)
}
}
func main() {
forest := NewForest() // 创建森林
// 创建 1000 棵松树,100 大橡树
forest.PlantTrees(1000, "Pine", "Green")
forest.PlantTrees(100, "Oak", "Yellow")
// 输出森林信息
forest.Draw()
}
这个例子中,我们模拟了一个森林,即使我们创建了一千多棵树,实际上只用了两个TreeType
实例,省去了一大部分内存的消耗。
代理模式 (Proxy)
代理模式是一种结构型设计模式, 让你能够提供对象的替代品或其占位符。代理控制着对于原对象的访问, 并允许在将请求提交给对象前后进行一些处理。
有时候我们希望能对某个对象的访问进行一些控制,而这些对象又可能是一些第三方的对象我们不好直接修改,因为这时候我们创建一个代理类。这就是代理模式的核心思想。
代理模式建议新建一个与原服务对象接口相同的代理类, 然后更新应用以将代理对象传递给所有原始对象客户端。 代理类接收到客户端请求后会创建实际的服务对象, 并将所有工作委派给它。
代理将自己伪装成数据库对象, 可在客户端或实际数据库对象不知情的情况下处理延迟初始化和缓存查询结果的工作。
这有什么好处呢? 如果需要在类的主要业务逻辑前后执行一些工作, 你无需修改类就能完成这项工作。 由于代理实现的接口与原类相同, 因此你可将其传递给任何一个使用实际服务对象的客户端。
使用场景
- 远程代理:为一个对象在不同的地址空间提供局部代表。
- 虚拟代理:通过使用一个小对象代表一个复杂对象,降低系统资源消耗。
- 安全代理:用来控制对原始对象的访问,例如,这原始对象有一些系统级别的操作权限。
- 智能引用:取代简单的指针,当对象被引用时,提供额外的动作,比如计算对象被引用的次数。
优点
- 代理模式能够提供对目标对象同样的方法。
- 安全性,尤其是对真实角色提供了一定的保护。
- 附加功能,比如进行参数检验,或者事务处理等。
缺点
- 由于在请求转发的过程中引入了代理对象,因此会有一定的请求延迟,增加系统复杂度。
代码
package main
import (
"fmt"
)
// Service 是一种服务,需要进行权限验证
type Service interface {
Request() string
}
// RealService 是实际的服务
type RealService struct{}
func (rs RealService) Request() string {
return "RealService: Handling request."
}
// ProxyService 是代理服务,进行额外的权限验证
type ProxyService struct {
service Service
owner string
}
// 创建一个代理服务
func NewProxyService(service Service, owner string) *ProxyService {
return &ProxyService{
service: service,
owner: owner,
}
}
// 代理请求,包括权限验证
func (ps ProxyService) Request() string {
if ps.checkAccess() {
result := ps.service.Request()
ps.logAccess()
return result
}
return ""
}
// 检查访问权限
func (ps ProxyService) checkAccess() bool {
fmt.Println("ProxyService: Checking access permissions.")
// 模拟权限验证,此处我们假设只有 "Admin" 拥有权限
return ps.owner == "Admin"
}
// 记录访问
func (ps ProxyService) logAccess() {
fmt.Println("ProxyService: Logging the successful request.")
}
func main() {
realService := &RealService{}
proxyService := NewProxyService(realService, "Admin")
fmt.Println(proxyService.Request())
}
在这个例子中,RealService
是一个真实的服务,而ProxyService
是对RealService
的一个代理,ProxyService
在调用RealService
的方法前先进行一些准备工作,如权限验证和日志记录等。这样的设计有效的解老了控制对原始对象的访问的需求,并且可以在不影响原始对象的情况下添加一些额外的功能。
模板方法模式 (Template Method Pattern)
模板方法模式是一种行为型设计模式,它在超类中定义算法的模板,允许子类为一个或多个步骤提供自己的实现方式。模板方法定义了一个算法的步骤,将某些(或全部)步骤延迟到子类中。这比让子类定义整个算法更灵活。
模板方法模式建议将算法分解为一系列步骤, 然后将这些步骤改写为方法, 最后在 “模板方法” 中依次调用这些方法。 步骤可以是 抽象
的, 也可以有一些默认的实现。 为了能够使用算法, 客户端需要自行提供子类并实现所有的抽象步骤。 如有必要还需重写一些步骤 (但这一步中不包括模板方法自身)。
适用场景
- 一次性实现一个算法的不变部分,并将可变件留给子类去实现。
- 各子类中公共的行为被提取出来并集中到一个公共父类中,从而避免代码重复。
- 控制子类扩展。模板方法只在特定点调用“hook(钩子)”操作,这样就只允许在这些点进行扩展。
优点
- 遵循开放封闭原则,更换子类可以改变算法的行为。
- 简化了算法的实现,子类实现具体的方法,但不需要关心算法中的固定部分。
- 必须保证使用相同的算法步骤。在父类中实现算法可以确保每个子类都使用了相同的算法步骤。
- 提供了继承以外的定制选项。
缺点
- 如果算法的变化点过多,可能会导致子类的数目增加,不利于系统的维护和扩展。
- 模板方法必须是确定的,如果算法中某个步骤的执行是由子类决定的,那么必须在父类中定义“hook”(钩子),这可能会导致虚方法数量的增加,从而增加系统设计的复杂度。
示例
package main
import "fmt"
// LunchCooker 是烹饪午餐的接口
type LunchCooker interface {
CookRice()
CookSoup()
}
// BreakfastCooker 是烹饪早餐的接口
type BreakfastCooker interface {
CookEgg()
CookBread()
}
// Food 接口显式声明了模板方法
type Food interface {
Cook()
}
// Lunch 实现了 Food 接口
type Lunch struct {
c LunchCooker
}
func NewLunch() *Lunch {
return &Lunch{}
}
// Lunch 实现模板方法 Cook()
func (l *Lunch) Cook() {
fmt.Println("烹饪午餐")
l.c.CookRice()
l.c.CookSoup()
fmt.Println("午餐准备好了!\n")
}
// Breakfast 实现了 Food 接口
type Breakfast struct {
c BreakfastCooker
}
func NewBreakfast() *Breakfast {
return &Breakfast{}
}
// Breakfast 实现模板方法 Cook()
func (b *Breakfast) Cook() {
fmt.Println("烹饪早餐")
b.c.CookEgg()
b.c.CookBread()
fmt.Println("早餐准备好了!\n")
}
// Cooker 是实现 LunchCooker 和 BreakfastCooker 接口的一个结构体
type Cooker struct{}
func (c *Cooker) CookRice() {
fmt.Println("制作米饭")
}
func (c *Cooker) CookSoup() {
fmt.Println("煮汤")
}
func (c *Cooker) CookEgg() {
fmt.Println("煎鸡蛋")
}
func (c *Cooker) CookBread() {
fmt.Println("烤面包")
}
func main() {
cooker := &Cooker{}
// 早餐
breakfast := NewBreakfast()
breakfast.c = cooker
breakfast.Cook()
// 午餐
lunch := NewLunch()
lunch.c = cooker
lunch.Cook()
}
在这个示例中,LunchCooker
和 BreakfastCooker
接口是两个可以用于 Cooker 的接口。Food
接口定义了模板方法 Cook()
。Lunch
和 Breakfast
为 Food
接口提供了具体实现。Cooker
实现了 LunchCooker
和 BreakfastCooker
接口的方法,那么它就是 Lunch
和 Breakfast
所需要的对象。Lunch
和 Breakfast
在实现 Cook()
的时候,先打印烹饪的开始信息,再依次调用 LunchCooker
或 BreakfastCooker
的方法,最后输出烹饪的成功信息。
这个例子中可以看出,模板方法模式提供了一种定义算法的框架,以允许子类为其中一个或多个步骤提供自己的实现方式,从而使得算法的工作方式更具模块化和可维护性,并且灵活性更高。
策略模式 (Strategy Pattern)
策略模式是一种行为设计模式, 它能让你定义一系列算法, 并将每种算法分别放入独立的类中, 以使算法的对象能够相互替换。
适用场景
- 有多个算法实现可以被使用,可以使用某个标识来优先选择使用哪个算法。
- 算法需要在运行时刻进行变化。
- 算法的实现需要被隐藏起来,不需要暴露给使用者。
优点
- 你可以在运行时切换对象内的算法。
- 你可以将算法的实现和使用算法的代码隔离开来。
- 你可以使用组合来代替继承。
- 开闭原则。 你无需对上下文进行修改就能够引入新的策略。
缺点
- 如果你的算法极少发生改变, 那么没有任何理由引入新的类和接口。 使用该模式只会让程序过于复杂。
- 客户端必须知晓策略间的不同——它需要选择合适的策略。
- 许多现代编程语言支持函数类型功能, 允许你在一组匿名函数中实现不同版本的算法。这样,你使用这些函数的方式就和使用策略对象时完全相同,无需借助额外的类和接口来保持代码简洁。
示例
package main
import "fmt"
// 商品结构体
type Product struct {
Price float64
}
// 计算总价格的策略接口
type PricingStrategy interface {
CalculatePrice(*Product) float64
}
// 固定折扣策略
type FixedDiscountStrategy struct {
FixedDiscount float64
}
func (fds *FixedDiscountStrategy) CalculatePrice(product *Product) float64 {
return product.Price - fds.FixedDiscount
}
// 百分比折扣策略
type PercentageDiscountStrategy struct {
PercentageDiscount float64
}
func (pds *PercentageDiscountStrategy) CalculatePrice(product *Product) float64 {
discount := product.Price * (pds.PercentageDiscount / 100)
return product.Price - discount
}
// 客户端代码
func main() {
product1 := &Product{Price: 100}
product2 := &Product{Price: 200}
// 固定折扣策略
fixedDiscount := &FixedDiscountStrategy{FixedDiscount: 10}
// 百分比折扣策略
percentageDiscount := &PercentageDiscountStrategy{PercentageDiscount: 20}
fmt.Println("商品1的总价:", fixedDiscount.CalculatePrice(product1))
fmt.Println("商品2的总价:", percentageDiscount.CalculatePrice(product2))
}
在这个示例中,Product
结构体表示商品,它有一个价格属性。FixedDiscountStrategy
实现了 PricingStrategy
接口,计算固定折扣的价格。同样,PercentageDiscountStrategy
实现了 PricingStrategy
接口,计算百分比折扣的价格。
客户端代码使用这些不同的策略来计算商品的总价格。在这个示例中,FixedDiscountStrategy
和 PercentageDiscountStrategy
是两个具体的策略,在实现 CalculatePrice()
方法时定义了不同的打折计算方法。客户端代码可以使用不同的策略,灵活地处理商品的价格,而不必改变其余的代码。这就是策略模式所要解决的问题。
观察者模式 (Observer Pattern)
观察者模式是一种行为设计模式, 允许你定义一种订阅机制, 可在对象事件发生时通知多个 “观察” 该对象的其他对象。
适用场景
- 当一个对象的状态变化,需要自动通知其他对象。
- 当一个对象需要监听其他对象的状态变化时。
优点
- 可以有效降低类之间的耦合度。
- 观察者和主题之间可以很容易的扩展和修改,因为它们之间没有太多的依赖关系。
- 实时性高,观察者可以及时获得状态变化的通知。
缺点
- 如果观察者对象较多,会导致性能瓶颈,影响程序效率。
- 观察者和主题之间的关系是双向的,如果处理不好容易出现循环依赖的情况。
示例
package main
import (
"fmt"
)
// 主题
type Subject interface {
Register(observer Observer)
Deregister(observer Observer)
Notify()
}
// 观察者
type Observer interface {
Update()
}
// SubjectImpl 实现了 Subject 接口
type SubjectImpl struct {
observers []Observer
}
// Register 方法用来注册观察者
func (s *SubjectImpl) Register(observer Observer) {
s.observers = append(s.observers, observer)
}
// Deregister 方法用来取消注册观察者
func (s *SubjectImpl) Deregister(observer Observer) {
for i, obs := range s.observers {
if obs == observer {
s.observers = append(s.observers[:i], s.observers[i+1:]...)
break
}
}
}
// Notify 方法用来通知观察者状态变化
func (s *SubjectImpl) Notify() {
for _, observer := range s.observers {
observer.Update()
}
}
// Observer1 是 Observer 接口的实现
type Observer1 struct{}
// Update 方法用来处理主题状态变化的通知
func (obs *Observer1) Update() {
fmt.Println("Observer1 收到了通知。")
}
// Observer2 是 Observer 接口的实现
type Observer2 struct{}
// Update 方法用来处理主题状态变化的通知
func (obs *Observer2) Update() {
fmt.Println("Observer2 收到了通知。")
}
func main() {
subject := &SubjectImpl{}
// 注册观察者1
observer1 := &Observer1{}
subject.Register(observer1)
// 注册观察者2
observer2 := &Observer2{}
subject.Register(observer2)
// 通知观察者
subject.Notify()
// 取消注册观察者2
subject.Deregister(observer2)
// 通知所有观察者
subject.Notify()
}
在这个例子中,主题和观察者分别被定义为 Subject
和 Observer
接口。主题 SubjectImpl
实现了主题接口,可以用来注册、注销和通知观察者。
观察者对象 Observer1
和 Observer2
都实现了 Observer
接口,并且处理主题对象状态变化的通知。在 main()
函数中,我们注册了两个观察者对象,并通知了所有注册的观察者通知。之后,我们通过 Deregister()
方法取消了其中一个观察者的注册,再次通知所有观
职责链模式 (Chain of Responsibility Pattern)
责任链模式是一种行为设计模式, 允许你将请求沿着处理者链进行发送。 收到请求后, 每个处理者均可对请求进行处理, 或将其传递给链上的下个处理者。
适用场景
- 处理请求的对象集合可以动态配置,或者不是固定的。
- 需要对请求进行处理的对象集合有时候需要改变顺序,或者需要动态改变处理的方式。
- 当请求的处理流程需要动态定义时。
优点
- 请求的发送者和接收者解耦。
- 可以随意添加、删除、修改请求处理对象。
- 可以实现复杂的请求处理流程。
缺点
- 对请求处理对象进行动态的调整比较困难。
- 不能保证请求一定会被处理。可能会出现所有请求对象都不能处理请求的情况。
示例
在电商平台上,完成某个订单的支付需要经过多个阶段的处理,例如订单校验、账户校验、库存校验、支付方式选择等。如果采用单个类去处理所有的支付状态,会增加类的复杂度并降低代码的可维护性。使用责任链模式可以将订单支付的每个阶段单独进行处理,在责任链上调用合适的阶段,以便逐步结束整个支付过程。
package main
import (
"fmt"
)
// IRequest 请求对象定义
type IRequest interface {
Validate() error
String() string
}
// Order 订单信息定义
type Order struct {
OrderId string
Amount float64
Description string
}
func (o *Order) Validate() error {
if o.Amount <= 0 {
return fmt.Errorf("订单金额不合法")
}
fmt.Println("校验订单信息通过")
return nil
}
func (o *Order) String() string {
return fmt.Sprintf("订单号:%s,支付金额:%0.2f,支付描述:%s", o.OrderId, o.Amount, o.Description)
}
// PaymentProcessor 支付处理器定义
type PaymentProcessor interface {
SetNext(PaymentProcessor) PaymentProcessor
Process(IRequest) error
}
// PaymentRequest 校验支付请求的信息
type PaymentRequest struct {
OrderInfo *Order
}
func (p *PaymentRequest) Validate() error {
if p.OrderInfo == nil {
return fmt.Errorf("订单信息为空")
}
return p.OrderInfo.Validate()
}
func (p *PaymentRequest) String() string {
return fmt.Sprintf("支付请求:%s", p.OrderInfo.String())
}
// PaymentValidator 订单校验支付处理器实现
type PaymentValidator struct {
next PaymentProcessor
}
func (p *PaymentValidator) SetNext(handler PaymentProcessor) PaymentProcessor {
p.next = handler
return handler
}
func (p *PaymentValidator) Process(request IRequest) error {
if err := request.Validate(); err != nil {
return err
}
fmt.Println("校验用户信息通过")
if p.next != nil {
return p.next.Process(request)
}
return nil
}
// PaymentAccountValidator 账户校验支付处理器实现
type PaymentAccountValidator struct {
next PaymentProcessor
}
func (p *PaymentAccountValidator) SetNext(handler PaymentProcessor) PaymentProcessor {
p.next = handler
return handler
}
func (p *PaymentAccountValidator) Process(request IRequest) error {
accountRequest, ok := request.(*PaymentRequest)
if !ok {
return fmt.Errorf("支付请求信息转换错误")
}
fmt.Println("校验账户信息通过")
if p.next != nil {
return p.next.Process(accountRequest)
}
return nil
}
// PaymentInventoryValidator 库存校验支付处理器实现
type PaymentInventoryValidator struct {
next PaymentProcessor
}
func (p *PaymentInventoryValidator) SetNext(handler PaymentProcessor) PaymentProcessor {
p.next = handler
return handler
}
func (p *PaymentInventoryValidator) Process(request IRequest) error {
inventoryRequest, ok := request.(*PaymentRequest)
if !ok {
return fmt.Errorf("支付请求信息转换错误")
}
fmt.Println("校验商品库存通过")
if p.next != nil {
return p.next.Process(inventoryRequest)
}
return nil
}
// PaymentMethodSelector 选择支付方式处理器实现
type PaymentMethodSelector struct {
next PaymentProcessor
}
func (p *PaymentMethodSelector) SetNext(handler PaymentProcessor) PaymentProcessor {
p.next = handler
return handler
}
func (p *PaymentMethodSelector) Process(request IRequest) error {
// 调用支付方式选择API
methodRequest, ok := request.(*PaymentRequest)
if !ok {
return fmt.Errorf("支付请求信息转换错误")
}
fmt.Println("选择支付方式通过")
fmt.Printf("支付成功:%s\n", methodRequest.String())
return nil
}
func main() {
order := &Order{
OrderId: "20220101001",
Amount: 100,
Description: "测试订单",
}
request := &PaymentRequest{
OrderInfo: order,
}
validator := &PaymentValidator{}
accountValidator := &PaymentAccountValidator{}
inventoryValidator := &PaymentInventoryValidator{}
methodSelector := &PaymentMethodSelector{}
validator.SetNext(accountValidator).SetNext(inventoryValidator).SetNext(methodSelector)
if err := validator.Process(request); err != nil {
fmt.Printf("支付失败:%s\n", err)
} else {
fmt.Println("支付成功")
}
}
在这个简单的示例中,我们将每个处理阶段抽象为具体的支付处理器实现。PaymentValidator
对象负责校验订单信息,PaymentAccountValidator
对象负责校验账户信息,PaymentInventoryValidator
对象负责校验库存信息,PaymentMethodSelector
对象负责根据不同的条件选择支付方式
迭代器模式 (Iterator Pattern)
迭代器模式是一种行为设计模式, 让你能在不暴露集合底层表现形式 (列表、 栈和树等) 的情况下遍历集合中所有的元素。
适用场景
- 需要遍历复杂的数据结构,并且在遍历时进行随机或条件访问。
- 数据结构可以是任何数据类型,如数组、列表或树等,并且不希望在遍历时暴露其内部实现。
优点
- 提供一种通用的方式来遍历聚合对象中的所有元素,不需要知道聚合对象的内部实现。
- 允许在不同的聚合对象上使用相同的遍历算法。
- 可以隐藏聚合对象的内部结构,使得代码更容易维护和扩展。
- 可以支持延迟加载,即仅在需要时才加载元素。
- 代码复用性高。
缺点
- 需要实现迭代器类,增加了代码的复杂度。
- 在某些情况下,使用迭代器模式可能会导致性能问题。
示例
假设我们正在开发一个音乐播放器应用程序。该应用程序需要提供一种遍历音乐库的方式,以查找特定歌曲。使用迭代器模式,我们可以将遍历逻辑从主要的播放器类中剥离出来,并将其封装在一个迭代器类中。
以下是一个基于链表实现的迭代器示例:
package main
import "fmt"
// MusicType 音乐类型
type MusicType int
const (
MusicTypeUnknown MusicType = iota
MusicTypeMP3
MusicTypeWAV
)
// Music 音乐信息
type Music struct {
name string
musicType MusicType
}
// MusicList 音乐列表
type MusicList struct {
head *musicNode
}
type musicNode struct {
data *Music
next *musicNode
}
// Iterator 迭代器接口
type Iterator interface {
HasNext() bool
Next() *Music
}
// MusicIterator 迭代器实现
type MusicIterator struct {
current *musicNode
}
func (i *MusicIterator) HasNext() bool {
return i.current != nil
}
func (i *MusicIterator) Next() *Music {
if i.current == nil {
return nil
}
result := i.current.data
i.current = i.current.next
return result
}
// Add 添加音乐
func (list *MusicList) Add(music *Music) {
node := &musicNode{data: music, next: nil}
if list.head == nil {
list.head = node
} else {
current := list.head
for current.next != nil {
current = current.next
}
current.next = node
}
}
// CreateIterator 创建迭代器
func (list *MusicList) CreateIterator() Iterator {
return &MusicIterator{current: list.head}
}
func main() {
list := &MusicList{}
list.Add(&Music{name: "歌曲1", musicType: MusicTypeMP3})
list.Add(&Music{name: "歌曲2", musicType: MusicTypeMP3})
list.Add(&Music{name: "歌曲3", musicType: MusicTypeWAV})
list.Add(&Music{name: "歌曲4", musicType: MusicTypeMP3})
iterator := list.CreateIterator()
for iterator.HasNext() {
music := iterator.Next()
fmt.Printf("歌曲名称:%s,类型:%d\n", music.name, music.musicType)
}
}
命令模式 (Command Pattern)
命令模式是一种行为设计模式, 它可将请求转换为一个包含与请求相关的所有信息的独立对象。 该转换让你能根据不同的请求将方法参数化、 延迟请求执行或将其放入队列中, 且能实现可撤销操作。
适用场景
- 在开发中,需要将请求发送者和接收者解耦,从而使其能够独立演化。
- 在开发中,需要通过将请求封装为对象的方式来实现日志记录、撤销和重做、队列等特性。
- 在开发中,需要支持事务性操作或批处理操作。
优点
- 请求发送者和接收者解耦,提高系统的灵活性和扩展性。
- 能够方便地添加新的命令和请求接收者,增加系统的适应性和可维护性。
- 能够很好地支持队列、日志、事务和撤销操作等特性。
- 代码复用性高。
缺点
- 命令模式的使用会增加整个系统的代码量,尤其是需要使用一个具有多个抽象方法的接口来实现一个具体的命令处理器时。
- 由于命令的封装,可以产生大量的具体命令类,这可能会导致类数目增多,对于简单的业务场景来说,使用命令模式可能会带来一定的过度设计。
示例
假设我们正在开发一个文本编辑器应用程序。该应用程序需要提供多种编辑功能,如打开、保存文档、剪切、复制、粘贴等,我们使用命令模式来实现这些功能。
package main
import (
"bufio"
"fmt"
"os"
)
// Document 文档
type Document struct {
content string
}
// EditorCmd 编辑命令接口
type EditorCmd interface {
Execute()
Undo()
}
// SaveCommand 保存命令
type SaveCommand struct {
document *Document
}
func NewSaveCommand(document *Document) EditorCmd {
return &SaveCommand{document: document}
}
func (c *SaveCommand) Execute() {
fmt.Print("请输入文件名:")
reader := bufio.NewReader(os.Stdin)
filename, _ := reader.ReadString('\n')
fmt.Printf("文本已保存到文件 %s 中。\n", filename[:len(filename)-1])
}
func (c *SaveCommand) Undo() {
fmt.Println("无法撤销保存命令。")
}
// CutCommand 剪切命令
type CutCommand struct {
document *Document
backupString string
startIndex int
endIndex int
}
func NewCutCommand(document *Document, startIndex, endIndex int) EditorCmd {
return &CutCommand{document: document, startIndex: startIndex, endIndex: endIndex}
}
func (c *CutCommand) Execute() {
c.backupString = c.document.content
c.document.content = c.document.content[:c.startIndex] + c.document.content[c.endIndex:]
}
func (c *CutCommand) Undo() {
c.document.content = c.backupString
}
// CopyCommand 复制命令
type CopyCommand struct {
document *Document
clipboard string
startIndex int
endIndex int
}
func NewCopyCommand(document *Document, startIndex, endIndex int) EditorCmd {
return &CopyCommand{document: document, startIndex: startIndex, endIndex: endIndex}
}
func (c *CopyCommand) Execute() {
c.clipboard = c.document.content[c.startIndex:c.endIndex]
}
func (c *CopyCommand) Undo() {
fmt.Println("无法撤销复制命令。")
}
// PasteCommand 粘贴命令
type PasteCommand struct {
document *Document
backupString string
startIndex int
}
func NewPasteCommand(document *Document, startIndex int, text string) EditorCmd {
return &PasteCommand{document: document, startIndex: startIndex, backupString: document.content}
}
func (c *PasteCommand) Execute() {
c.document.content = c.document.content[:c.startIndex] + c.backupString + c.document.content[c.startIndex:]
}
func (c *PasteCommand) Undo() {
c.document.content = c.backupString
}
// Editor 编辑器
type Editor struct {
document *Document
history []EditorCmd
}
func NewEditor() *Editor {
return &Editor{
document: &Document{},
history: []EditorCmd{},
}
}
func (e *Editor) ExecuteCommand(cmd EditorCmd) {
cmd.Execute()
e.history = append(e.history, cmd)
}
func (e *Editor) UndoLastCommand() {
cmdCount := len(e.history)
if cmdCount == 0 {
return
}
lastCmd := e.history[cmdCount-1]
lastCmd.Undo()
e.history = e.history[:cmdCount-1]
}
func (e *Editor) Print() {
fmt.Println("当前文本:")
fmt.Println(e.document.content)
}
func main() {
editor := NewEditor()
editor.ExecuteCommand(NewPasteCommand(editor.document, 0, "Hello World!"))
editor.ExecuteCommand(NewCopyCommand(editor.document, 0, 5))
editor.ExecuteCommand(NewCutCommand(editor.document, 0, 5))
editor.ExecuteCommand(NewSaveCommand(editor.document))
editor.UndoLastCommand()
editor.Print()
}
在这个示例中,我们首先定义了文档和命令接口 EditorCmd
。文档承载着我们编辑的文本,而 EditorCmd
接口定义了可以在文本上执行的命令。然后,我们为保存、剪切、复制、粘贴等编辑操作定义了具体的命令实现。每个命令都实现了 EditorCmd
接口,并包含 Execute()
和 Undo()
两个方法。 Execute()
方法用于执行操作,而 Undo()
方法用于撤销最后一个操作。最后,我们定义了 Editor
类来管理文档和命令历史记录,并提供撤销命令的能力。
在主函数中,我们首先创建了一个 Editor
实例,然后使用几个不同的命令(剪切、复制、粘贴等)修改文本。接下来,我们执行了保存命令,并撤销了最后一个操作。最后,我们打印出当前文本。
这个示例说明了命令模式的强大之处。通过命令模式,我们可以轻松地将不同的编辑功能(例如剪切、复制、粘贴等)封装为对象,并执行这些对象,以修改文档。同时,我们还可以轻松地撤销最后一个编辑操作,或者重新执行命令历史记录中的某个操作。
备忘录模式 (Memento Pattern)
备忘录模式是一种行为设计模式, 允许在不暴露对象实现细节的情况下保存和恢复对象之前的状态。
适用场景
- 当需要保存和恢复对象的状态时使用备忘录模式。
- 当需要实现撤销、重做操作时使用备忘录模式。
优点
- 可以保留对象的状态快照,以便稍后恢复其先前的状态。
- 将备忘录对象从原始对象分离,保持了底层实现的简单性。它不会增加额外的复杂性或降低性能。
- 由于备忘录和原始对象的分离,它确保了对状态的封装,该状态状态不能由客户端对象访问或修改。
- 可以改进应用程序的错误处理能力。
缺点
- 如果备忘录对象包含许多数据,则可能会影响性能。
- 在为客户端提供需要大量状态恢复的方法时,备忘录模式会增加一定的复杂性。
示例
假设我们正在开发一个简单的编辑器应用程序。在该应用程序中,我们需要实现“保存文件”和“撤销上次操作”等功能。我们可以使用备忘录模式来实现这些功能。
package main
import "fmt"
// Memento 备忘录接口,保存副本
type Memento interface {
GetState() string
}
// Editor 编辑器
type Editor struct {
State string
}
// CreateMemento 创建备忘录并保存副本
func (e *Editor) CreateMemento() Memento {
return &editorMemento{state: e.State}
}
// Restore 通过备忘录恢复状态
func (e *Editor) Restore(m Memento) {
e.State = m.GetState()
}
// editorMemento 备忘录实现
type editorMemento struct {
state string
}
// GetState 获取编辑器状态
func (m *editorMemento) GetState() string {
return m.state
}
// HistoryTracker 历史记录跟踪器
type HistoryTracker struct {
mementos []Memento
}
// Save 将当前状态添加到历史记录中
func (t *HistoryTracker) Save(m Memento) {
t.mementos = append(t.mementos, m)
}
// UndoLast 恢复最后一个状态历史记录
func (t *HistoryTracker) UndoLast(editor *Editor) {
count := len(t.mementos)
if count == 0 {
return
}
lastMemento := t.mementos[count-1]
editor.Restore(lastMemento)
t.mementos = t.mementos[:count-1]
fmt.Println("编辑器状态:", editor.State)
}
func main() {
editor := &Editor{}
editor.State = "行1\n行2\n行3"
fmt.Println("编辑器状态:", editor.State)
historyTracker := &HistoryTracker{}
historyTracker.Save(editor.CreateMemento())
editor.State = "行1\n行2\n行3\n行4"
fmt.Println("编辑器状态:", editor.State)
historyTracker.Save(editor.CreateMemento())
editor.State = "行1\n行2\n行3\n行4\n行5"
fmt.Println("编辑器状态:", editor.State)
historyTracker.UndoLast(editor)
historyTracker.UndoLast(editor)
}
在这个示例中,我们定义了 Editor
类来表示编辑器,并实现了两个方法 CreateMemento()
和 Restore()
来创建备忘录并从备忘
与命令模式的区别
命令模式可以将操作封装成对象,对一系列操作进行请求、排队和记录;而备忘录模式是对对象状态的保存和恢复,通过备忘录对象实现状态保存和恢复的操作
状态模式 (State Pattern)
状态模式是一种行为设计模式, 让你能在一个对象的内部状态变化时改变其行为, 使其看上去就像改变了自身所属的类一样。
适用场景
- 当对象的行为取决于它是什么状态时使用状态模式。
- 当对象需要根据当前状态修改它的操作/行为时使用状态模式。
- 当存在多个条件判断语句并且这些语句的行为取决于对象的状态时使用状态模式。
优点
- 单一职责原则。 将与特定状态相关的代码放在单独的类中。
- 开闭原则。 无需修改已有状态类和上下文就能引入新状态。
- 通过消除臃肿的状态机条件语句简化上下文代码。
缺点
- 状态模式可能会导致类的数量增加,尤其是在有多个状态和多个包含该状态的对象的情况下。
- 如果状态转换过程比较复杂,则使用状态模式可能会导致代码结构变得复杂。
示例
package main
import "fmt"
// PlayerState 多媒体播放器状态
type PlayerState interface {
play()
pause()
stop()
}
// PlayingState 播放状态
type PlayingState struct{}
func (p *PlayingState) play() {
fmt.Println("播放器已在播放...")
}
func (p *PlayingState) pause() {
fmt.Println("播放器已暂停...")
}
func (p *PlayingState) stop() {
fmt.Println("播放器已停止...")
}
// PausedState 暂停状态
type PausedState struct{}
func (p *PausedState) play() {
fmt.Println("播放器继续播放...")
}
func (p *PausedState) pause() {
fmt.Println("播放器已暂停...")
}
func (p *PausedState) stop() {
fmt.Println("播放器已停止...")
}
// StoppedState 停止状态
type StoppedState struct{}
func (s *StoppedState) play() {
fmt.Println("播放器开始播放...")
}
func (s *StoppedState) pause() {
fmt.Println("播放器当前未播放,无法暂停...")
}
func (s *StoppedState) stop() {
fmt.Println("播放器当前未播放,无法停止...")
}
// MediaPlayer 多媒体播放器
type MediaPlayer struct {
state PlayerState
}
// play 播放
func (m *MediaPlayer) play() {
m.state.play()
m.setState(&PlayingState{})
}
// pause 暂停
func (m *MediaPlayer) pause() {
m.state.pause()
m.setState(&PausedState{})
}
// stop 停止
func (m *MediaPlayer) stop() {
m.state.stop()
m.setState(&StoppedState{})
}
// setState 设置状态
func (m *MediaPlayer) setState(s PlayerState) {
m.state = s
}
func main() {
player := &MediaPlayer{}
player.setState(&StoppedState{})
player.play() // 输出: 播放器开始播放...
player.pause() // 输出: 播放器已暂停...
player.stop() // 输出: 播放器已停止...
player.pause() // 输出: 播放器当前未播放,无法暂停...
player.stop() // 输出: 播放器已停止...
player.play() // 输出: 播放器开始播放...
player.pause() // 输出: 播放器已暂停...
player.stop() // 输出: 播放器已停止...
}
在这个示例中,我们定义了 PlayerState
接口以及几个不同的状态类型(播放状态、暂停状态和停止状态)。然后,我们定义了 MediaPlayer
类,并在其中实现了 play()
、pause()
和 stop()
方法来控制播放器状态的切换,并更改状态。在主函数中,我们设置播放器状态,并演示每个方法调用的输出结果以说明状态模式的使用优势。
这个示例同样可以让我们看到状态模式如何让代码更加易于维护和扩展。我们可以轻松地添加新的播放状态,并在运行时动态地切换状态,从而让多媒体播放器更加灵活和多功能。同时,我们在这个示例中看到了状态模式所带来的另一个优势:可以清晰地划分代码,保持代码的结构简洁和易读。
访问者模式 (Visitor Pattern)
访问者模式是一种行为型设计模式,它允许在不更改对象结构的情况下向一组对象添加新的操作。简单来说,它是一种将算法与对象结构分离的方式,因此算法可以独立地附加到对象结构上。
适用场景
- 当一个对象结构包含许多不同类的对象,并且需要对这些对象执行多种类型的操作时使用访问者模式。
- 当添加新的操作可能导致在所有类中都添加新方法时使用访问者模式。
- 当一个对象结构相对稳定,但需要经常在该结构上定义新的操作时使用访问者模式。
优点
- 访问者模式将对对象实现某种功能的代码封装到一个访问者类中。这使得代码更加模块化,易于理解和维护。
- 可以将访问者之间的关注点分离,使其互相独立。这提高了代码的可重用性和灵活性。
- 访问者模式支持新增操作而无需更改对象结构。这使得该模式尤其适用于需要频繁添加新功能或操作的应用程序。
缺点
- 在添加新的元素时需要更改访问者接口,这可能会导致很多相关的修改。
- 访问者模式可能会导致复杂的调用链,从而使代码变得难以理解和维护。
示例
package main
import "fmt"
// Employee 员工接口
type Employee interface {
getSalary() int
accept(visitor Visitor)
}
// Developer 程序员
type Developer struct {
name string
salary int
}
func (d *Developer) getSalary() int {
return d.salary
}
func (d *Developer) accept(visitor Visitor) {
visitor.visitDeveloper(d)
}
// Manager 经理
type Manager struct {
name string
salary int
}
func (m *Manager) getSalary() int {
return m.salary
}
func (m *Manager) accept(visitor Visitor) {
visitor.visitManager(m)
}
// Visitor 访问者接口
type Visitor interface {
visitDeveloper(d *Developer)
visitManager(m *Manager)
}
// SalaryVisitor 计算员工薪水的访问者
type SalaryVisitor struct {
totalSalary int
count int
}
func (s *SalaryVisitor) visitDeveloper(d *Developer) {
s.totalSalary += d.getSalary()
s.count++
}
func (s *SalaryVisitor) visitManager(m *Manager) {
s.totalSalary += m.getSalary()
s.count++
}
// ReportGenerator 报表生成器
type ReportGenerator struct{}
// generateReport 生成报表
func (r *ReportGenerator) generateReport(employees []Employee) {
visitor := &SalaryVisitor{}
// 遍历所有员工,计算总薪水和员工的数量
for _, e := range employees {
e.accept(visitor)
}
// 计算平均薪水
averageSalary := float64(visitor.totalSalary / visitor.count)
fmt.Printf("员工总薪水为: %d, 平均薪水为: %.2f\n", visitor.totalSalary, averageSalary)
}
func main() {
dev1 := &Developer{"张三", 5000}
dev2 := &Developer{"李四", 6000}
manager := &Manager{"王五", 10000}
employees := []Employee{dev1, dev2, manager}
reportGenerator := &ReportGenerator{}
reportGenerator.generateReport(employees)
}
在这个示例中,我们定义了 Employee
接口以及几个不同的员工类型(程序员和经理)。我们定义了 Visitor
接口以及 SalaryVisitor
访问者,用于计算员工的薪水。我们还定义了 ReportGenerator
类,并在其中实现了 generateReport()
方法,用于生成员工的总薪水和平均薪水的报告。
在 generateReport()
方法中,我们创建一个 SalaryVisitor
访问者,并使用该访问者遍历所有员工,计算总薪水和员工的数量。然后,我们计算平均薪水。最后,我们输出总薪水和平均薪水的报告。
这个示例说明了访问者模式如何将与员工相关的功能封装在一个访问者中,从而使代码更易于维护和扩展。我们可以轻松地添加新的访问者,并在运行时动态地切换访问者,从而让这个系统更加灵活和多样化。
中介者模式 (Mediator Pattern)
中介者模式是一种行为型设计模式,它允许将对象之间的通信封装到一个中介者对象中。它可以将复杂的交互关系简化为中介者和对象之间的单向通讯,从而减少了对象之间的耦合度。
适应场景
- 当一个对象中有太多的直接依赖关系时,可以使用中介者模式来简化这些依赖关系并将它们转移到一个中介者对象中。
- 当一个应用程序中的对象之间存在复杂的相互交互时,可以使用中介者模式来统一管理这些交互并降低系统的复杂度。
优点
- 中介者模式可以将对象之间的通信封装在一个中介者对象中,从而降低了对象之间的依赖。
- 中介者模式可以简化对象之间的交互关系,使得代码更易于理解和维护。
- 中介者模式可以集中控制应用程序中的交互逻辑,从而使代码更加可维护和可扩展。
缺点
- 如果中介者对象过于庞大,可能会让代码难以理解和维护。
- 在某些情况下,在不同的对象之间传递消息可能会变得更加复杂,因为所有的信息都需要经过中介者对象。
示例
package main
import "fmt"
// Mediator 中介者接口
type Mediator interface {
openDialog()
setDialog(dialog *Dialog)
}
// Window 窗口组件
type Window struct {
mediator Mediator
}
// openDialog 打开对话框
func (w *Window) openDialog() {
fmt.Println("打开对话框...")
w.mediator.openDialog()
}
// setMediator 设置中介者对象
func (w *Window) setMediator(mediator Mediator) {
w.mediator = mediator
}
// Dialog 对话框组件
type Dialog struct {
mediator Mediator
}
// sendData 发送数据
func (d *Dialog) sendData() {
fmt.Println("发送数据...")
}
// closeDialog 关闭对话框
func (d *Dialog) closeDialog() {
fmt.Println("关闭对话框...")
}
// setMediator 设置中介者对象
func (d *Dialog) setMediator(mediator Mediator) {
d.mediator = mediator
}
// DialogMediator 对话框中介者对象
type DialogMediator struct {
window *Window
dialog *Dialog
isShown bool
}
// openDialog 打开对话框
func (m *DialogMediator) openDialog() {
if !m.isShown {
m.dialog.sendData()
m.dialog.setMediator(m)
m.isShown = true
}
}
// setDialog 设置对话框
func (m *DialogMediator) setDialog(dialog *Dialog) {
m.dialog = dialog
}
func main() {
window := &Window{}
dialog := &Dialog{}
mediator := &DialogMediator{window: window, isShown: false}
window.setMediator(mediator)
dialog.setMediator(mediator)
mediator.setDialog(dialog)
window.openDialog() // 输出: 打开对话框... 发送数据...
dialog.closeDialog() // 输出: 关闭对话框...
}
在这个示例中,我们定义了 Window
、Dialog
和 Mediator
接口。我们还实现了具有窗口和对话框组件的 DialogMediator
中介者类,并在其中实现了打开对话框和关闭对话框的逻辑。主函数中,我们创建了 Window
、Dialog
和 DialogMediator
对象,并将依赖关系设置在了中介者对象上。当我们调用 Window
的 openDialog
方法时,中介者对象会调用 Dialog
的 sendData
方法,然后在 Dialog
中打开对话框。然后我们关闭了对话框,并输出了相应的消息。
这个示例说明了如何使用中介者模式来简化两个组件之间的通信。它将窗口和对话框之间的复杂依赖从客户代码中删除,并将其封装在中介者对象中。这使得代码更灵活和易于维护,因为它可以轻松地添加或删除组件,而不会对现有代码产生影响。
解释器模式 (Interpreter Pattern)
在解释器模式中,我们定义了一个语言的语法及其解释器,在给定一段文本后,将文本解析成为解释器可以理解的结构体。解释器模式是一种行为型设计模式,它可以帮助我们设计复杂的文法和语言,并可以将其映射到一个解释器类上。
适应场景
- 当我们需要解析和分析语言文本的语法结构时,可以考虑使用解释器模式。
- 当我们需要创建一个像编程语言或查询语句这样的 DSL 时,可以使用解释器模式来实现。
优点
- 解释器模式可以在运行时生成语法树,从而动态地修改和扩展解释器的行为。
- 解释器模式可以将复杂的语法分解为简单的规则,并且使得语法规则更加易于理解和维护。
- 解释器模式将编译过程拆分成多个单独的模块,使得代码更加分离和可重用。
缺点
- 在解释器模式中,语法分析和执行过程是交织在一起的,这会导致解释器的设计变得更加复杂。
- 应用程序通常需要大量的解释器对象,这会占用大量的内存空间。
示例
以下是一个简单的示例,展示了如何使用解释器模式来解析和计算基本的算术表达式。在这个例子中,我们将支持基本的加法和减法运算符。
package main
import (
"fmt"
"strconv"
"strings"
)
// Expression 解释器接口
type Expression interface {
interpret() int
}
// NumberExpression 数字表达式
type NumberExpression struct {
value int
}
func (n *NumberExpression) interpret() int {
return n.value
}
// AdditionExpression 加法表达式
type AdditionExpression struct {
expr1 Expression
expr2 Expression
}
func (a *AdditionExpression) interpret() int {
return a.expr1.interpret() + a.expr2.interpret()
}
// SubtractionExpression 减法表达式
type SubtractionExpression struct {
expr1 Expression
expr2 Expression
}
func (s *SubtractionExpression) interpret() int {
return s.expr1.interpret() - s.expr2.interpret()
}
// Parser 解析器
type Parser struct {
expressions []Expression
}
// parse 分析表达式字符串
func (p *Parser) parse(input string) error {
tokens := strings.Split(input, " ")
for i := 0; i < len(tokens); i++ {
token := tokens[i]
if token == "+" {
expr1, err := p.pop()
if err != nil {
return err
}
expr2, err := p.pop()
if err != nil {
return err
}
additionExpr := &AdditionExpression{expr1: expr1, expr2: expr2}
p.push(additionExpr)
} else if token == "-" {
expr1, err := p.pop()
if err != nil {
return err
}
expr2, err := p.pop()
if err != nil {
return err
}
subtractionExpr := &SubtractionExpression{expr1: expr1, expr2: expr2}
p.push(subtractionExpr)
} else {
val, err := strconv.Atoi(token)
if err != nil {
return err
}
numExpr := &NumberExpression{value: val}
p.push(numExpr)
}
}
return nil
}
// pop 弹出栈顶表达式
func (p *Parser) pop() (Expression, error) {
if len(p.expressions) == 0 {
return nil, fmt.Errorf("表达式无效")
}
expr := p.expressions[len(p.expressions)-1]
p.expressions = p.expressions[:len(p.expressions)-1]
return expr, nil
}
// push 将表达式推入栈中
func (p *Parser) push(expr Expression) {
p.expressions = append(p.expressions, expr)
}
func main() {
expression := "1 2 + 3 -"
parser := &Parser{}
err := parser.parse(expression)
if err != nil {
fmt.Println(err)
}
result := parser.expressions[0].interpret()
fmt.Printf("解析表达式: %s 结果为: %d\n", expression, result)
}
在这个示例中,我们定义了 Expression
接口以及三个不同类型的表达式:NumberExpression
、AdditionExpression
和 SubtractionExpression
。我们还定义了一个 Parser
类,用于解析输入的表达式,并将其转换为一个 Expression 对象。
在主函数中,我们定义了一个简单的表达式 “1 2 + 3 -“,并创建了一个 Parser
对象,然后我们调用 parse
方法将表达式解析为 Expression
对象,并计算出表达式的值。最后,我们输出表达式和计算结果。
这个示例说明了使用解释器模式来解析和计算简单的算术表达式。它将算法表达式分解成简单的规则,并使用接口将其组合在一个解释器类中。这使得代码更加简单、清晰和易于维护,并且可以轻松地扩展到支持更多类型的算法表达式。