创建型模式:如何创建对象
| 模式名称 | 作用 |
|---|---|
| 单例模式 | 是保证一个类仅有一个实例,并提供一个访问它的全局访问点。 |
| 简单工厂模式 | 通过专门定义一个类来负责创建其他类的实例,被创建的实例通常都具有共同的父类。 |
| 工厂方法模式 | 定义一个创建产品对象的工厂接口,将实际创建工作推迟到子类中。 |
| 抽象工厂模式 | 提供一个创建一系列相关或者相互依赖的接口,而无需指定它们具体的类。 |
| 原型模式 | 用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。 |
| 建造者模式 | 将一个复杂的构建与其表示相分离,使得同样的构建过程可以创建不同的表示。 |
单例模式
概念
单例模式(Singleton Pattern)是设计模式中的一种,它的核心目的是确保某个类在系统中只有一个实例,并提供一个全局访问点来获取该实例。
单例模式的三个关键要点:
- 唯一实例:类只能有一个实例。
- 实例自创建:类自己创建唯一实例。
- 全局访问点:整个系统通过某个全局方法访问该实例。
实现方式
1. 饿汉式单例
饿汉式单例模式在类加载时就创建实例,避免了多线程并发创建实例的问题。它的实现简单,但存在内存浪费的缺点,因为即使实例没有被使用,系统也会为其分配内存。
示例代码:
package mainimport ( "fmt" "time")// Log 结构体表示日志对象type Log struct { logLevel string logFile string}// 创建一个唯一的日志实例var instance = new(Log)// GetInstance 获取日志实例的全局函数func GetInstance() *Log { return instance}// LogInfo 记录普通信息日志func (l *Log) LogInfo(message string) { fmt.Printf("%s [INFO]: %s\n", time.Now().Format(time.RFC3339), message)}// LogError 记录错误日志func (l *Log) LogError(message string) { fmt.Printf("%s [ERROR]: %s\n", time.Now().Format(time.RFC3339), message)}func main() { // 获取单例实例 log1 := GetInstance() // 记录日志 log1.LogInfo("这是log1的普通信息日志") log1.LogError("这是log1的错误信息日志") // 再次获取实例 log2 := GetInstance() log2.LogInfo("这是log2的普通信息日志") // 检查两次获取的实例是否是同一个 fmt.Println("log1和log2是否是相同实例", log1 == log2)}运行结果:
2025-02-06T11:16:30+08:00 [INFO]: 这是log1的普通信息日志2025-02-06T11:16:30+08:00 [ERROR]: 这是log1的错误信息日志2025-02-06T11:16:30+08:00 [INFO]: 这是log2的普通信息日志log1和log2是否是相同实例 true优缺点:
- 优点:确保日志系统全局唯一,避免了多次创建日志实例,降低了系统资源浪费。代码简单,线程安全。
- 缺点:即使没有日志输出,日志实例也会在程序启动时创建,占用内存。
其他使用场景:
- 数据库连接池:可以通过单例模式来实现一个数据库连接池,在程序的整个生命周期内共享同一个数据库连接实例。
- 配置管理:配置管理器可以通过单例模式来确保整个应用程序中只有一个配置实例,避免多次读取配置文件或环境变量。
总结:
- 饿汉式单例是通过在程序启动时立即创建实例来保证唯一性。其优点在于实现简单,且线程安全,但缺点是可能会浪费内存,尤其是当实例未被使用时。
- 适用场景:适用于需要保证全局唯一且初始化时立即需要使用的对象,如日志系统、配置管理、数据库连接等。
2. 懒汉式单例
懒汉式单例模式只有在首次访问时才创建实例,因此避免了饿汉式中的内存浪费。然而,懒汉式并不是线程安全的,如果多个线程并发调用 GetInstance(),可能会导致多个实例的创建,违背了单例模式的初衷。
示例代码:
package mainimport "fmt"// CacheManager 是缓存管理器的结构体type CacheManager struct { cache map[string]string}// 存储缓存管理器的单例实例var instance *CacheManager// GetInstance 获取缓存管理器的单例实例func GetInstance() *CacheManager { // 只有在第一次访问时才创建实例 if instance == nil { instance = &CacheManager{ cache: make(map[string]string), } fmt.Println("缓存管理器已初始化") // 仅在第一次调用时打印 } return instance}// SetCache 设置缓存func (cm *CacheManager) SetCache(key, value string) { cm.cache[key] = value}// GetCache 获取缓存值func (cm *CacheManager) GetCache(key string) string { return cm.cache[key]}func main() { // 模拟使用缓存管理器 cache1 := GetInstance() cache1.SetCache("user_1", "Alice") cache1.SetCache("user_2", "Bob") // 再次获取缓存管理器实例 cache2 := GetInstance() fmt.Println("cache1 == cache2:", cache1 == cache2) // 打印为 true,验证单例 // 获取缓存值 fmt.Println("user_1:", cache2.GetCache("user_1")) fmt.Println("user_2:", cache2.GetCache("user_2"))}运行结果:
缓存管理器已初始化cache1 == cache2: trueuser_1: Aliceuser_2: Bob因为这个实现是没有并发安全的,所以在多个 goroutine 同时访问时,可能会导致 CacheManager 实例被初始化多次,这违背了单例模式的初衷。现在修改 main() 函数,创建多个 goroutine 来同时调用 GetInstance(),从而验证是否会出现并发错误。
func main() { var wg sync.WaitGroup wg.Add(5) // 模拟并发访问 for i := 0; i < 5; i++ { go func(i int) { defer wg.Done() // 获取缓存管理器实例 cacheInstance := GetInstance() cacheInstance.SetCache(fmt.Sprintf("user_%d", i+1), fmt.Sprintf("User_%d", i+1)) // 打印当前缓存 fmt.Printf("goroutine %d - %v\n", i+1, cacheInstance.cache) }(i) } // 等待所有 goroutine 执行完 wg.Wait()}运行结果:
缓存管理器已初始化goroutine 1 - map[user_1:User_1]goroutine 4 - map[user_1:User_1 user_2:User_2 user_4:User_4 user_5:User_5]goroutine 2 - map[user_1:User_1 user_2:User_2 user_4:User_4 user_5:User_5]goroutine 5 - map[user_1:User_1 user_5:User_5]goroutine 3 - map[user_1:User_1 user_2:User_2 user_3:User_3 user_4:User_4 user_5:User_5]从输出结果来看,确实出现了并发安全的问题,导致了 多个 goroutine 获取了不同的缓存内容。
- 缓存管理器已初始化 只被打印了一次,表示 GetInstance() 只在第一次调用时创建了 CacheManager 实例,说明懒汉式单例的初始化逻辑在某些 goroutine 中被触发。
- 各个 goroutine 中的缓存内容不同,原因在于多个 goroutine 可能在访问 instance 时,并未保证线程安全,所以不同的 goroutine 获取了不同版本的缓存管理器(即可能在没有加锁的情况下,多个 goroutine 各自初始化了 CacheManager)。
优缺点:
- 优点:不会浪费内存,实例只有在需要时才创建。
- 缺点:非线程安全,多个线程并发调用时可能导致多个实例。
3. 线程安全的懒汉式
上面的“懒汉式”实现是非线程安全的设计方式,也就是如果多个线程或者协程同时首次调用 GetInstance()方法有概率导致多个实例被创建,则违背了单例的设计初衷。那么在上面的基础上进行修改,可以利用 Sync.Mutex 进行加锁,保证线程安全。这种线程安全的写法,有个最大的缺点就是每次调用该方法时都需要进行锁操作,在性能上相对不高效,具体的实现改进如下:
package mainimport ( "fmt" "sync")// CacheManager 是缓存管理器的结构体type CacheManager struct { cache map[string]string}// 存储缓存管理器的单例实例var instance *CacheManagervar mu sync.Mutex // 用于加锁,确保线程安全// GetInstance 获取缓存管理器的单例实例func GetInstance() *CacheManager { // 加锁确保线程安全 mu.Lock() defer mu.Unlock() // 只有在第一次访问时才创建实例 if instance == nil { instance = &CacheManager{ cache: make(map[string]string), } fmt.Println("缓存管理器已初始化") // 仅在第一次调用时打印 } return instance}// SetCache 设置缓存func (cm *CacheManager) SetCache(key, value string) { cm.cache[key] = value}// GetCache 获取缓存值func (cm *CacheManager) GetCache(key string) string { return cm.cache[key]}func main() { // 模拟使用缓存管理器 cache1 := GetInstance() cache1.SetCache("user_1", "Alice") cache1.SetCache("user_2", "Bob") // 再次获取缓存管理器实例 cache2 := GetInstance() fmt.Println("cache1 == cache2:", cache1 == cache2) // 打印为 true,验证单例 // 获取缓存值 fmt.Println("user_1:", cache2.GetCache("user_1")) fmt.Println("user_2:", cache2.GetCache("user_2"))}运行结果:
缓存管理器已初始化cache1 == cache2: trueuser_1: Aliceuser_2: Bob每次调用 GetInstance() 都需要加锁,虽然锁的粒度很小(仅限于实例化过程),但在高并发的场景下,频繁的加锁解锁会引入性能开销。
4. 使用原子操作改进懒汉式
使用原子操作(sync/atomic)来优化懒汉式单例,使得在确保线程安全的同时提高性能。通过标记位来判断实例是否已初始化,避免重复加锁。
示例代码:
package mainimport ( "fmt" "sync" "sync/atomic")// CacheManager 是缓存管理器的结构体type CacheManager struct { cache map[string]string}// 存储缓存管理器的单例实例var instance *CacheManagervar initialized uint32 // 用于标记是否初始化(0:未初始化,1:已初始化)var mu sync.Mutex // 互斥锁,确保初始化的安全性// GetInstance 获取缓存管理器的单例实例(使用 atomic 进行优化)func GetInstance() *CacheManager { // 第一层检查(无锁): 如果实例已初始化,直接返回,避免加锁 if atomic.LoadUint32(&initialized) == 1 { return instance } // 加锁确保线程安全 mu.Lock() defer mu.Unlock() // 第二层检查(有锁): 再次检查,避免多个线程初始化 if instance == nil { instance = &CacheManager{ cache: make(map[string]string), } fmt.Println("缓存管理器已初始化") // 仅在第一次调用时打印 // 标记为已初始化 atomic.StoreUint32(&initialized, 1) } return instance}// SetCache 设置缓存func (cm *CacheManager) SetCache(key, value string) { cm.cache[key] = value}// GetCache 获取缓存值func (cm *CacheManager) GetCache(key string) string { return cm.cache[key]}func main() { // 模拟使用缓存管理器 cache1 := GetInstance() cache1.SetCache("user_1", "Alice") cache1.SetCache("user_2", "Bob") // 再次获取缓存管理器实例 cache2 := GetInstance() fmt.Println("cache1 == cache2:", cache1 == cache2) // 打印为 true,验证单例 // 获取缓存值 fmt.Println("user_1:", cache2.GetCache("user_1")) fmt.Println("user_2:", cache2.GetCache("user_2"))}运行结果:
缓存管理器已初始化cache1 == cache2: trueuser_1: Aliceuser_2: Bob上述的实现其实 Golang 有个方法已经帮助开发者实现完成,就是 Once 模块,来看下 Once.Do()方法的源代码:
// Once is an object that will perform exactly one action.//// A Once must not be copied after first use.//// In the terminology of the Go memory model,// the return from f “synchronizes before”// the return from any call of once.Do(f).type Once struct { // done indicates whether the action has been performed. // It is first in the struct because it is used in the hot path. // The hot path is inlined at every call site. // Placing done first allows more compact instructions on some architectures (amd64/386), // and fewer instructions (to calculate offset) on other architectures. done atomic.Uint32 m Mutex}// Do calls the function f if and only if Do is being called for the// first time for this instance of Once. In other words, given//// var once Once//// if once.Do(f) is called multiple times, only the first call will invoke f,// even if f has a different value in each invocation. A new instance of// Once is required for each function to execute.//// Do is intended for initialization that must be run exactly once. Since f// is niladic, it may be necessary to use a function literal to capture the// arguments to a function to be invoked by Do://// config.once.Do(func() { config.init(filename) })//// Because no call to Do returns until the one call to f returns, if f causes// Do to be called, it will deadlock.//// If f panics, Do considers it to have returned; future calls of Do return// without calling f.func (o *Once) Do(f func()) { // Note: Here is an incorrect implementation of Do: // // if o.done.CompareAndSwap(0, 1) { // f() // } // // Do guarantees that when it returns, f has finished. // This implementation would not implement that guarantee: // given two simultaneous calls, the winner of the cas would // call f, and the second would return immediately, without // waiting for the first's call to f to complete. // This is why the slow path falls back to a mutex, and why // the o.done.Store must be delayed until after f returns. if o.done.Load() == 0 { // Outlined slow-path to allow inlining of the fast-path. o.doSlow(f) }}func (o *Once) doSlow(f func()) { o.m.Lock() defer o.m.Unlock() if o.done.Load() == 0 { defer o.done.Store(1) f() }}结构体设计和内存布局
type Once struct { done atomic.Uint32 // 用于标记 Do 是否已执行 m Mutex // 用于加锁,确保线程安全}- done:这个字段是 atomic.Uint32 类型,用于标记是否已经执行过目标函数 f()。它被设计成 atomic 类型,以便高效且安全地处理并发访问。
- m:一个 Mutex,用于同步多个 goroutine 对 Do 方法的调用,保证 goroutine 在执行 f() 时的并发安全问题。
性能优化设计
// done first in the struct because it is used in the hot path.// The hot path is inlined at every call site.// Placing done first allows more compact instructions on some architectures (amd64/386),// and fewer instructions (to calculate offset) on other architectures.done atomic.Uint32- 热路径:指的是程序中经常被执行的那部分代码。它是程序性能的关键部分,因为它被频繁调用,直接影响程序的总体性能。
- 慢路径:指的是程序中不常执行的部分,通常只有在特定条件下才会触发,或者是当热路径执行失败时才会进入的代码路径。
Go 在设计 sync.Once 时,考虑了性能的优化。将 done 字段放在结构体的首部,是为了 提高性能,尤其是在“热路径”上,确保访问 done 的操作能更高效,因为它会被频繁访问。对于一些架构(如 amd64 或 386),这样能减少指令的数量,提高程序执行的效率。
Do 方法的实现
func (o *Once) Do(f func()) { if o.done.Load() == 0 { // 如果没有执行过 o.doSlow(f) // 执行慢路径 }}- o.done.Load() == 0:首先检查 done 是否为 0,如果是,表示尚未执行过目标函数 f(),然后进入慢路径。Load 是一个原子操作,能够高效地读取 done 字段的值。
慢路径:doSlow
func (o *Once) doSlow(f func()) { o.m.Lock() // 获取锁,确保只有一个 goroutine 可以执行 f() defer o.m.Unlock() if o.done.Load() == 0 { f() // 执行目标函数 o.done.Store(1) // 设置 `done` 为 1,表示已经执行过 }}- o.m.Lock():慢路径通过锁来确保只有一个 goroutine 可以执行目标函数 f()。当多个 goroutine 并发调用 Do 时,只有第一个调用会执行 f(),其余的 goroutine 会等待,直到第一个 goroutine 执行完毕。
- o.done.Store(1):这一步将 done 标记为 1,确保将来不再执行 f()。这一步是在 f() 执行完成后才进行,确保 f() 执行的完成同步。
为什么不使用 CAS?
Go 官方实现的注释里提到,尝试使用 CompareAndSwap (CAS) 来代替锁的做法是 不正确的。原因是,CAS 只能确保原子地将 done 从 0 修改为 1,但它无法保证在 f() 执行完成之前,其他 goroutine 不会提前返回。简单来说,CAS 无法保证同步,因为如果两个 goroutine 同时调用 Do,第一个 goroutine 可能会成功执行 f(),而第二个 goroutine 会立即返回,而不等待第一个 goroutine 完成 f() 的执行。
因此,官方实现选择使用互斥锁来保证 f() 执行的完全同步,确保只有一个 goroutine 会调用 f(),并且所有 goroutine 都会等到 f() 执行完毕后再继续。
设计上的考虑
- 原子操作 + 锁:Go 官方 sync.Once 实现结合了原子操作 (atomic) 和互斥锁 (Mutex) 的双重保证。通过原子操作 Load 和 Store 来快速检查和设置 done 标志,只有在必要时(即进入慢路径时)才使用锁来确保同步。
- 不允许 Once 被复制:官方注释明确指出,Once 不应在使用之后被复制。这是因为 Once 依赖于原子操作和锁来确保线程安全,一旦发生复制,可能会导致多个实例并发执行目标函数,从而破坏 Once 的单次执行保证。
- 处理 panic:如果 f() 执行时发生 panic,Do 方法会认为 f() 已经完成并返回,不会再重新执行。这种行为确保了即使发生错误,Do 也不会造成死锁,并且后续的调用会跳过 f() 的执行。
5. 使用 sync.Once 优化
Go 提供了 sync.Once 类型,确保某个操作只执行一次,利用它可以更加简洁地实现线程安全的单例模式。Once.Do() 会保证传入的函数只会执行一次。
package mainimport ( "fmt" "sync")// CacheManager 是缓存管理器的结构体type CacheManager struct { cache map[string]string}// 存储缓存管理器的单例实例var instance *CacheManagervar once sync.Once // `sync.Once` 确保只执行一次// GetInstance 获取缓存管理器的单例实例func GetInstance() *CacheManager { once.Do(func() { instance = &CacheManager{ cache: make(map[string]string), } fmt.Println("缓存管理器已初始化") // 仅在第一次调用时打印 }) return instance}// SetCache 设置缓存func (cm *CacheManager) SetCache(key, value string) { cm.cache[key] = value}// GetCache 获取缓存值func (cm *CacheManager) GetCache(key string) string { return cm.cache[key]}func main() { // 模拟使用缓存管理器 cache1 := GetInstance() cache1.SetCache("user_1", "Alice") cache1.SetCache("user_2", "Bob") // 再次获取缓存管理器实例 cache2 := GetInstance() fmt.Println("cache1 == cache2:", cache1 == cache2) // 打印为 true,验证单例 // 获取缓存值 fmt.Println("user_1:", cache2.GetCache("user_1")) fmt.Println("user_2:", cache2.GetCache("user_2"))}运行结果:
缓存管理器已初始化cache1 == cache2: trueuser_1: Aliceuser_2: Bob简单工厂模式
概念
简单工厂模式 由一个工厂类 负责创建所有类型的实例,它根据传入的参数决定创建哪种具体对象。如果不使用简单工厂模式,用户在创建不同种类的对象时,每次都需要编写特定的创建对象逻辑。
因此,简单工厂模式只是对创建多种不同种类对象的方式进行封装,让调用者无需直接实例化对象,而是通过一个统一的工厂方法 来获取所需对象。
实现方式
package mainimport "fmt"// Logger 是日志接口type Logger interface { Log(message string)}// FileLogger 具体的文件日志type FileLogger struct{}func (f *FileLogger) Log(message string) { fmt.Println("[FileLogger]:", message)}// ConsoleLogger 具体的控制台日志type ConsoleLogger struct{}func (c *ConsoleLogger) Log(message string) { fmt.Println("[ConsoleLogger]:", message)}// LoggerFactory 是简单工厂方法,根据 logType 创建不同的 Logger 实例func LoggerFactory(logType string) Logger { if logType == "file" { return &FileLogger{} } else if logType == "console" { return &ConsoleLogger{} } return nil}// 客户端代码func main() { // 创建文件日志 fileLogger := LoggerFactory("file") fileLogger.Log("This is a file log") // 创建控制台日志 consoleLogger := LoggerFactory("console") consoleLogger.Log("This is a console log")}运行结果:
[FileLogger]: This is a file log[ConsoleLogger]: This is a console log优点:
- 封装对象创建,调用者不需要知道具体对象的创建细节,提高代码的可维护性。
- 提高代码复用性,避免重复编写对象创建逻辑。
缺点:
- 不符合开闭原则,每次新增一种日志类型,都需要修改 LoggerFactory 方法,增加 if-else 分支,容易导致代码复杂度增加。
- 当子类过多时,工厂方法的逻辑会变得复杂。
- 扩展性受限,因为 LoggerFactory 需要知道所有可能的 Logger 具体实现。
工厂方法模式
概念
工厂方法模式(Factory Method Pattern)是一个 创建型设计模式,它定义了一个用于创建对象的接口,但由 子类决定实例化哪一个类。换句话说,工厂方法模式通过子类化来推迟对象的创建过程,避免客户端直接使用具体类,增强了系统的灵活性和扩展性。
相比于简单工厂模式,工厂方法模式将对象的创建逻辑交给了具体的子类,而不是由一个统一的工厂类处理,这使得代码更加符合 开闭原则(OCP),即系统可以在不修改现有代码的情况下扩展新的对象类型。
实现方式
package mainimport "fmt"// Database 定义数据库接口type Database interface { Connect() string}// MySQL 实现 Database 接口type MySQL struct{}func (m *MySQL) Connect() string { return "Connected to MySQL"}// PostgreSQL 实现 Database 接口type PostgreSQL struct{}func (p *PostgreSQL) Connect() string { return "Connected to PostgreSQL"}// DatabaseFactory 是工厂方法接口type DatabaseFactory interface { CreateDatabase() Database}// MySQLFactory 负责创建 MySQLtype MySQLFactory struct{}func (f *MySQLFactory) CreateDatabase() Database { return &MySQL{}}// PostgreSQLFactory 负责创建 PostgreSQLtype PostgreSQLFactory struct{}func (f *PostgreSQLFactory) CreateDatabase() Database { return &PostgreSQL{}}func main() { var factory DatabaseFactory // 使用 MySQL 工厂创建 MySQL 实例 factory = &MySQLFactory{} mysql := factory.CreateDatabase() fmt.Println(mysql.Connect()) // 使用 PostgreSQL 工厂创建 PostgreSQL 实例 factory = &PostgreSQLFactory{} postgres := factory.CreateDatabase() fmt.Println(postgres.Connect())}运行结果:
Connected to MySQLConnected to PostgreSQL优点:
- 符合开闭原则(OCP):可以通过扩展新的工厂类来创建不同类型的对象,而不需要修改原有的工厂代码。
- 增强了系统的灵活性:通过工厂方法的接口化,可以在运行时灵活切换不同的数据库或其他产品类型。
- 支持产品家族扩展:新增不同类型的产品时,只需扩展新的工厂类,客户端代码无需修改。
- 减少了客户端对具体类的依赖:客户端依赖的是工厂接口,而不是具体的数据库类型,符合依赖倒置原则(DIP)。
缺点:
- 类的数量增加:每个产品需要有一个具体工厂类,系统的类会变得更多。
- 工厂方法的选择问题:如果产品种类过多,工厂方法的管理和选择可能变得复杂,导致代码冗长。
- 抽象层次过多:为了支持工厂方法,代码中引入了很多的抽象层次,这可能导致设计过于复杂,特别是产品种类比较少时。
抽象工厂模式
概念
抽象工厂模式(Abstract Factory Pattern) 是一种 创建型设计模式,用于创建一组相关或相互依赖的对象,而不需要指定它们的具体类。与工厂方法模式不同,抽象工厂模式不仅创建单个对象,而是创建一整套对象(产品族)。
抽象工厂提供多个工厂方法,每个方法用于创建不同类型的产品,确保同一产品族的实例在一起使用。
实现方式
package mainimport "fmt"// Button 按钮接口type Button interface { Render()}// Checkbox 复选框接口type Checkbox interface { Render()}// WindowsButton 具体按钮type WindowsButton struct{}func (w *WindowsButton) Render() { fmt.Println("Rendering Windows Button")}// MacButton 具体按钮type MacButton struct{}func (m *MacButton) Render() { fmt.Println("Rendering Mac Button")}// WindowsCheckbox 具体复选框type WindowsCheckbox struct{}func (w *WindowsCheckbox) Render() { fmt.Println("Rendering Windows Checkbox")}// MacCheckbox 具体复选框type MacCheckbox struct{}func (m *MacCheckbox) Render() { fmt.Println("Rendering Mac Checkbox")}// GUIFactory 抽象工厂接口type GUIFactory interface { CreateButton() Button CreateCheckbox() Checkbox}// WindowsFactory 负责创建 Windows GUItype WindowsFactory struct{}func (w *WindowsFactory) CreateButton() Button { return &WindowsButton{}}func (w *WindowsFactory) CreateCheckbox() Checkbox { return &WindowsCheckbox{}}// MacFactory 负责创建 Mac GUItype MacFactory struct{}func (m *MacFactory) CreateButton() Button { return &MacButton{}}func (m *MacFactory) CreateCheckbox() Checkbox { return &MacCheckbox{}}func main() { // 使用 Windows 工厂创建 Windows GUI var factory GUIFactory = &WindowsFactory{} button := factory.CreateButton() checkbox := factory.CreateCheckbox() button.Render() checkbox.Render() // 使用 Mac 工厂创建 Mac GUI factory = &MacFactory{} button = factory.CreateButton() checkbox = factory.CreateCheckbox() button.Render() checkbox.Render()}运行结果:
Rendering Windows ButtonRendering Windows CheckboxRendering Mac ButtonRendering Mac Checkbox优点:
- 符合开闭原则(OCP):可以通过增加新的具体工厂来扩展新的产品族,而不会影响现有代码。
- 符合依赖倒置原则(DIP):客户端依赖于抽象接口(GUIFactory),而不是具体类(WindowsButton、MacButton)。
- 保证产品族的兼容性:同一个工厂创建的产品都属于同一产品族,防止出现跨平台不兼容问题。
- 封装对象创建过程:将创建逻辑集中到具体工厂中,客户端代码无需关注创建细节。
缺点:
- 代码复杂度提高:相比于简单工厂和工厂方法模式,抽象工厂模式需要更多的类和接口。
- 不适合产品族变化频繁的情况:如果产品种类经常变动(新增或移除产品),则需要修改抽象工厂接口,影响较大。
- 扩展新产品困难:如果要添加新的 UI 组件(例如 TextField),需要修改 GUIFactory 接口,破坏了开闭原则。
总结
| 设计模式 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 单例模式 | 全局唯一对象或需要避免重复创建的情况 | 保证全局唯一,节省资源,延迟加载 | 难以扩展,增加测试复杂度,隐藏依赖关系 |
| 简单工厂模式 | 单一产品或简单需求创建时 | 实现简单,易于理解 | 当产品种类增多时,工厂类会变得庞大,难以维护 |
| 工厂方法模式 | 多种产品类型,需要扩展时 | 可以灵活扩展产品类型,符合开闭原则 | 类的数量增多,复杂度上升 |
| 抽象工厂模式 | 需要创建多个相关产品,并保证产品族兼容时 | 保证产品族的一致性和兼容性,便于扩展 | 实现复杂,修改产品族时可能违反开闭原则 |