拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Python工厂模式:从单例到对象创建限制的工程实践

先说实话我对这类标题的第一反应是Java 里的工厂模式我写得多了但放到 Python 里总有一种“穿西装打领带去跑步”的违和感。这篇文章想聊的东西很明确用 Pythonic 的方式写一个带“对象创建限制”的工厂。什么是对象创建限制往小了说是单例、固定实例数量、创建频率上限往大了说还可以是参数校验、对象池复用、按名称复用实例。这些都是实际工程里非常常见的需求尤其是做连接池、缓存、配置中心这类组件时几乎绕不开。我会从需求拆解讲起逐步给出可以直接落地的代码然后对比元类、装饰器、类注册表几种实现方案的取舍最后聊聊测试和线上会踩的坑。整篇文章的目标读者是已经能熟练写 Python 业务代码但还没系统想过“如何优雅地控制对象生命周期”的人。读完你至少能判断自己项目里到底是该硬上工厂模式还是用一个简单的函数就解决了。1. 先理清“限制”到底限制什么需求拆解是第一道坎“对象创建限制”这个词太抽象了不同人说出来可能完全是两回事。我见过团队里为了“限制对象创建”专门写了一个 Abstract Factory 加 Builder结果真实需求只是“某个配置对象全局只允许初始化一次”。所以在动手写代码之前先花十分钟把需求拆清楚能省掉后面三天的重构。1.1 单例只是最基础的“创建限制”最常见的限制就是单例——一个类在整个进程生命周期里只有一个实例。Java 里要写私有构造器、静态内部类、getInstance()写法五花八门。Python 里新手的做法往往是在__init__里加个判断发现已经初始化过就 return但这里有个大坑__init__在对象内存已经分配之后才会被调用你即便 return 了ClassName()这个表达式依然会返回一个新建对象。真正拦截创建动作的钩子不是__init__而是__new__这一点我在下一章详细说。先列一下我实际遇到过的几种“创建限制”方便大家对照自己的场景单例全局唯一配置对象、日志管理器、线程池入口。固定上限最多 N 个数据库连接池、外部服务客户端实例、License 受限的资源。窗口期限流单位时间内最多 N 次创建防止误操作疯狂 new 对象打爆下游。参数维度限制相同参数只创建一个按 URL 缓存 HTTP 客户端、按队列名复用消费者实例。创建前后条件校验参数互斥、环境开关测试环境禁止创建生产级连接。1.2 别混淆“限制创建”和“复用对象”这里我踩过很深的坑。有一段时间我在做消息队列的消费者管理需求描述是“限制同一个队列不要创建多个消费者”一开始我写了个计数器每次创建都加一超过阈值直接抛异常。后来发现线上消费者经常因为断线重连而重建旧对象还没被垃圾回收计数已经到顶导致新消费者永远创建不出来。真实需求根本不是“最多能创建几个”而是“同一个队列应该复用同一个消费者”。这两种需求的实现方式完全不同限制数量核心是拒绝超出的创建请求复用对象核心是把已有对象返回给调用方本质上是一个字典缓存。很多设计模式在落地时被搞复杂就是因为没有区分这两者。工厂模式本身只是把“创建逻辑”从业务代码里抽离出来而“限制”和“复用”是附加在创建逻辑上的策略可以独立实现也能组合使用。后面第 4 章我会把这两种策略组合到一起。2. 最小实现为什么说__init__里做拦截是伪拦截如果要给“对象创建限制”写一个最简版本直接改__new__就够了。但在给出代码之前必须先讲清楚 Python 对象创建的执行顺序否则你会在__init__里加一万个判断也拦不住对象被创建出来。2.1__init__拦截不到的真相对象已经“出生”了看一下Foo()这个表达式背后的字节码级行为调用type(Foo).__call__也就是元类的__call__默认是type.__call__type.__call__内部先调用Foo.__new__(Foo, ...)拿到一个实例如果__new__返回的是Foo的实例type.__call__再自动调用Foo.__init__(instance, ...)。所以在__init__里 return、raise、加判断对象内存都已经分配好了。你做的最多是“创建一个对象然后立刻销毁它”而不是“阻止创建”。对普通业务代码来说区别不大但在高并发或创建成本极高的场景里这种无效分配会造成不必要的性能损耗更关键的是你写出来的代码会误导后来者让人以为“在__init__里判断就能实现单例”。2.2 用__new__实现实例数量上限直接看代码import weakref class BoundedConnection: 最多只能存在 max_instances 个存活实例 _max_instances 5 _alive weakref.WeakSet() def __new__(cls, *args, **kwargs): if len(cls._alive) cls._max_instances: raise RuntimeError( f最多只能创建 {cls._max_instances} 个实例当前存活 {len(cls._alive)} 个 ) instance super().__new__(cls) cls._alive.add(instance) return instance def __init__(self, name: str): self.name name这里有几个细节值得讲一下为什么用WeakSet而不用计数器计数器最大的问题是“只增不减”对象被垃圾回收后计数不会自动减一。WeakSet里存放的是对象的弱引用实例从强引用域消失后它会自动从WeakSet中移除这样len(cls._alive)才是“当前存活实例数”。这是这个实现里最核心的 Pythonic 设计。为什么在__new__里 add而不是在__init__里 add因为在__new__返回之前实例还不存在WeakSet.add需要一个可 hash 的对象__new__里拿到的 instance 完全符合条件。更重要的是如果__init__后续抛异常这个对象虽然没有被业务持有但已经从WeakSet里消失了——因为外层没有强引用实例马上会被回收。所以放在__new__里加反而不会造成“无效实例占坑”的问题。为什么用 RuntimeError 而不是自定义异常类型单看这段代码自定义InstanceLimitExceeded异常更语义化。但实际项目里我建议先想清楚调用方要捕获什么如果只是要一个“创建失败”的标志Python 内置的RuntimeError已经足够。过度自定义异常类型除了让except分支变多并没有其他好处。2.3 线程安全check-then-act 的并发隐患上面这个版本在单线程环境完全没问题但它存在一个典型的竞态条件两个线程同时进入__new__都读到len(cls._alive) 4都通过了if判断然后各自创建了一个新实例最终存活实例数变成 6超出上限。这个 bug 在测试环境几乎不会被发现因为需要精确的线程调度时机。解决方式很简单加一个类级别的锁import threading class ThreadSafeBoundedConnection: _max_instances 5 _alive weakref.WeakSet() _lock threading.RLock() def __new__(cls, *args, **kwargs): with cls._lock: if len(cls._alive) cls._max_instances: raise RuntimeError(...) instance super().__new__(cls) cls._alive.add(instance) return instance这里有两个要点用RLock而不是Lock。RLock允许同一个线程重入如果你的类有继承关系子类的__new__里调super().__new__实际上不会重新进入当前类的__new__但 RLOCK 可以在更复杂的继承或混入场景里避免死锁。退一步说RLock 的性能开销和 Lock 差别极小没必要在这里抠性能。锁必须在检查之前就拿到。len(cls._alive) cls._max_instances这个判断和super().__new__必须是原子操作。如果锁只包住 add 那一步检查还是并发的问题依旧存在。这是典型的 check-then-act 竞态思路和写缓存时“先查再写要加锁”是同一个道理。3. 用元类和装饰器重构从“能用”走向“Pythonic”__new__版本很直接但它有一个明显的坏味道限制逻辑和业务类耦合在一起。如果我有十个类都需要“限制创建数量”就得把这套_alive、_lock代码复制十份。这不是 Pythonic 的做法。Pythonic 的核心是 DRY但更重要的是“以符合语言惯用法的方式做抽象”。3.1 元类版本把限制逻辑收敛到__call__前面说过Foo()本质是调用元类的__call__。所以最“正统”的拦截点其实在元类里。我们可以在元类层面定义一套可复用的“实例数量限制”逻辑import threading import weakref class LimitedInstancesMeta(type): 为类提供 max_instances 属性强制限制存活实例数量 def __new__(mcs, name, bases, namespace, **kwargs): cls super().__new__(mcs, name, bases, namespace, **kwargs) cls._alive weakref.WeakSet() cls._limit_lock threading.RLock() return cls def __call__(cls, *args, **kwargs): max_instances getattr(cls, max_instances, None) if max_instances is not None: with cls._limit_lock: if len(cls._alive) max_instances: raise RuntimeError( f{cls.__name__} 最多允许 {max_instances} 个存活实例 ) instance super().__call__(*args, **kwargs) cls._alive.add(instance) return instance return super().__call__(*args, **kwargs) class DatabaseConnection(metaclassLimitedInstancesMeta): max_instances 3 def __init__(self, dsn: str): self.dsn dsn这个元类有几个值得注意的设计_alive和_limit_lock是在__new__元类的__new__里动态加上的子类不需要自己声明。这符合“限制逻辑集中管理”的目标。max_instances没有硬编码在基类里而是约定成类属性。没有设置该属性的类走普通创建流程元类完全不干预。super().__call__内部会走__new____init__所以默认行为完全不变。但元类方案也有取舍。第一元类会增加团队的理解成本很多 Python 程序员看到metaclass就开始皱眉第二如果super().__call__里__init__抛异常此时实例还没加入_alive因为super().__call__没返回这个实例就直接丢了不会有残留问题但创建的并发窗口变大了需要在锁内执行super().__call__这等于把整个__init__过程都串行化了。我自己的建议是元类方案只适合团队已经对元类有共识、且限制策略确实需要在多个类之间复用的场景。如果你只是为了一个单例类直接上元类反而过设计。3.2 装饰器版本限制的对象从“类”变成“工厂函数”如果“工厂”真的是一个函数——接收参数返回实例——那用装饰器来描述限制就非常自然。尤其是当你的创建逻辑不是简单的ClassName()而是涉及参数变换、缓存、重试等操作时装饰器的表达能力比元类更强import functools import threading import weakref def limit_instance_count(max_instances: int): 装饰器限制工厂函数创建出来的实例存活数量 def decorator(factory_func): alive weakref.WeakSet() lock threading.RLock() functools.wraps(factory_func) def wrapper(*args, **kwargs): with lock: if len(alive) max_instances: raise RuntimeError( f工厂 {factory_func.__name__} 最多允许 {max_instances} 个存活实例 ) instance factory_func(*args, **kwargs) alive.add(instance) return instance return wrapper return decorator limit_instance_count(max_instances2) def create_expensive_client(endpoint: str): # 这里是真实的创建逻辑可能涉及网络握手、证书加载等 return {endpoint: endpoint}这个方案的好处非常明显不侵入类定义。任何“能返回对象的函数”都可以被装饰哪怕返回的是第三方库的对象。异常安全。factory_func如果抛异常实例不会加入alive不会出现“半成品占名额”的情况。对调用方完全透明。functools.wraps保留了原函数的__name__、__doc__等属性调用方感知不到包了一层。3.3 三种方案怎么选一张表说清楚方案侵入性复用性可测试性适用场景__new__改写中改业务类低复制代码高单个类需要限制且类本身归你管元类中改类声明高多个类共享低元类逻辑难 mock多类统一限制团队认可元类装饰器低不改类高任意函数都能用高装饰器可单独测工厂函数、第三方对象创建函数真实的项目里我目前用到最多的其实是装饰器方案。原因很简单现代 Python 工程里“创建对象”的动作本身往往已经收敛到一个工厂函数里了比如get_client()、build_conn_pool()。限制创建数量本质上是限制这个函数的“产出数量”所以装饰器是最贴近语义的写法。__new__方案更适合写库的时候用比如你设计了一个基础类希望所有子类自动具备限制能力。3.4 为什么说“最小实现”往往已经够用上面给了三种姿势但我要泼一盆冷水大多数项目的“对象创建限制”需求用第 2 章的__new__最小实现就够了。我见过不止一个团队为了“优雅”引进了 ABC、抽象工厂、依赖注入容器最后代码量翻了三倍线上 bug 反而更多。设计模式是工具箱里的工具不是客厅里的装饰画。什么时候才真的需要抽象当你的第二个使用方出现的时候。第一个类需要限制写死没问题第二个类也需要限制那就开始考虑提取公共逻辑。这符合“三次法则”Rule of Three——第三次重复才值得做抽象。过早抽象和过度设计是 Python 项目里最常见的复杂度来源。4. 更复杂的限制策略时间窗口、参数校验与对象池如果“对象创建限制”只是数量和单例那这篇文章就没有写下去的必要了。实际业务里更常见的是一堆组合式限制条件。这一章我把高压场景里会遇到的几类策略展开讲。4.1 基于时间窗口的限频创建有的场景下你不关心同时存活多少个对象只关心“单位时间内不要创建太多次”。比如外部 SDK 客户端频繁创建可能导致下游连接数瞬间飙升或者某个组件每创建一次就得申请一个临时 License而 License 中心本身有 QPS 限制。这种需求适合用时间窗口去限制import time from collections import deque class RateLimitedResource: _max_per_window 100 _window_seconds 60.0 _created_at deque(maxlen_max_per_window) def __new__(cls, *args, **kwargs): now time.monotonic() # 清掉窗口时间之外的时间戳 while cls._created_at and now - cls._created_at[0] cls._window_seconds: cls._created_at.popleft() if len(cls._created_at) cls._max_per_window: raise RuntimeError( f创建频率超过限制{cls._window_seconds:.0f} 秒内最多 {cls._max_per_window} 次 ) cls._created_at.append(now) return super().__new__(cls)注意几个细节用time.monotonic()而不是time.time()。time.time()受系统时间跳变影响比如自动 NTP 校时把系统时间往前拨了一小时这个窗口就会立刻失效很久。monotonic只增不减语义上就是“单调时钟”适合测量间隔。deque(maxlenN)天然适合滚动窗口。超过 maxlen 的旧数据会自动被丢弃但这里我依然写了手动 popleft 的循环因为maxlen丢弃的是“超量的新数据”不是“超时的旧数据”。窗口内时间戳是否过期需要根据now - timestamp判断。这个方案的变体是把窗口改成滑动计数 令牌桶但核心逻辑都一样记录关键动作的时间点再根据时间窗口做聚合判断。4.2 基于参数条件的实例级限制“同一个参数只允许创建一个实例”也是常见限制。写法和单例很像但它不是全局唯一而是按某个键唯一import weakref class HttpClientByEndpoint: _registry weakref.WeakValueDictionary() def __new__(cls, endpoint: str, *args, **kwargs): cached cls._registry.get(endpoint) if cached is not None: return cached instance super().__new__(cls) cls._registry[endpoint] instance return instance def __init__(self, endpoint: str): # __init__ 会被重复调用这是这个写法的一个坑 self.endpoint endpoint这段代码有一个非常隐蔽而且我在生产环境踩过的坑__new__返回缓存对象后Python 解释器依然会调用__init__。也就是说第二次HttpClientByEndpoint(http://api.a.com)的时候__new__返回了上一次缓存的对象随后这个对象又被执行了一次__init__可能把已有的状态重置了。这是 Python 对象创建机制里最容易被误解的点之一__new__决定“返回哪个对象”__init__决定“如何初始化返回的对象”两者是独立流程。这里有几个解决方案方案一用一个工厂函数替代__new__在函数里自己判断“要不要初始化”方案二在__init__里加一个if not hasattr(self, _initialized)之类的幂等保护方案三使用__new__中设置标记__init__中检测标记跳过重复初始化。我在实际代码里更倾向于方案一因为可读性最好_registry: dict[str, HttpClientByEndpoint] {} def get_client(endpoint: str) - HttpClientByEndpoint: if endpoint not in _registry: client HttpClientByEndpoint.__new__(HttpClientByEndpoint) client.endpoint endpoint _registry[endpoint] client return _registry[endpoint]这样HttpClientByEndpoint的构造函数就被完全绕过了所有创建逻辑都收敛在get_client这个工厂函数里。这也是“显式优于隐式”的体现——你一眼能看出这个函数就是唯一的创建入口而不需要去理解__new__和__init__的微妙关系。4.3 对象池复用让“创建限制”换个思路前文说过“限制”和“复用”经常会混在一起。真正的连接池处理的就是这么一个问题连接数量有上限但这个上限不是“拒绝创建”而是“创建达到上限后把空闲对象借给调用方”。Python 标准库的queue.Queue天然适合做这个import queue class ConnectionPool: def __init__(self, create_func, max_size: int 10): self._create_func create_func self._pool queue.Queue(maxsizemax_size) self._created 0 self._lock threading.RLock() def acquire(self): try: return self._pool.get_nowait() except queue.Empty: with self._lock: if self._created self._pool.maxsize: raise RuntimeError(连接池已满) self._created 1 return self._create_func() def release(self, conn): self._pool.put(conn)这段代码把“数量限制”内化成了队列的容量上限甚至都不需要显式地拒绝创建——maxsize就是_created的天然屏障。但要注意acquire里“取不到空闲连接就新建”的逻辑和_created自增之间也有竞态所以我把检查上限和自增放在同一个RLock临界区里。至于要不要用get_nowait而不是get(timeout...)取决于业务上是否允许调用方阻塞等待这里我故意用get_nowait让超限语义更明确。4.4 组合策略的挂载方式实际设计时限制条件往往不是单一条。比如某组件既要“全局最多 100 个实例”又要“同一个 endpoint 复用”还要“每分钟最多创建 20 个”。如果把所有逻辑塞进一个__new__这个类就废了后面的人根本不敢动。我的建议是用装饰器把策略叠加每个装饰器只做一件事limit_instance_count(max_instances100) limit_rate(window_seconds60, max_calls20) cache_by_arg(arg_index0) def create_complex_client(endpoint: str, options: dict): ...装饰器的顺序很重要从下往上执行。比如上面这个组合cache_by_arg先执行如果是相同 endpoint 直接返回缓存根本不会触发limit_rate只有真正需要新建对象的时候才会去检查频率和总数。这种叠加方式让每个策略都能够独立测试、独立替换是我目前最推荐的“复合限制”写法。5. 测试与边界条件限制逻辑最容易在哪些场景翻车写完限制逻辑如果不针对边界条件做测试上线必炸。我自己在这些代码上踩过的坑可以列一个清单。5.1 单元测试要覆盖三条主路径我习惯为“对象创建限制”设计三类测试正常创建未达到上限时每次创建都能成功且返回的对象是独立实例。达到上限后的拒绝第 N1 次创建必须抛出预期异常且已经创建的对象不受影响。回收后重新放行删除一些强引用触发垃圾回收后可以继续创建新实例。一个具体的测试例子import gc import pytest def test_connection_limit_and_gc_release(): pool [BoundedConnection(fconn-{i}) for i in range(5)] with pytest.raises(RuntimeError): BoundedConnection(conn-over-limit) # 删掉一个强引用并强制垃圾回收 del pool[0] gc.collect() # 此时应该可以再创建一个实例 new_conn BoundedConnection(conn-after-gc) assert new_conn.name conn-after-gc这里有个特别容易翻车的点WeakSet只有在垃圾回收真正发生时才会移除元素。测试里必须显式调用gc.collect()否则del pool[0]之后立即创建新对象可能仍然报超限。这不是代码 bug是 CPython 引用计数和循环引用检测机制之间的时序差异。线上环境遇到“删了对象却仍然超限”的情况先检查是不是有循环引用把对象留住了。5.2 递归创建、异常路径与弱引用失效递归创建是一个极少人注意但很现实的坑。假设一个对象的__init__内部会去创建另一个同类型对象而这两个类共享同一个限制器那么可能直接触发递归超限。举一个具体的伪场景class Node: max_instances 10 def __init__(self, children: list[Node]): self.children children如果你在自定义工厂里写Node([Node([])])那么内层Node创建时外层Node还没有完成创建。如果计数逻辑放在“创建前自增创建后自减”这个流程还可能勉强工作但如果放在“创建后自增”内层会比外层先记录顺序很迷惑。我的建议是限制器的语义必须是“同一时刻存活的对象数量”不是“累计创建次数”。这样面对递归创建至少不会出现“自己卡死自己”的问题——因为外层还没创建成功内层创建时外层的实例还不存在于_alive里。另一个坑是弱引用本身。WeakSet依赖对象必须可弱引用且可哈希。如果一个类定义了__slots__而忘记加__weakref__或者定义了__hash__ None那WeakSet.add会直接抛异常。这类问题在测试阶段通常不会暴露因为测试用例往往用简单的普通类一到生产环境接入了复杂的业务模型才开始报错。建议给限制器写一个“构造前置检查”在类定义时就校验能不能被弱引用def _check_weakref_compatible(cls): if cls.__hash__ is None: raise TypeError(f{cls.__name__} 不可哈希无法用于弱引用跟踪) if hasattr(cls, __slots__) and __weakref__ not in cls.__slots__: raise TypeError(f{cls.__name__} 缺少 __weakref__ 槽位)5.3 边界选择限制对象还是限制“可见实例”最后一个需要想清楚的问题是你限制的是“对象的总数”还是“对象可见实例的总数”。直接说的例子某个类没有持有实例只是把实例放到一个全局列表里用那它到底算不算“存活”从垃圾回收角度看全局列表持有的是强引用它当然算存活但从业务角度看可能这个实例已经不提供服务了。设计限制逻辑时我强烈建议把口径定义写在代码注释里否则三个月后你自己回来维护都会搞混。比如class BoundedConnection: # 限制口径被任意强引用持有的实例。只要还被引用就占用额度。如果限制的是“正在被使用的实例”那纯粹用引用计数就不够还需要接入显式的close()或release()生命周期。这种情况下把“创建”和“销毁”都通过池子来管理比依赖垃圾回收要可靠得多。6. 实战经验模块级注册表比工厂模式更常见讲完各种实现最后扯一点返璞归真的东西。很多人会把“工厂模式”简单理解成一个类图但 Python 社区真正频繁出现的模式其实是“模块级注册表 公共函数”。这种写法有时候看起来一点也不“模式”但它是最符合 Python 哲学的做法。6.1 一个更 Pythonic 的替代注册表 get_instance 函数_REGISTRY: dict[str, ServiceClient] {} def get_service_client(name: str) - ServiceClient: 按名称获取客户端不存在则创建存在则复用。 if name not in _REGISTRY: client ServiceClient(namename) _REGISTRY[name] client return _REGISTRY[name]这段代码同时实现了“单例按 key 的单例”和“懒加载”。它没有任何魔法没有改写__new__没有元类一个普通工程师看一眼就能懂。从可维护性角度看这比任何设计模式都有价值。Python 的哲学是“简单直白胜于绕弯子”这一点在对象创建的问题上体现得特别明显。那什么时候不该用这个简单版本当创建逻辑本身很复杂比如要根据配置生成不同的类、要注入依赖、要执行一系列初始化和重试逻辑那还是抽一个Factory类更合适。但注意这里的 Factory 类做的事情是“组织复杂创建过程”不是“实现创建限制”。限制逻辑依然应该独立考虑。6.2 硬套工厂模式的典型反例我 Review 过不少代码最典型的问题是一个类只有一种创建方式也没有扩展需求却硬套了 Abstract Factory接口里定义create_product_a()、create_product_b()实现类却只有一个。最后所有人都在这个抽象层上改业务抽象层越来越厚业务方越来越难受。Python 不是 Java没有“接口必须存在”的强制压力。如果你的“工厂”没有第二种子类实现没有“需要在运行时切换创建策略”的需求那就别建工厂目录了。get_service_client()一个函数足够。反过来如果你只是需要一个全局唯一的配置对象functools.lru_cache是更地道的写法from functools import lru_cache lru_cache(maxsize1) def get_app_config(): return load_config_from_env()lru_cache(maxsize1)天然就是“函数级单例”比任何单例类都简洁还自带线程安全。这也是 Python 语言工具箱里被严重低估的一个特性。6.3 给初学者的落地建议如果你正在做项目需要实现“对象创建限制”我建议按这个顺序来先用最简单的模块级注册表用字典做缓存用函数做创建入口。跑通业务确认这个限制真的有用而不是自嗨。当第二个类也需要同样限制时提取装饰器或者公共函数。这时候你对“限制口径”已经理解得更清楚抽象会更有针对性。当限制逻辑需要覆盖类继承体系时才考虑__new__或元类。而且一定要先写测试再动结构。无论如何把限制逻辑和真正的业务创建逻辑分开。创建是在做“怎么建对象”限制是在做“能不能建对象”两者混合在同一个函数里代码会越来越难维护。我在实际项目里踩过最痛的一次就是在一个大对象里同时做了“构造”“缓存”“限流”和“数据加载”结果任何一步出问题都很难定位。后来拆成create()、validate()、cache()三个独立模块问题立刻清晰了。对象创建限制本质上不是什么高深理论它就是一个非常具体的工程约定谁能造对象、造多少、什么时候造。把这个约定明确下来用什么语法去实现反而不重要。回到标题里的“Pythonic”这个词我的理解是不是炫技去用生成器、元类、描述符而是用最贴近 Python 语言习惯的方式写出别人能一眼看懂的代码。能用字典就不用类能用函数就不用类工厂能用lru_cache就不用单例模式——这些判断比掌握任何一种设计模式都重要。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门