☰
单例模式与工厂模式实战:线程安全与架构设计
2026/10/5 8:45:09 网站建设 项目流程

1. 全局认知:为什么单例和工厂是登场率最高的两个模式

做了十几年开发,面试过的人也快上百了,我发现一个很有意思的现象:23种设计模式里,真正能在日常业务代码中频繁见到、每个人都能聊上几句的,翻来覆去就是单例和工厂。其他模式要么场景太窄,要么抽象层次太高,有些模式我甚至一年到头都碰不到一次。而单例和工厂不一样,它们解决的是最基础、最普遍的痛点:对象应该怎么创建、怎么管理。如果你能把这两个模式吃透,不仅写出来的代码更干净,别人review你的代码时也会舒服很多。

所谓单例模式,就是保证一个类在整个进程中只有一个实例存在,并且提供一个全局访问点。举个生活化的例子:一个公司只能有一个CEO,你不能今天见一个CEO,明天又冒出另一个CEO来发号施令,那公司就乱套了。对应到代码里,典型的场景比如日志管理器、配置管理器、线程池、数据库连接池,这些对象如果每个模块都各自new一个,内存里会有好几份重复的配置数据、连接、日志流,既浪费资源又容易造成状态不一致。我记得早期接手过一个老项目,日志模块没有做单例,结果多个线程各自往文件里写日志,文件句柄互相冲突,日志内容也乱成一片,排查了半天才发现是这么低级的问题。

工厂模式则解决另一个问题:创建对象的逻辑不该散落在业务代码里。想象一下,你去餐厅点菜,不需要知道厨师是怎么备料、怎么下锅的,你只需要告诉服务员"来一份鱼香肉丝",后厨自然会给你端出来。工厂模式就是这个服务员,把"创建对象"这件事集中起来,让调用方不需要关心具体创建的是哪个类、构造参数怎么传。这样做的好处是:当你需要新增一种产品类型时,业务调用方一行代码都不用改,只需要扩展工厂即可。

这两个模式经常放在一起讨论,还有一个实际原因:单例模式创建的往往就是一个工厂对象。这就好比工厂是一个全局唯一的"对象生产车间",既要保证车间只有一个,又要保证车间能生产各种产品。两者结合,几乎成了大型项目的基础设施标配。接下来的内容,我会分别拆解这两种模式的实现方式、选型逻辑和踩过的坑,代码会用Java和C++各写一遍对照,这两个语言在单例实现上有些关键差异,新手很容易踩雷。

2. 单例模式:五种实现方式与线程安全深度拆解

2.1 懒汉式:最直观的实现,却是一颗定时炸弹

先看最基础的懒汉式写法,Java版本:

public class Singleton { private static Singleton instance; private Singleton() { // 私有构造,禁止外部new } public static Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }

这种写法在单线程环境下完全没问题,逻辑也很清晰:第一次调用getInstance时才创建实例,之后直接复用。"懒"就懒在不到万不得已不创建,省资源。但是,多线程环境下它就废了。假设线程A和线程B同时进入getInstance方法,两个线程都判断instance为null,然后都能进入if块,各自new一个实例出来,单例就失效了。更麻烦的是,即使不是同时进入,也可能出现线程A已经new完正在赋值,线程B刚好读到了旧值,拿到的还是null。

解决这个问题最直接的办法是加synchronized关键字:

public static synchronized Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; }

但这种方式性能很差,因为getInstance是高频调用方法,每次进来都要抢锁,而实际上只有第一次创建时才需要同步,后面都是读操作。这就好比每次去食堂打饭都要过安检,明明大家都知道饭已经打过了,还得重复检查,白白浪费时间。我之前做过一个简单的压测,这种写法在多线程高并发场景下,性能损耗可以达到数倍,在真正的服务端项目里是不能接受的。

2.2 双重检查锁:C++和Java的两种命运

为了既保证线程安全又不损失性能,经典的做法是双重检查锁(DCL,Double-Checked Locking):

public class Singleton { private static volatile Singleton instance; private Singleton() {} public static Singleton getInstance() { if (instance == null) { // 第一次检查,不加锁 synchronized (Singleton.class) { if (instance == null) { // 第二次检查,加锁 instance = new Singleton(); } } } return instance; } }

第一层判断就是为了过滤掉大多数进入方法但实例已经创建好的情况,不用抢锁;只有实例确实为null时,才进入同步块,在锁内部再做一次检查,防止多个线程同时通过第一层判断后重复创建。这个"双重检查"的写法在Java里有一个关键点:instance必须用volatile修饰,否则在极端情况下会出大问题。

为什么必须volatile?因为instance = new Singleton()这行代码在CPU执行时不是原子的,它本质上是三步:分配内存、调用构造方法初始化对象、把内存地址赋值给instance引用。在Java内存模型的规则下,编译器和CPU允许指令重排,也就是第三步可能先于第二步执行。线程A执行完第一步和第三步后,第二步还没执行完,线程B这时候进来看到instance不为null,直接拿去用,得到的却是一个"半初始化"的对象,里面的字段都是默认值,一调用就出问题。volatile关键字能禁止这种重排序,保证赋值操作一定在对象完整初始化之后。

但是C++的DCL在早期是很尴尬的,因为在C++03标准里并没有volatile的多线程语义,volatile只是告诉编译器"别把这个变量的读写优化掉",根本不解决指令重排问题。直到C++11标准引入了原子库和内存序(memory order),这个问题才算真正有了标准解法。现在的推荐写法是:

class Singleton { private: static std::atomic<Singleton*> instance; public: static Singleton* getInstance() { Singleton* p = instance.load(std::memory_order_acquire); if (p == nullptr) { std::lock_guard<std::mutex> lock(mutex_); p = instance.load(std::memory_order_relaxed); if (p == nullptr) { p = new Singleton(); instance.store(p, std::memory_order_release); } } return p; } };

这段代码里的acquire和release是什么含义呢?release存储的意思是说:"在我这个存储之前的所有内存操作,都不能被重排到这个存储之后",其他线程通过acquire加载读到这个值时,就能保证看到释放侧之前所有的修改。简单理解就是:release相当于"我把货架上的商品摆整齐了,然后贴出'已上架'的告示",acquire相当于"看到'已上架'告示的人,就能放心地认为商品是完整的"。这样就能确保拿到指针的线程一定看到一个完整构造好的单例对象。

2.3 饿汉式与静态内部类:线程安全为何不再需要加锁

双重检查锁虽然解决了性能问题,但代码复杂度和理解成本都上去了。如果你不需要懒加载——也就是程序启动时就创建好实例,那么饿汉式是最简单的选择:

public class Singleton { private static final Singleton INSTANCE = new Singleton(); private Singleton() {} public static Singleton getInstance() { return INSTANCE; } }

它的核心原理是:JVM在类加载阶段就会完成静态变量的初始化,而类加载机制本身就保证了线程安全。多个线程访问同一个类时,JVM内部有锁机制保证类只会被成功加载一次,静态变量的赋值也只会执行一次。所以饿汉式天生就是线程安全的,不需要加任何同步控制。

但饿汉式的缺点是:只要类被加载(有可能只是被引用了一下,还没调用getInstance),实例就已经创建了。对资源要求苛刻的场景,比如一个大项目里有很多单例组件,如果全是饿汉式,启动过程会一次性初始化所有东西,内存和初始化时间都会飙升。

静态内部类的写法恰好弥补了这个缺陷,这也是我认为Java里最优雅的单例实现方式:

public class Singleton { private Singleton() {} private static class Holder { private static final Singleton INSTANCE = new Singleton(); } public static Singleton getInstance() { return Holder.INSTANCE; } }

静态内部类SingletonHolder只有在getInstance方法第一次被调用时才会被JVM加载,从而触发INSTANCE的创建。也就是说它同时实现了懒加载和线程安全:线程安全靠的还是类加载机制,懒加载靠的是"外部类加载时不会加载内部类"这一机制。既没有锁,也没有复杂的判断逻辑,代码简洁得不能再简洁,这是我个人最推荐的Java单例写法。

2.4 枚举单例:防御反射与序列化的终极方案

如果追求极致的安全,Java里还有一个容易被忽略的写法——枚举单例:

public enum Singleton { INSTANCE; public void doSomething() { // 业务逻辑 } }

这种方式虽然看着简单,但它天然解决了两个很隐蔽的问题。第一是反射破坏:普通单例的私有构造函数,可以通过setAccessible(true)强行调用,然后new出第二个实例。而枚举类在JVM层面就没有无参构造可以被反射调用,即使强行攻击也会抛异常。第二是序列化破坏:如果单例类实现了Serializable接口,反序列化时会通过特殊机制直接创建一个新实例,不走构造函数,单例就破了。普通类的解决办法是在类里加readResolve方法,返回已有实例,但枚举类从语言层面根本不允许反序列化创建新实例。

不过实际工作里用枚举单例的人确实不多,因为团队习惯和代码风格问题,在一个全是普通类的项目里突然冒出个枚举,很多人不理解。而且枚举单例的可读性相对差一些,不太好承载复杂的初始化逻辑。我的建议是:如果你的单例涉及序列化,或者对安全要求极高(比如支付核心模块),用枚举是最稳的;一般业务场景,静态内部类完全够用。

2.5 C++的Meyers单例:我见过最简单的线程安全实现

C++里还有个非常取巧但极其优雅的写法,叫Meyers单例,用局部静态变量实现:

class Singleton { public: static Singleton& getInstance() { static Singleton instance; return instance; } private: Singleton() {} Singleton(const Singleton&) = delete; Singleton& operator=(const Singleton&) = delete; };

为什么这么简单就能线程安全?因为C++11标准明确规定:函数内局部静态变量的初始化是线程安全的,编译器会自动生成保护代码,确保多个线程同时第一次调用getInstance时,只有一个线程会执行构造,其他线程会阻塞等待初始化完成。这相当于编译器帮你做好了之前DCL需要手动做的所有事。代价就是没有懒加载(第一次调用必然触发构造)和不能控制构造时机,但绝大多数场景这都不是问题。

我见过有些老项目还在用C++03时代的对拍写法,没有delete拷贝构造,导致外部可以Singleton a = Singleton::getInstance()复制一份实例出来,单例名存实亡。你这个class至少要把拷贝构造和赋值运算符禁掉,否则这个单例是锁不住的。

3. 工厂模式:从简单工厂到抽象工厂的演变逻辑

3.1 简单工厂:一个工厂类里的if-else

工厂模式最容易上手的是简单工厂,它不是GoF那23种设计模式中的正式成员,但写起来最直白:

public class WeaponFactory { public static Weapon create(String type) { if ("sword".equals(type)) { return new Sword(); } else if ("gun".equals(type)) { return new Gun(); } else if ("bow".equals(type)) { return new Bow(); } throw new IllegalArgumentException("未知武器类型: " + type); } }

调用方拿到的只是Weapon接口(或者抽象类),不需要知道具体是哪个实现类。这么做最直接的价值是:把"创建哪个类"的判断逻辑从业务代码里抽了出来。游戏里的拾取装备、商店购买、Boss掉落,都可能需要不同武器,如果在每个系统里都写一遍这种if-else去判断创建,代码会到处都是一样的碎片。集中到WeaponFactory后,新增武器只需要改动一个文件。

但简单工厂的问题很明显:首先,新增一个类型就要改动工厂类的判断逻辑,违背了开闭原则(对扩展开放,对修改关闭);其次,如果产品类型非常多,工厂类会膨胀成一个巨大的if-else集合,再往下就是switch-case,维护体验很差。所以它适合产品种类少、变动不频繁的场景,比如常量解析、消息类型创建。我自己经常在Parser相关的代码里用到它,因为消息类型虽然多,但相对固定。

3.2 工厂方法模式:把创建动作延迟到子类

工厂方法模式把"创建对象"这件事从一个大工厂类拆散到各个子类中,每个子类负责创建自己对应的产品:

public abstract class EnemyFactory { public abstract Enemy createEnemy(); public void spawnEnemy() { Enemy enemy = createEnemy(); enemy.spawn(); } } public class ZombieFactory extends EnemyFactory { @Override public Enemy createEnemy() { return new Zombie(); } } public class BossFactory extends EnemyFactory { @Override public Enemy createEnemy() { return new Boss(); } }

注意这里的模板方法思想:spawnEnemy是父类定义好的完整流程,但具体创建什么敌人由子类决定。也就是说,父类不关心到底创建的是Zombie还是Boss,它只负责协调流程。当你新增一种怪物时,只需要新增一个子工厂,改一行传入的工厂对象,其他所有逻辑不动。这解决了简单工厂"每加一个类型就要改工厂类"的痛点。

工厂方法模式的代价是类的数量翻倍:每加一个产品就要加一个对应的工厂类。这个代价在小项目里可能显得多余,但当一个项目里需要为不同平台(Android/iOS/Web)、不同数据库(MySQL/PostgreSQL)提供同一套接口的不同实现时,工厂方法模式的扩展性优势就非常明显了。每个平台一套子工厂,互不干扰。

3.3 抽象工厂模式:创建一族相关对象

抽象工厂模式是工厂方法模式的延伸,解决的是"几个产品之间有固定搭配关系"的场景。比如游戏里一个"角色"实例,不仅需要武器,还需要坐骑、套装皮肤,这三者通常是一起被创建出来、并且配套使用的:

public interface CharacterFactory { Weapon createWeapon(); Mount createMount(); Skin createSkin(); } public class KnightFactory implements CharacterFactory { @Override public Weapon createWeapon() { return new Sword(); } @Override public Mount createMount() { return new Horse(); } @Override public Skin createSkin() { return new IronArmor(); } } public class ArcherFactory implements CharacterFactory { @Override public Weapon createWeapon() { return new Bow(); } @Override public Mount createMount() { return new Wolf(); } @Override public Skin createSkin() { return new LeatherArmor(); } }

使用抽象工厂的核心意义在于保证产品族的配套一致性。如果你用两个独立工厂分别创建武器和坐骑,很容易出现"骑士拿了一把弓,弓手骑着一匹马"这种搭配混乱的情况。抽象工厂从入口就绑定了这一族产品必须一起出现。缺点是扩展产品族中的新品种非常痛苦,比如现在要添加一个"护盾"产品,CharacterFactory接口以及所有实现它的工厂类全部要改一遍。这是抽象工厂的固有缺陷,所以这个模式更适合产品族结构稳定、但不同系列之间变化多的场合。

3.4 Unity场景中的工厂模式应用参考

不少做游戏的朋友搜到工厂模式,是因为在Unity里遇到了对象管理的问题。Unity里创建对象一般用Instantiate,但如果你的项目同时存在多种敌人、多种子弹、多种掉落物,直接在MonoBehaviour各个脚本里写Instantiate会非常散乱。用工厂模式重构之后,一般会长这样:

public class EnemyFactory { private Dictionary<EnemyType, GameObject> prefabMap; public GameObject CreateEnemy(EnemyType type, Vector3 pos, Quaternion rot) { if (!prefabMap.ContainsKey(type)) { Debug.LogError("未知敌人类型: " + type); return null; } return Object.Instantiate(prefabMap[type], pos, rot); } }

这种做法的价值在哪里?第一,把"敌人prefab从哪来"这件事集中到一个字典里,策划加新敌人只需要在配置表和工厂字典里注册一次;第二,生产逻辑(比如某些敌人出生时需要对位置做偏移、需要挂上特殊脚本)统一在工厂里处理,不会散落在各个Spawn脚本中。我自己在游戏项目里还喜欢把对象池(Object Pool)和工厂结合起来:工厂负责创建,池子负责回收复用,两者协同能显著减少频繁实例化和销毁带来的卡顿。搜索热词里提到"unity工厂模式",大概率就是碰到了这类问题,思路理清楚再写代码会顺手很多。

4. 模式联手:单例工厂架构的完整实战设计

4.1 需求场景与整体架构

单独用单例和单单独用工厂都不算难,难的是理解它们怎么配合。我在一个Spring Boot网关项目中实践过这种组合架构,这里梳理一个完整的例子,场景设定为一个日志收集系统,需要支持不同的日志输出端(控制台、文件、远程服务),同时整个系统中只允许存在一个日志管理入口,所有模块都通过这个入口来记录日志。

架构设计如下:

  • LoggerManager:全局唯一的日志管理入口,采用单例模式,负责初始化日志配置、接收各模块的日志请求。
  • LoggerTarget:日志输出端的抽象接口,定义output(String msg)方法。
  • ConsoleTarget、FileTarget、RemoteTarget:三种具体输出实现。
  • TargetFactory:采用简单工厂(型号不大会变化),根据配置创建LoggerTarget实例,这个工厂对象本身由LoggerManager拥有。

这样拆分后,模块之间完全不直接依赖LoggerTarget的具体实现类。比如订单模块需要打日志,它只调用LoggerManager.getInstance().debug("订单创建成功"),LoggerManager再根据当前配置的target从TargetFactory取一个输出端对象。新增一种日志输出端(比如钉钉、企业微信告警),只需要写一个新Target类,在工厂里注册一下,整个业务模块零改动。

4.2 核心代码实现与设计要点

Target接口和实现类:

public interface LoggerTarget { void output(String msg); } public class ConsoleTarget implements LoggerTarget { @Override public void output(String msg) { System.out.println("[console] " + msg); } } public class FileTarget implements LoggerTarget { private String filePath; public FileTarget(String filePath) { this.filePath = filePath; } @Override public void output(String msg) { // 追加写入文件的具体IO逻辑 } }

TargetFactory,用简单工厂就够了:

public class TargetFactory { public static LoggerTarget create(String type, Properties config) { switch (type) { case "console": return new ConsoleTarget(); case "file": return new FileTarget(config.getProperty("log.file.path")); case "remote": return new RemoteTarget(config.getProperty("log.remote.url")); default: throw new IllegalArgumentException("unsupported target: " + type); } } }

LoggerManager采用静态内部类单例:

public class LoggerManager { private LoggerTarget target; private LoggerManager() { Properties config = loadConfig(); String targetType = config.getProperty("log.target.type", "console"); target = TargetFactory.create(targetType, config); } public static LoggerManager getInstance() { return Holder.INSTANCE; } private static class Holder { private static final LoggerManager INSTANCE = new LoggerManager(); } public void debug(String msg) { target.output("[DEBUG] " + msg); } }

这个设计最关键的地方是构造时机与控制反转:LoggerManager的构造方法里读取配置并创建对应的Target,而这个构造过程只会在Holder类第一次被加载时执行一次,也就是getInstance第一次被调用时。后面无论哪个模块调用getInstance,拿到的都是已经完全初始化好的实例,Target也已经根据最新配置创建完毕。你若是在初始化完成后修改了配置文件,这里不会生效,因为单例的构造已经执行过了——这是单例的固有限制,也需要在业务上明确"配置在启动时读取一次即可"。

这里要注意一个常见设计失误:有人会把"根据配置选择Target"的逻辑直接放进LoggerManager里,而不是抽到TargetFactory里。如果后面Target类型变多了,LoggerManager类就会变成"既管日志入口,又管创建Target"的职责混杂类。职责分离后,LoggerManager只关心"日志往哪写"不关心"怎么写",TargetFactory只关心"怎么创建"不关心"谁会来要"。这也是工厂模式最核心的价值——边界清晰。我自己实际写的时候还会把LoggerManager设计成接口+实现,方便测试时替换mock,这也是一种策略模式的思路,但放在这两个模式的基础之上讲显得复杂了,项目需要时再升级就可以。

4.3 为什么不能只用一个模式

有人可能会问:LoggerManager直接用饿汉单例,然后在构造方法里写if-else判断用哪个Target,这样不行吗?行是行,项目小的时候完全够用。但你要考虑扩展和维护成本:如果团队要加一种"批量异步日志输出",你需要改LoggerManager的构造方法;如果还要加一种"按日志级别分流输出",又要改LoggerManager。一个入口类的构造方法里塞满了各种判断逻辑,每个新需求都来改一遍这个类,单例类会慢慢膨胀成一个"上帝类"。把创建逻辑迁到工厂单独维护,LoggerManager的职责就稳定了——不管日志类型怎么变,它的构造方法里始终只有一行target = TargetFactory.create(...)。

单例解决了"全局唯一入口"的问题,工厂解决了"入口内部该创建什么对象"的问题。这两种模式的联合使用,实际上是很多框架底层的老套路:Spring的ApplicationContext本身就是一个大型单例,同时它又承担了BeanFactory的角色,负责创建和管理所有Bean对象。理解了这个套路,你看很多框架源码时会恍然大悟:原来里面到处都是这两个模式的影子。

5. 高频踩坑与排查记录:写给容易翻车的人

5.1 Fragment单例与ViewBinding的冲突

搜热词里出现"fragment 单例模式 oncreateview binding",我猜大概率是有人想在Fragment里用单例方式持有ViewBinding对象,结果各种空指针、内存泄漏。这里明确说一下:Fragment自身不应该做单例,ViewBinding更不适合被单例持有。ViewBinding绑定的是具体某个View的生命周期,Fragment销毁后View也销毁了,binding持有的View引用就成了"死引用",再往后你想用它更新界面,拿到的可能是已经detach的View,轻则空指针,重则界面改不动但内存还在泄漏。我在项目里见过新人在Fragment的onCreateView里把binding存到一个单例Utils类中,结果页面切换几次后就出现了界面状态错乱。正确做法是binding作为Fragment的成员变量,在onDestroyView里置null,随Fragment的生命周期走。

5.2 双重检查锁的指令重排问题

Java的DCL如果不加volatile,前面我讲过会出现半初始化对象。但实际开发中还有个变种问题:有些同学把volatile加在了方法的参数上,或者加在了类字段上但不够彻底,导致实例的可见性也出问题。我排查过一个线上bug,单例在某个线程中拿到的是null,另一个线程其实已经初始化过了,就是因为漏了volatile导致线程缓存问题。记住一个口诀:DCL里单例引用字段一定要volatile,而且只能是字段。

5.3 单元测试里怎么打破单例的限制

很多团队在写单元测试时被单例坑过:单例的私有构造函数没法mock,你没法在测试用例之间重置状态。我自己的做法是给单例类增加一个包内可见的resetForTest()方法或者用反射在测试里重置INSTANCE。这不是什么高大上的技巧,但在团队工程规范里很实用。如果你用的是Spring,直接把单例Bean的作用域改成prototype放在测试profile里也行,但System.out.println式的Demo项目就没必要为这个纠结。

5.4 简单工厂的class类参数写法

有些人写工厂时会用Class作为参数:

public static <T extends Weapon> T createWeapon(Class<T> clazz) { try { return clazz.getDeclaredConstructor().newInstance(); } catch (Exception e) { throw new RuntimeException(e); } }

这是利用了Java反射机制,好处是新增产品类时工厂完全不用改,坏处是编译器无法检查你传入的Class有没有无参构造,容易在运行时才炸出来。我之前在一个项目里用过这种方式,后来发现一个产品类的无参构造里加了带参逻辑,导致运行时疯狂报错排查了很久。如果产品类构造逻辑复杂,不推荐这种写法,老老实实把创建逻辑写在工厂里,出了问题也方便排查。

5.5 工厂模式滥用也会造成灾难

我有一个非常深刻的教训:刚学会工厂模式那阵子,想把所有new都替换成工厂,结果项目里涌现出十几个工厂类,每个工厂类对应的产品可能只有一个实现,创建逻辑比直接new更绕,阅读代码时需要跳好几层才能看到一个真正的对象。后来我给自己定了个规矩:如果工厂里只有一个产品的分支、并且这个产品不会有其他替代实现,直接new就好,别为了模式而模式。设计模式是工具,目的是解决问题,不是为了在代码里"秀肌肉"。

6. 面试里关于这两个模式的高频追问整理

如果你是在准备面试或者期末考,这几个点几乎必考:单例模式的线程安全实现有哪些?DCL为什么需要volatile?静态内部类单例为什么是线程安全的?简单工厂、工厂方法、抽象工厂三者的区别和适用场景分别是什么?还有些面试官会故意挖坑:单例能不能被反射破坏?能不能被序列化破坏?枚举单例为什么天然免疫这两种破坏?这些内容我上面都提到了,如果你能用自己的话把"类加载机制保证线程安全"这件事讲清楚,面试官通常会认为你是真懂而不是背的。

另外要提醒一点:面试时如果有人问"你最常用哪些设计模式",不要只说单例和工厂,最好能带出它们在你实际项目中的具体场景,比如"我在网关里用单例管理了全局配置,同时通过工厂模式按路由类型创建不同的处理器"。能举一个真实场景,比背十遍定义都有说服力。

7. 最后聊聊我自己的使用体会

这两个模式我在不同项目里反复用过、也反复踩过坑,最后沉淀下来的经验就三条:第一,单例不是哪里都需要,别为了省一个对象就把一个无状态工具类设计成单体,很多时候一个普通的public类加静态工具方法就足够了;第二,工厂的边界感要把握好,不是所有对象的创建都值得抽一个工厂出来,只有创建逻辑有重复、或者产品种类有明显变化趋势时才值得;第三,模式之间是可以组合的,单例负责唯一入口,工厂负责对象生产,策略模式可以配合同一接口的不同实现切换,观察者模式可以在工厂生产出对象后自动通知订阅方,代码的世界里没有银弹,只有合适不合适。希望这篇文章能帮你少走一些我当年走过的弯路。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询