设计模式概要介绍

大家好,这里是编程Cookbook。本文是对设计模式的概要介绍,包括 23 种设计模式和一些设计原则。
优秀代码的特点
我们所说的优秀代码通常具有以下特点:
- 可靠性、安全性:功能正确、运行稳定、确保安全。
- 性能优化:时间复杂度优、空间复杂度优、资源利用高效。
- 可维护性、可拓展性:低耦合高内聚、遵循设计原则和模式、易于修改扩展。
- 可读性:命名规范、结构清晰、注释适当。
- 可测试性:单元测试友好、可模拟隔离。
- 兼容性:跨平台兼容、版本兼容。
设计模式基础概念
设计模式及其作用
什么是设计模式?
设计模式是解决软件设计中常见问题的经验总结,它提供了一套经过验证的、可重用的解决方案模板。设计模式并不是具体的代码,而是一种设计思想或方法论,用于指导开发者编写高质量、可维护、可扩展的代码。
设计模式的作用
帮助我们写出更好的代码:
- 提高代码复用性:通过提供通用的解决方案,避免重复造轮子。
- 提升代码可维护性:使代码结构清晰,易于理解和修改。
- 增强代码扩展性:通过松耦合的设计,方便未来功能的扩展。
- 促进团队协作:设计模式是通用的设计语言,便于团队成员之间的沟通和理解。
- 解决特定问题:针对常见的设计问题(如对象创建、接口适配、行为管理等),提供标准化的解决方案。
一句话概括
设计模式:
- 软件设计中常见问题的可重用解决方案模板,用于提高代码的复用性、可维护性和扩展性。
为什么要用设计模式?
- 因为设计模式能帮助我们写出更好的代码。
简要概括23种设计模式
1. 创建型模式(Creational Patterns)
目的
灵活控制对象创建过程,解耦对象的创建与使用。
常用模式
单例模式(Singleton Pattern)
- 定义:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
- 应用场景:适用于
资源管理类
,如数据库连接池、线程池,确保整个应用程序中只存在一个实例,避免资源浪费和冲突;也常用于配置文件读取类
,保证配置信息读取的一致性,Go 语言通过sync.Once
保证只执行一次。- 数据库连接池(全局唯一的数据库连接)
- 配置管理(全局共享的配置)
- 日志系统(单一实例,避免多次创建对象)
package main
import (
"fmt"
"sync"
)
type Singleton struct{}
var instance *Singleton
var once sync.Once
func GetInstance() *Singleton {
once.Do(func() { // 确保只执行一次
instance = &Singleton{}
})
return instance
}
func main() {
s1 := GetInstance()
s2 := GetInstance()
fmt.Println(s1 == s2) // true,说明是同一个实例
}
工厂模式(Factory Pattern)
- 定义:定义
一个
创建对象的接口,由子类决定实例化哪个类。 - 应用场景:在对象创建过程复杂,或者需要
根据不同条件创建不同类型对象时使用
,比如游戏开发中创建不同类型角色,或图形绘制系统中创建不同形状图形。- 数据库连接工厂(支持 MySQL、PostgreSQL、SQLite)
- 支付系统(不同支付方式,如支付宝、微信支付)
- 日志系统(不同日志存储方式,如本地文件、数据库、云存储)
package main
import "fmt"
// Logger 接口
type Logger interface {
Log(message string)
}
// 文件日志
type FileLogger struct{}
func (f *FileLogger) Log(message string) {
fmt.Println("FileLogger:", message)
}
// 数据库日志
type DatabaseLogger struct{}
func (d *DatabaseLogger) Log(message string) {
fmt.Println("DatabaseLogger:", message)
}
// 工厂方法
func GetLogger(loggerType string) Logger {
switch loggerType {
case "file":
return &FileLogger{}
case "database":
return &DatabaseLogger{}
default:
return nil
}
}
func main() {
logger := GetLogger("file")
logger.Log("Application started") // 输出:FileLogger: Application started
}
抽象工厂模式(Abstract Factory Pattern)
-
定义:创建一组相关的对象,而不是一个单独的对象,避免使用具体的类,让工厂决定创建哪些对象。
模式 主要特点 适用场景 工厂模式 提供一个方法,根据传入的参数返回特定类型的对象 当对象创建逻辑比较简单(创建单一产品,如 Logger
),且只需要一个工厂方法时抽象工厂模式 提供一个工厂接口,每个具体工厂负责 创建一组相关的对象
当需要创建多个相关或依赖的对象(创建一组相关产品,如 Button
、TextBox
等),且不希望直接依赖具体类时 -
应用场景:你需要开发一个跨平台的 UI 库,支持不同操作系统(如 Windows、Mac)的按钮和文本框。如果工厂模式是生产单个产品(比如创建一个按钮),那么抽象工厂模式是创建一整套产品(比如创建整个 UI 组件库)。
- 跨平台 UI 组件(Windows 按钮 vs Mac 按钮)
- 数据库连接池(不同数据库的 SQL 语法不同)
package main
import "fmt"
// 1. 数据库连接池接口
type DBConnectionPool interface {
Connect() string // 模拟连接数据库
}
// 2. 抽象工厂接口
type DatabaseFactory interface {
CreateConnectionPool() DBConnectionPool // 创建数据库连接池
}
// 3. MySQL 连接池实现
type MySQLConnectionPool struct{}
func (m *MySQLConnectionPool) Connect() string {
return "Connecting to MySQL Database"
}
// 4. PostgreSQL 连接池实现
type PostgreSQLConnectionPool struct{}
func (p *PostgreSQLConnectionPool) Connect() string {
return "Connecting to PostgreSQL Database"
}
// 5. MySQL 工厂实现
type MySQLFactory struct{}
func (m *MySQLFactory) CreateConnectionPool() DBConnectionPool {
return &MySQLConnectionPool{}
}
// 6. PostgreSQL 工厂实现
type PostgreSQLFactory struct{}
func (p *PostgreSQLFactory) CreateConnectionPool() DBConnectionPool {
return &PostgreSQLConnectionPool{}
}
// 7. 客户端代码
func main() {
var factory DatabaseFactory
// 假设根据某些条件选择数据库类型
// 选择 MySQL 工厂
factory = &MySQLFactory{}
mysqlPool := factory.CreateConnectionPool()
fmt.Println(mysqlPool.Connect()) // 输出:Connecting to MySQL Database
// 选择 PostgreSQL 工厂
factory = &PostgreSQLFactory{}
postgresPool := factory.CreateConnectionPool()
fmt.Println(postgresPool.Connect()) // 输出:Connecting to PostgreSQL Database
}
解释:DatabaseFactory
作为一个统一的接口,定义了创建相应数据库连接池的标准方法。通过这一接口,客户端能够根据不同的数据库类型灵活地选择和扩展所需的连接池工厂。在 main
函数中,我们使用 DatabaseFactory
类型的变量来选择具体的工厂实现(如 MySQLFactory
或 PostgreSQLFactory
)。每个工厂类(例如 MySQLFactory
和 PostgreSQLFactory
)都是工厂模式的具体实现,负责创建相应数据库的连接池,从而实现了数据库连接池的灵活切换和扩展。
建造者模式(Builder Pattern)
- 定义:将复杂对象的
构建
(对象包含的属性)与其表示
(最终对象的属性具体类型)分离,使同样的构建过程能创建不同表示。 - 应用场景:用于创建复杂对象,且
对象有多个可选部件或步骤
的场景,例如电脑组装,可根据不同需求组装不同配置电脑;房屋建造,按不同设计要求建造不同房屋。- 创建复杂的营销活动
package main
import "fmt"
// 定义营销活动的结构——最终要构建的产品
type MarketingCampaign struct {
Name string
Discount float64
Channel string
Budget float64
StartDate string
EndDate string
}
// 定义营销活动的构建者接口
type CampaignBuilder interface {
SetName(name string) CampaignBuilder
SetDiscount(discount float64) CampaignBuilder
SetChannel(channel string) CampaignBuilder
SetBudget(budget float64) CampaignBuilder
SetStartDate(startDate string) CampaignBuilder
SetEndDate(endDate string) CampaignBuilder
Build() *MarketingCampaign
}
// 具体的建造者实现
type ConcreteCampaignBuilder struct {
campaign MarketingCampaign
}
func (b *ConcreteCampaignBuilder) SetName(name string) CampaignBuilder {
b.campaign.Name = name
return b
}
func (b *ConcreteCampaignBuilder) SetDiscount(discount float64) CampaignBuilder {
b.campaign.Discount = discount
return b
}
func (b *ConcreteCampaignBuilder) SetChannel(channel string) CampaignBuilder {
b.campaign.Channel = channel
return b
}
func (b *ConcreteCampaignBuilder) SetBudget(budget float64) CampaignBuilder {
b.campaign.Budget = budget
return b
}
func (b *ConcreteCampaignBuilder) SetStartDate(startDate string) CampaignBuilder {
b.campaign.StartDate = startDate
return b
}
func (b *ConcreteCampaignBuilder) SetEndDate(endDate string) CampaignBuilder {
b.campaign.EndDate = endDate
return b
}
func (b *ConcreteCampaignBuilder) Build() *MarketingCampaign {
return &b.campaign
}
// 客户端使用建造者来构建一个复杂的营销活动
func main() {
builder := &ConcreteCampaignBuilder{}
campaign := builder.SetName("Spring Sale").
SetDiscount(15.0).
SetChannel("Social Media").
SetBudget(5000).
SetStartDate("2025-04-01").
SetEndDate("2025-04-30").
Build()
fmt.Printf("Marketing Campaign Details: %+v\n", campaign)
}
不常用模式
原型模式(Prototype Pattern)
- 定义:用原型实例指定创建对象的种类,通过拷贝原型创建新对象。
- 应用场景:适用于创建成本大(如初始化时间长、占用资源多)的对象,且对象状态初始化后变化不大的场景,如在图形绘制系统创建大量相似图形,或频繁创建复杂初始化参数的机器学习模型对象。
2. 结构型模式(Structural Patterns)
目的
组合类或对象以形成更大结构,通过组合扩展功能。
常用模式
适配器模式(Adapter Pattern)
- 定义:将一个类的接口
转换成
客户希望的另一个接口,使原本接口不兼容的类能一起工作。 - 应用场景:版本更新且需要兼容旧版本时使用。需要复用现存类,但接口与复用环境不兼容时使用,比如使用不同厂商的数据库接口,通过适配器统一接口;或者在已有系统中集成新的第三方库,该库接口与现有系统不匹配时。
- API 兼容性(适配不同的第三方 API)
- 日志适配(适配不同日志系统,如 log4j、zap)
- 数据库适配(兼容不同的数据库驱动)
- 优势:避免修改现有代码(开闭原则);实现向后兼容(不更新之前的代码,后面的代码使用新的版本)
package main
import "fmt"
// 旧接口
type OldDB struct{}
func (o *OldDB) Query() string {
return "Querying using OldDB"
}
// 适配器
type DBAdapter struct {
oldDB *OldDB
}
func (a *DBAdapter) ExecuteQuery() string {
return a.oldDB.Query()
}
// 客户端代码
func main() {
oldDB := &OldDB{}
adapter := &DBAdapter{oldDB: oldDB}
fmt.Println(adapter.ExecuteQuery()) // Querying using OldDB
}
代理模式(Proxy Pattern)
- 定义:为其他对象提供代理以控制对该对象的访问。
- 应用场景:适用于远程代理(代表远程对象)、虚拟代理(在真正对象创建前占位)、保护代理(控制对象访问权限)等场景,如网络图片加载,先使用代理图片占位,真实图片加载后替换;或在系统中对敏感资源访问进行权限控制。
- 权限控制(在 API 请求前检查权限)
- 缓存代理(Redis 缓存数据库查询结果)
- 远程代理(封装 RPC 或 HTTP 请求)
package main
import (
"fmt"
)
// 用户接口
type User interface {
HasPermission() bool
}
// 真实用户:具有权限的用户
type RealUser struct {
name string
}
func (u *RealUser) HasPermission() bool {
// 假设只有管理员有权限
return u.name == "admin"
}
// 代理用户:用于权限检查
type UserProxy struct {
realUser *RealUser
}
func (p *UserProxy) HasPermission() bool {
// 代理检查权限
if p.realUser.HasPermission() {
fmt.Println("Permission granted.")
return true
}
fmt.Println("Permission denied.")
return false
}
func main() {
// 创建真实用户和代理用户
realUser := &RealUser{name: "admin"}
proxy := &UserProxy{realUser: realUser}
// 请求权限
proxy.HasPermission() // 输出:Permission granted.
// 创建非管理员用户
realUser2 := &RealUser{name: "guest"}
proxy2 := &UserProxy{realUser: realUser2}
proxy2.HasPermission() // 输出:Permission denied.
}
装饰器模式(Decorator Pattern)
- 定义:动态给对象添加额外职责,相比继承更灵活。
- 应用场景:在不改变现有对象结构的情况下,给对象动态添加功能,例如给咖啡添加不同配料(糖、牛奶等);在Web开发中,为Servlet添加不同功能的过滤器。
- 动态添加功能(如新增触达方法、日志增强、权限校验)
package main
import "fmt"
type Notifier interface {
Send(message string)
}
type EmailNotifier struct{}
func (e *EmailNotifier) Send(message string) {
fmt.Println("Sending Email:", message)
}
// 装饰器
type SMSDecorator struct {
notifier Notifier
}
func (s *SMSDecorator) Send(message string) {
s.notifier.Send(message)
fmt.Println("Sending SMS:", message)
}
func main() {
email := &EmailNotifier{}
notifier := &SMSDecorator{notifier: email}
notifier.Send("Hello!") // 先发送邮件,再发送短信
}
解释:SMSDecorator
装饰了原始的 EmailNotifier
,使得 EmailNotifier
在原有功能的基础上增加了发送短信的功能,而不需要修改原始的 EmailNotifier
类(符合开闭原则)。
桥接模式(Bridge Pattern)
- 定义:将抽象部分与实现部分分离,使它们能独立变化。
- 应用场景:当有多个维度变化时适用,例如,不同图形(圆形、矩形等)和不同颜色(红色、蓝色等)组合,通过桥接模式可独立扩展图形和颜色,避免在多个维度变化时类数量呈指数级增长。
组合模式(Composite Pattern)
- 定义:将对象组合成树形结构以表示“部分 - 整体”层次结构,使用户对单个对象和组合对象的使用具有一致性。
- 应用场景:常用于处理树形结构数据,如文件系统(文件和文件夹)、组织机构(部门和员工)等,方便对整体和部分进行统一操作。
不常用模式
外观模式(Facade Pattern)
- 定义:为子系统中的一组接口提供一致界面,定义高层接口使子系统更易使用。
- 应用场景:当需要简化复杂系统调用接口时使用,例如操作电脑硬件(CPU、硬盘、内存等),通过操作系统外观类统一操作,然而,在现代软件开发中,依赖注入等技术在一定程度上替代了外观模式的功能,所以使用频率相对较低。
享元模式(Flyweight Pattern)
- 定义:运用共享技术有效支持大量细粒度对象。
- 应用场景:适用于系统存在大量相似对象时,如游戏中的树木、棋子等对象,通过共享相同状态减少对象数量,但由于其实现复杂,且在现代硬件资源相对充足的情况下,性能提升不明显,所以使用较少。
3. 行为型模式(Behavioral Patterns)
目的
管理对象间的相互协作,以完成单个对象无法单独实现的任务。
常用模式
观察者模式(Observer Pattern)
- 定义:定义
一对多依赖关系
,当主题对象状态变化时,通知所有观察者对象自动更新状态。 - 应用场景:广泛应用于消息推送(发布者 - 订阅者模式)、股票行情监测等场景,当一个对象状态改变需通知其他多个对象时使用。
- 事件驱动系统(监听用户行为,如点击、登录)
- 消息推送(当新文章发布时,自动通知订阅者)
- 监控系统(监控数据变更,触发告警)
package main
import "fmt"
// 观察者接口
type Observer interface {
Update(message string)
}
// 发布者
type Publisher struct {
subscribers []Observer
}
func (p *Publisher) Subscribe(o Observer) {
p.subscribers = append(p.subscribers, o)
}
func (p *Publisher) Notify(message string) {
for _, subscriber := range p.subscribers {
subscriber.Update(message)
}
}
// 具体观察者:邮件订阅者
type EmailSubscriber struct{}
func (e *EmailSubscriber) Update(message string) {
fmt.Println("Sending email:", message)
}
// 具体观察者:短信订阅者
type SMSSubscriber struct{}
func (s *SMSSubscriber) Update(message string) {
fmt.Println("Sending SMS:", message)
}
func main() {
publisher := &Publisher{}
emailSub := &EmailSubscriber{}
smsSub := &SMSSubscriber{}
publisher.Subscribe(emailSub)
publisher.Subscribe(smsSub)
publisher.Notify("New blog post published!")
}
责任链模式(Chain of Responsibility Pattern)
- 定义:为解除请求的发送者和接收者之间耦合,使多个对象都有机会处理请求,将这些对象连成一条链,并沿着链传递请求,直到有对象处理它为止。
- 应用场景:适用于处理流程不确定,可能由多个对象中的一个或多个来处理请求的场景,例如请假审批流程,可能由组长、经理、总监等不同级别人员审批,请求在职责链上传递,直到被处理。
- 请求处理流程(如 HTTP 请求处理中,多个中间件处理请求)
- 日志记录系统(不同级别的日志处理,如 DEBUG、INFO、ERROR)
- 审批流程(不同级别的审批,如经理审批、总监审批)
- 权限验证(多级权限验证,依次验证用户权限)
package main
import "fmt"
// 处理器接口
type Logger interface {
HandleRequest(level, message string)
SetNext(logger Logger)
}
// 基础的日志处理器
type BaseLogger struct {
next Logger
}
func (b *BaseLogger) SetNext(logger Logger) {
b.next = logger
}
func (b *BaseLogger) HandleRequest(level, message string) {
if b.next != nil {
b.next.HandleRequest(level, message)
}
}
// 具体日志处理器:DEBUG级别日志
type DebugLogger struct {
BaseLogger
}
func (d *DebugLogger) HandleRequest(level, message string) {
if level == "DEBUG" {
fmt.Println("DEBUG: ", message)
} else if d.next != nil {
d.next.HandleRequest(level, message)
}
}
// 具体日志处理器:INFO级别日志
type InfoLogger struct {
BaseLogger
}
func (i *InfoLogger) HandleRequest(level, message string) {
if level == "INFO" {
fmt.Println("INFO: ", message)
} else if i.next != nil {
i.next.HandleRequest(level, message)
}
}
// 具体日志处理器:ERROR级别日志
type ErrorLogger struct {
BaseLogger
}
func (e *ErrorLogger) HandleRequest(level, message string) {
if level == "ERROR" {
fmt.Println("ERROR: ", message)
} else if e.next != nil {
e.next.HandleRequest(level, message)
}
}
func main() {
// 创建责任链
debugLogger := &DebugLogger{}
infoLogger := &InfoLogger{}
errorLogger := &ErrorLogger{}
// 设置责任链
debugLogger.SetNext(infoLogger)
infoLogger.SetNext(errorLogger)
// 测试:传递不同的日志级别
debugLogger.HandleRequest("DEBUG", "This is a debug message")
debugLogger.HandleRequest("INFO", "This is an info message")
debugLogger.HandleRequest("ERROR", "This is an error message")
}
解释:创建一个日志处理链,处理不同级别的日志(如 DEBUG、INFO、ERROR)。每个日志处理器将处理一个日志消息,或将其传递给下一个处理器。
package main
import "fmt"
// 审批请求
type Request struct {
Amount float64
Reason string
}
// 处理器接口
type Approver interface {
HandleRequest(request Request)
SetNext(approver Approver)
}
// 基础审批人(包含下一个审批人)
type BaseApprover struct {
next Approver
}
func (b *BaseApprover) SetNext(approver Approver) {
b.next = approver
}
func (b *BaseApprover) HandleRequest(request Request) {
if b.next != nil {
b.next.HandleRequest(request)
}
}
// 经理审批
type Manager struct {
BaseApprover
}
func (m *Manager) HandleRequest(request Request) {
if request.Amount <= 1000 {
fmt.Printf("Manager approves request: %.2f, Reason: %s\n", request.Amount, request.Reason)
} else if m.next != nil {
m.next.HandleRequest(request)
}
}
// 总监审批
type Director struct {
BaseApprover
}
func (d *Director) HandleRequest(request Request) {
if request.Amount > 1000 && request.Amount <= 5000 {
fmt.Printf("Director approves request: %.2f, Reason: %s\n", request.Amount, request.Reason)
} else if d.next != nil {
d.next.HandleRequest(request)
}
}
// CEO审批
type CEO struct {
BaseApprover
}
func (c *CEO) HandleRequest(request Request) {
if request.Amount > 5000 {
fmt.Printf("CEO approves request: %.2f, Reason: %s\n", request.Amount, request.Reason)
} else if c.next != nil {
c.next.HandleRequest(request)
}
}
func main() {
// 创建审批链
manager := &Manager{}
director := &Director{}
ceo := &CEO{}
// 设置责任链
manager.SetNext(director)
director.SetNext(ceo)
// 测试:不同金额的审批请求
request1 := Request{Amount: 500, Reason: "Office supplies"}
manager.HandleRequest(request1)
request2 := Request{Amount: 2000, Reason: "Conference fee"}
manager.HandleRequest(request2)
request3 := Request{Amount: 6000, Reason: "Equipment purchase"}
manager.HandleRequest(request3)
}
解释:创建一个审批流程链,处理不同金额的审批请求,依次通过经理、总监和 CEO 审批。
策略模式(Strategy Pattern)
- 定义:定义一系列算法,将每个算法封装起来,使它们可以相互替换,且算法的变化不会影响到使用算法的客户。
- 应用场景:在一个系统中,某个行为有多种实现方式,且在运行时根据
不同条件
选择不同实现方式
的场景,比如电商系统中不同的促销策略(满减、折扣、赠品等),在订单结算时根据订单条件选择合适的策略。- 支付系统(不同支付方式)
- 排序算法(快速排序、冒泡排序等)
- 日志格式化(支持 JSON、XML、普通文本格式)
package main
import "fmt"
// 策略接口,定义支付行为
type PaymentStrategy interface {
Pay(amount float64)
}
// 具体策略:PayPal 支付
type PayPalPayment struct{}
func (p *PayPalPayment) Pay(amount float64) {
fmt.Printf("Paying %.2f using PayPal\n", amount)
}
// 具体策略:信用卡支付
type CreditCardPayment struct{}
func (c *CreditCardPayment) Pay(amount float64) {
fmt.Printf("Paying %.2f using Credit Card\n", amount)
}
// 上下文类,持有一个策略对象
type PaymentContext struct {
strategy PaymentStrategy
}
// 设置策略
func (pc *PaymentContext) SetStrategy(strategy PaymentStrategy) {
pc.strategy = strategy
}
// 执行支付
func (pc *PaymentContext) ExecutePayment(amount float64) {
pc.strategy.Pay(amount)
}
func main() {
// 创建上下文对象
context := &PaymentContext{}
// 设置 PayPal 策略并执行支付
context.SetStrategy(&PayPalPayment{})
context.ExecutePayment(100.0) // 输出:Paying 100.00 using PayPal
// 设置信用卡支付策略并执行支付
context.SetStrategy(&CreditCardPayment{})
context.ExecutePayment(200.0) // 输出:Paying 200.00 using Credit Card
}
解释:实现一个支付系统,支持多种支付方式(如 PayPal 和信用卡支付)
模板方法模式(Template Method Pattern)
- 定义:定义操作算法骨架,将部分步骤延迟到子类实现,子类可在不改变算法结构的前提下重定义某些特定步骤。
- 应用场景:多个子类有相同方法,但某些步骤实现有差异时使用,例如冲泡不同类型的茶和咖啡(都有烧水、冲泡、倒入杯子等步骤,但冲泡材料不同)。
状态模式(State Pattern)
- 定义:允许对象在内部状态改变时改变它的行为,对象看起来好像修改了它的类。
- 应用场景:当一个对象的行为取决于它的状态,并且它必须在运行时根据状态改变行为时使用,比如游戏角色在不同状态(如正常、中毒、隐身等)下有不同行为。
迭代器模式(Iterator Pattern)
- 定义:提供一种方法顺序访问一个聚合对象中的各个元素,而又不暴露该对象的内部表示。
- 应用场景:在需要遍历聚合对象(如集合、数组等)元素,但不希望暴露聚合对象内部结构时使用,然而,在大多数编程语言中,已经提供了内置的迭代器支持,所以显式使用迭代器模式的场景相对较少。
不常用模式
备忘录模式(Memento Pattern)
- 定义:在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便以后恢复到这个状态。
- 应用场景:适用于需要保存和恢复对象状态的场景,如游戏存档、文本编辑器的撤销操作等,但由于保存对象状态可能占用大量资源,且实现过程相对复杂,所以在实际应用中使用频率不高。
命令模式(Command Pattern)
- 定义:将请求封装为对象,可用不同请求对客户进行参数化,支持请求排队、记录日志及撤销操作。
- 应用场景:适用于需要将请求发送者和接收者解耦,或实现命令的撤销、重做等操作的场景,例如菜单操作(每个菜单项对应一个命令)、遥控器按键操作,但在实际应用中,对于简单操作使用命令模式会增加系统复杂度,所以使用频率不高。
中介者模式(Mediator Pattern)
- 定义:用一个中介对象来封装一系列的对象交互,中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。
- 应用场景:适用于多个对象之间存在复杂交互关系,导致对象间耦合度高的场景,例如,在机场调度系统中,飞机、跑道、塔台等对象之间的交互通过中介者进行管理,但在实际应用中,如果系统中对象交互关系简单,使用中介者模式会增加系统复杂性,所以使用不多。
访问者模式(Visitor Pattern)
- 定义:表示一个作用于某对象结构中的各元素的操作,它使你可以在不改变各元素类的前提下定义作用于这些元素的新操作。
- 应用场景:当一个对象结构包含多种类型对象,且需要对这些对象执行不同操作,但又不想在这些对象类中添加过多方法时使用,例如,在编译器中,对不同语法树节点(如变量声明节点、表达式节点等)进行语义检查、代码生成等操作,但由于访问者模式增加了代码复杂性和维护难度,所以使用频率较低。
解释器模式(Interpreter Pattern)
- 定义:给定一个语言,定义它的文法的一种表示,并定义一个解释器,这个解释器使用该表示来解释语言中的句子。
- 应用场景:当有一个简单语言需要解释执行,且可以将该语言中的语句表示为一个抽象语法树时使用。例如在小型特定领域编程语言(DSL)的实现中,对用该DSL编写的语句进行解析和执行操作可采用解释器模式;在一些简单的规则引擎设计中,如根据特定规则判断某些条件是否满足,也可应用此模式。但由于解释器模式在处理复杂语言和大量语句时,构建语法树和递归解释可能导致性能问题,并且实现复杂的文法会使系统变得庞大和难以维护,所以在实际项目中使用频率不是特别高。
设计原则
软件设计原则是指导软件设计的基本准则,旨在提高代码的可维护性、可扩展性和复用性。以下是面向对象设计的 五大原则(SOLID 即 单一职责、开闭原则、里氏替换、接口隔离、依赖倒置) 以及其他重要设计原则的详细介绍。
1. 单一职责原则(Single Responsibility Principle, SRP)
定义
一个类应该只有一个引起它变化的原因,即 一个类只负责一项职责
。
特点
- 优点:
- 提高类的内聚性,使类更容易理解和维护。
- 降低类的复杂度,减少类之间的耦合。
- 缺点:
- 可能会导致类的数量增加,增加系统的复杂度。
使用场景
- 当一个类承担了多个职责时,可以将这些职责分离到不同的类中,如有的接口会把创建和更新结合封装成一个接口。
示例
type Report struct {
content string
}
func (r *Report) Generate() string {
return r.content
}
type ReportSaver struct{}
func (rs *ReportSaver) Save(report *Report) {
// Save report to file
}
2. 开闭原则(Open/Closed Principle, OCP)
定义
软件实体(类、模块、函数等)应该 对扩展开放,对修改关闭
。
特点
- 优点:
- 提高系统的可扩展性,可以在不修改现有代码的情况下扩展功能。
- 减少引入新功能时的风险。
- 缺点:
- 需要更多的抽象和设计,增加了系统的复杂度。
使用场景
- 当需要扩展系统功能时,可以通过添加新类或模块来实现,而不是修改现有代码。这样可以避免因为修改现有代码而引入新的错误。
示例
type Shape interface {
Area() float64
}
type Rectangle struct {
width, height float64
}
func (r *Rectangle) Area() float64 {
return r.width * r.height
}
type Circle struct {
radius float64
}
func (c *Circle) Area() float64 {
return math.Pi * c.radius * c.radius
}
3. 里氏替换原则(Liskov Substitution Principle, LSP)
定义
子类应该能够替换其父类,并且替换后不会影响程序的正确性
。
- 这要求子类必须实现父类的所有抽象方法和属性,并且不能改变父类方法的前置条件和后置条件。
特点
- 优点:
- 提高代码的可复用性和可维护性。
- 确保继承关系的正确性。
- 缺点:
- 需要仔细设计类的继承关系,避免违反原则。
使用场景
- 当使用继承时,确保子类可以替换父类而不影响程序的正确性。
示例
type Bird interface {
Fly() string
}
type Sparrow struct{}
func (s *Sparrow) Fly() string {
return "Sparrow is flying"
}
type Ostrich struct{}
func (o *Ostrich) Fly() string {
return "Ostrich cannot fly"
}
4. 接口隔离原则(Interface Segregation Principle, ISP)
定义
接口应该小而专一
。客户端不应该依赖它不需要的接口,即一个类对另一个类的依赖应该建立在最小的接口上。这样可以避免客户端依赖一些不必要的方法。
特点
- 优点:
- 提高接口的内聚性,减少接口的复杂度。
- 减少类之间的耦合。
- 缺点:
- 可能会导致接口数量增加,增加系统的复杂度。
使用场景
- 当一个接口包含多个方法,而某些类只需要其中一部分方法时,可以将接口拆分为多个小接口。
示例
type Printer interface {
Print(document string)
}
type Scanner interface {
Scan() string
}
type MultiFunctionDevice interface {
Printer
Scanner
}
5. 依赖倒置原则(Dependency Inversion Principle, DIP)
定义
高层模块不应该依赖低层模块,二者都应该依赖抽象
。- 抽象不应该依赖细节,细节应该依赖抽象。
特点
- 优点:
- 提高代码的灵活性和可维护性。
- 减少模块之间的耦合。
- 缺点:
- 需要更多的抽象和设计,增加了系统的复杂度。
使用场景
- 当需要解耦高层模块和低层模块时,可以通过依赖抽象来实现。
示例
// 抽象
type Database interface {
Save(data string)
}
// 细节部分
type MySQL struct{}
// MySQL 实现了 Save 方法,因此它隐式实现了 Database 接口
func (m *MySQL) Save(data string) {
// Save data to MySQL
}
// 高层模块,依赖 Database 接口
type Service struct {
db Database
}
func NewService(db Database) *Service {
return &Service{db: db}
}
func (s *Service) SaveData(data string) {
s.db.Save(data)
}
func main() {
// 使用MySQL
mysqlDB := &MySQL{}
service := NewService(mysqlDB)
service.SaveData("data1")
// 未来可以轻松切换为其他数据库
/*
postgresDB := &PostgreSQL{}
service = NewService(postgresDB)
service.SaveData("data2")
*/
}
额外解释:
假设我们有一个高层模块 “订单处理系统”,它需要依赖一个低层模块 “数据库存储” 来保存订单信息。如果直接依赖具体的数据库存储实现,比如 MySQL 数据库,那么当我们需要将数据库换成 Oracle 时,订单处理系统的代码就可能需要大量修改。
传统方式(不符合DIP):
Service → MySQL (高层直接依赖低层)
依赖倒置方式:
Service → Database ← MySQL (双方都依赖抽象)
6. 迪米特法则 / 最少知识原则(Law of Demeter, LoD)
定义
一个对象应当对其他对象保持最少的了解
,即只与直接的朋友通信
。
特点
- 优点:
- 减少类之间的耦合,提高系统的可维护性。
- 提高代码的可读性和可理解性。
- 缺点:
- 可能会导致类的数量增加,增加系统的复杂度。
使用场景
- 当需要减少类之间的耦合时,可以通过限制类之间的直接交互来实现。
示例
type Person struct {
name string
}
func (p *Person) GetName() string {
return p.name
}
type Group struct {
people []*Person
}
func (g *Group) GetPersonNames() []string {
names := make([]string, 0)
for _, person := range g.people {
names = append(names, person.GetName())
}
return names
}
额外解释:
假设有一个学校系统,包含学生、班级和学校三个类。学生属于某个班级,班级属于某个学校。如果学生类需要获取学校的信息,按照迪米特法则,学生不应该直接与学校类交互,而是通过班级类来获取学校信息。因为班级是学生的直接朋友,而学校对于学生来说是陌生人。
总结
- 单一职责原则:一个类只负责一项职责。
- 开闭原则:对扩展开放,对修改关闭。
- 里氏替换原则:子类可以替换父类而不影响程序的正确性。
- 接口隔离原则:接口应该小而专一,客户端不应该依赖它不需要的接口。
- 依赖倒置原则:高层模块和低层模块都应该依赖抽象。
- 迪米特法则:一个对象应该对其他对象有最少的了解,只与直接的朋友通信。