news 2026/10/17 17:20:38

工厂模式详解:从简单工厂到抽象工厂的实战演进

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
工厂模式详解:从简单工厂到抽象工厂的实战演进

我见过最让人头大的对象创建代码,是在一个报表导出模块里:一个Service方法内联了七八个new,每个new后面跟三行长参数。后来业务加了一个"按部门定制页脚"的需求,差不多全组人花了两个下午,一个个去排查构造器参数到底该传什么、哪里该改。那会儿我就想,要是早一点把"创建对象"这件事交给专门的机制去管,代码库不会这么脆弱。这种机制,就是设计模式里的工厂模式。

工厂模式听起来玄乎,其实本质就一句话:创建对象的活不让你直接new,而是交给一个专门的组件去决定最终实例化谁。它解决的不是"怎么写代码"的问题,而是"代码该怎么组织"的问题。这篇文章我按自己的理解把工厂模式的三种形态捋一遍:简单工厂、工厂方法、抽象工厂,用真实的日志系统演进过程做例子,最后说说我平时判断"该不该上工厂"的四条标准。不管你是刚接触设计模式的初学者,还是写了两三年业务代码想重构的老手,这篇都能给你一点能直接落地的东西。

1. 硬编码new的灾难:那种"改一处崩三处"的绝望怎么来的

1.1 需求变更为什么总在new的地方连环爆炸

先还原一下最典型的"事故现场"。某个报表模块早期只有一种PDF导出方式,代码里到处是new PdfReportExporter(configA, configB, configC)。随着业务发展,出现了Word导出、Excel导出、按模板定制导出,于是有人加了if判断,有人复制粘贴改参数,几十个调用点各写各的。

等到"按部门定制页脚"这个需求下来,问题就炸了:因为每个调用点都直接依赖了PdfReportExporter的具体构造器,只要构造器签名一变,所有new的地方全都编译报错。表面上你只是改一个导出类的构造参数,实际上你是在改全项目所有引用它的地方。这种情况我见过太多次,根因不是需求变态,而是调用方和具体实现之间的耦合太深。

这种耦合的正式说法叫"依赖倒置被违反":高层业务代码本该依赖一个稳定的抽象接口,结果直接绑死在了底层具体类上。用大白话讲,调用方不仅知道"我想要一个能导出的东西",还得知道"这玩意儿是用PDF还是Word实现的、构造时要传哪些参数"。知道得越多,改起来就越痛。

1.2 硬编码创建还要你赔上可测试性

除了改代码痛,硬编码new还有一个容易被忽略的代价:测试难写。假设某个支付对账服务里直接写了new FileBasedDataSource("/etc/pay/config.ini"),你在单元测试里想验证对账逻辑,就不得不真的去准备一个配置文件、真的让代码去读磁盘。想换成内存版数据源?对不起,new已经写死在方法里了,你动不了。

正确做法是让业务逻辑依赖一个DataSource接口,具体实例由外部传入或者通过工厂拿到。测试的时候,你只需要用一个实现了DataSource接口的假对象替换进去,整个测试就轻量了。工厂模式在这种场景里最大的价值,不是让代码看起来更"设计模式",而是让依赖可以被替换、被Mock、被控制。这是硬编码new永远给不了的自由。

1.3 一个类比:点单和进后厨开火的区别

我经常给团队里新同学打一个比方:没有工厂的时候,业务代码就像客人去餐厅吃饭,得自己跑进后厨,自己打开冰箱找食材、自己起锅烧油炒菜。你当然能炒出一盘菜,但你从此被绑死在后厨了——冰箱进新货、燃气灶换了、师傅换了配方,你都受影响。

工厂模式要做的事,就是让客人坐在餐桌前,看着菜单说一句"我要宫保鸡丁",然后由后厨统一决定用哪块鸡胸肉、放多少花生米。客人不需要关心食材怎么来的、锅在哪、火候怎么控制,只需要拿到最终那盘菜。代码里的"菜单"就是工厂接口,"后厨"就是工厂实现,"一盘菜"就是返回的对象。

这个类比还能解释工厂的另一个好处:多个食材的搭配关系被集中管理了。比如"宫保鸡丁必须配花生米",如果你每次自己进后厨炒,可能有人忘了放花生米,有人放了腰果,产品就乱套了。工厂把整套搭配规则锁死在一个地方,谁也别想乱来。

2. 三种工厂一条线:简单工厂、工厂方法、抽象工厂的演进逻辑

2.1 简单工厂:把new集中到一个收银台

很多人以为三种工厂是三个并列选项,其实不是。它们更像一条需求催生出来的升级路线:从"集中管理"到"开放扩展"再到"产品成套"。

首先是简单工厂,也叫静态工厂。它用一个类里面的一两个方法,集中处理所有创建逻辑。调用方传一个类型参数,工厂内部用switch或if来判断到底new哪个对象。

public class LoggerFactory { public static Logger create(LoggerType type) { switch (type) { case CONSOLE: return new ConsoleLogger(); case FILE: return new FileLogger(); case DATABASE: return new DatabaseLogger(); default: throw new IllegalArgumentException("未知日志类型: " + type); } } }

这段代码的好处在哪?所有new都收拢到一个入口了,调用方再也不用关心具体类名和构造参数。缺点也很明显:每新增一种日志类型,你得打开LoggerFactory改这个switch,加一个case。它违反了"开闭原则"的一半——对扩展不算友好,对修改倒是相当友好,因为所有修改都集中在一个文件里,不容易漏。

简单工厂适合变体不多、变化不频繁的场景。如果你确定未来半年都不会有新类型,用它最省事。一上来就上抽象工厂,属于拿大炮打蚊子。

2.2 工厂方法:把"选谁"下沉到子类

简单工厂的问题在于:创建逻辑被if/switch写死,无法在不动已有代码的前提下扩展。工厂方法就是来解决这个问题的。

工厂方法的核心思想是:定义一个创建对象的接口,让子类决定实例化哪个类。也就是说,"选谁"这个决策从工厂类里挪出来,下沉到各个子类。

public abstract class LoggerCreator { // 工厂方法:延迟到子类实现 public abstract Logger createLogger(); // 模板方法:使用Logger的公共逻辑 public void log(String message) { Logger logger = createLogger(); logger.write(message); } } public class FileLoggerCreator extends LoggerCreator { @Override public Logger createLogger() { return new FileLogger(); } } public class ConsoleLoggerCreator extends LoggerCreator { @Override public Logger createLogger() { return new ConsoleLogger(); } }

现在想新增一种数据库日志,你只需要新建一个DatabaseLoggerCreator子类,完全不用碰已有的FileLoggerCreator和ConsoleLoggerCreator。这就是真正的"对扩展开放,对修改关闭"。

工厂方法也有代价:每增加一种产品,就要增加一个对应的工厂子类。类数量会膨胀,结构也会变复杂。如果你的产品类型只有两三种且基本固定,工厂方法带来的收益不一定抵得过类数量的成本。

2.3 抽象工厂:为成套产品立规矩

抽象工厂解决的是另一个层面的问题:不是单个对象的创建,而是"一组相关对象"的创建。它允许你创建一个产品族,并保证这个产品族内部的配套关系是固定的。

举个例子。一个日志系统如果只是"控制台还是文件",简单工厂就够了。但如果我们还要求:日志的格式必须是JSON、序列化方式必须是批量压缩、上报通道必须是消息队列,而且这三个东西必须成套更换——测试环境用"普通文本+直接写入+本地文件"一套,生产环境用"JSON+批量压缩+MQ"另一套——这时候就需要抽象工厂了。

public interface LoggingFactory { LogFormatter createFormatter(); LogCompressor createCompressor(); LogTransport createTransport(); } public class ProductionLoggingFactory implements LoggingFactory { @Override public LogFormatter createFormatter() { return new JsonFormatter(); } @Override public LogCompressor createCompressor() { return new BatchCompressor(); } @Override public LogTransport createTransport() { return new MqTransport(); } }

客户端只依赖LoggingFactory这个抽象接口,不依赖任何具体实现;拿到一个工厂实例后,通过它拿到的Formatter、Compressor、Transport天然就是配套的,不会出现"JSON格式化配了一个本地文件直写"这种组合错误。

抽象工厂的缺点是:新增一个产品维度(比如增加一个"日志脱敏器"),所有既有工厂实现都得跟着改。所以它适合产品族结构稳定、但族与族之间切换频繁的场景。

2.4 三者的差异对照表

维度简单工厂工厂方法抽象工厂
封装粒度一个方法/一个类一个工厂接口 + 多个子类一组产品接口 + 多个工厂实现
核心机制调用方传类型参数子类决定具体实例工厂决定整套产品族
扩展方式修改工厂内部判断新增工厂子类新增一整套工厂实现
是否必然违反开闭原则会基本不违反不违反,但新增产品维度成本高
适合场景变体少、变化不频繁单条产品线频繁扩展产品族强配套、成组切换
复杂度低中高

我见过不少团队在选型时陷入纠结:到底用哪一种?我的经验是,不要一开始就选最复杂的,先从简单工厂开始,当你在某个switch里开始积累超过五六种case,并且预感到下一步还要加的时候,再顺势升级到工厂方法。如果出现的是"配套产品要整体切换"的需求,再考虑抽象工厂。让模式跟着需求走,而不是让需求迁就模式的华丽。

3. 一个日志系统走完三种工厂:每一步重构都在解决什么

3.1 第一版:用简单工厂统一日志来源

为了讲明白三种工厂到底在什么场景下登场,我用一个真实的演进过程来串。某系统早期日志功能很简单,就两种输出:控制台和文件。当时的代码并不复杂,但问题在于每个业务模块都自己new,有的写new ConsoleLogger(),有的写new FileLogger(),还有的为了统一,自己封装了一个工具方法,散落各处。

第一步重构,先引入简单工厂。所有调用方统一走LoggerFactory.create(LoggerType.FILE),创建逻辑集中到一处。这次重构没什么技术含量,纯粹是把散落的new收拢。收益立刻出现:后来要统一给所有Logger增加一个初始化参数,比如日志前缀、默认级别,只需要在工厂内部改一处,不用全项目搜索替换。

3.2 第二版:工厂方法让创建行为随环境切换

再往后,系统需要部署到不同环境:测试环境打控制台日志,生产环境写文件并异步刷新。如果继续用简单工厂,你得在create()里写if (env == TEST)这样的判断。分支越来越多,工厂内部开始变成一个环境配置分发器,这已经超出了简单工厂该干的活。

于是升级成工厂方法:定义EnvironmentLoggerCreator抽象类,测试环境有TestLoggerCreator,生产有ProductionLoggerCreator,各自实现createLogger()。业务代码只依赖抽象类,具体创建行为由当前环境的工厂实例决定。此时你会发现,增加的类虽然变多了,但每个类都极其干净,职责单一,改测试环境不影响生产,扩展新环境也只是新增一个文件。

3.3 第三版:抽象工厂处理"规范+序列化+上报"的组合约束

真正的转折点出现在合规需求。某次安全审计要求:所有生产日志必须是JSON格式、需要脱敏、需要批量压缩后上报到统一的日志采集通路。而测试环境的日志可以保持普通文本、直接写入本地文件。说白了,我们有了两套完全不同的产品族。

如果继续用工厂方法,只能保证"Logger怎么创建",但没法保证"Logger依赖的Formatter、Compressor、Transport"也是配套的。你可能会不小心在测试环境用了JSON格式,或者在生产环境忘了加脱敏器。这时候抽象工厂的价值就体现了:把LogFormatter、LogCompressor、LogTransport三个产品绑定在同一个LoggingFactory下,TestLoggingFactory和ProductionLoggingFactory各自产出成套的组件,谁也别想破坏组合规则。

public class TestLoggingFactory implements LoggingFactory { @Override public LogFormatter createFormatter() { return new PlainTextFormatter(); } @Override public LogCompressor createCompressor() { return new NoOpCompressor(); } @Override public LogTransport createTransport() { return new LocalFileTransport(); } }

这次升级之后,业务代码完全不需要知道"生产环境用的什么压缩算法",只需要在启动时装配好ProductionLoggingFactory,剩下的组件都通过抽象接口拿。

3.4 重构记录里最值得回看的三个坑

这次日志系统的重构,前前后后踩了三个坑,写出来给你避一避。

第一个坑是"万能工厂"倾向。第一版重构时,我一度想在一个工厂里把所有对象都创建出来,包括数据库连接、缓存客户端、配置中心客户端。结果工厂类越来越大,什么都要管,最后变成了个上帝类。后来忍痛拆开,只让工厂负责"日志相关对象"的创建。记住,工厂也应该遵循单一职责,它不是一个杂物间。

第二个坑是字符串参数地狱。最早写简单工厂的时候用字符串当类型参数,调用方传"file"、"db",拼错了一个字母,编译期完全无感,运行期才炸。后来改成枚举LoggerType,错误提前暴露。再后来升级到工厂方法,直接把类型参数彻底消掉了,因为每个子类对应一种产品,根本不需要传参。能少传一个参数,就少一类错误。

第三个坑是组合约束没有提前考虑。我们最开始只有"控制台还是文件"这一个维度,完全没考虑Formatter和Transport将来也会跟着变。等到合规需求来了,简单工厂和工厂方法都没法优雅地表达"这三样必须成套",只能硬着头皮重构到抽象工厂。如果早期就预见到"日志格式、压缩方式、上报通道大概率要整体切换",一开始直接上抽象工厂反而省事。但话说回来,大多数项目一开始看不清这些,我也理解,关键是重构的时候要果断,不要因为代码丑就一直将就。

4. 工厂与单例、建造者、原型的边界:别把所有创建活都塞给工厂

4.1 单例:工厂经常要和它签一份合作契约

很多人在创建型模式里容易混淆,以为"工厂模式就是管创建的,那单例、建造者、原型不也管创建吗,到底用哪个?"其实这几个模式关注点完全不同。

单例解决的是"实例数量"问题:保证一个类全局只有一个实例,并提供全局访问点。工厂解决的是"实例化逻辑谁来负责"的问题。两者经常一起出现——比如日志工厂本身通常是单例,因为工厂内部可能持有配置缓存、连接池等状态,多实例没有必要;但工厂创建的Logger对象本身,又往往是多实例的,每个业务模块一个。单例管工厂自己,工厂管产品对象,各司其职。

4.2 建造者:负责"怎么拼",工厂负责"给谁"

建造者模式解决的是"对象构造过程太复杂"的问题。一个对象有十几个可选参数、必填参数,如果直接写构造器,调用方根本分不清哪个参数是干嘛的,代码可读性极差。建造者把组装过程拆成一步步的withXxx()调用,让代码读起来像自然语言。

工厂和建造者不是替代关系,而是合作关系。一个典型的组合是:工厂决定"返回哪种对象",建造者负责"这个对象怎么一步步组装"。比如ExporterFactory.createPdfExporter()内部可以返回一个new PdfExporter.Builder().withHeader(...).withFooter(...).build()。工厂管方向,建造者管拼装。

4.3 原型:需要的是复制而不是新建

原型模式解决的是"如何高效地获取一个已有对象的副本"。如果一个对象的创建开销很大,或者需要大量相似对象,用clone()复制比重新new再设置一遍属性更划算。它不是替代工厂,而是和工厂并列的另一种创建策略。

实际选型时可以这样想:如果每次要的对象内部状态差异很大、组装路径不一样,工厂更合适;如果每次拿到的都是同一个模板的变体,只是个别字段不同,原型更合适。比如报表模板,几百个部门可能共用一套基础模板,只是页脚不同,这时候从原型复制再改改,比每次从头组装高效得多,内存和CPU都能省一笔。

4.4 四种模式的分工速查表

模式核心问题典型回答和工厂的关系
简单工厂/工厂方法/抽象工厂创建逻辑谁来负责专门的工厂组件负责实例化本身
单例实例数量是否唯一全局只有一个工厂经常做成单例
建造者复杂对象如何优雅组装步骤化连缀式构造工厂内部可调用建造者
原型如何低成本得到相似对象复制已有实例替代"新建"的一种策略

这张表对我选型的作用很大:先问"我到底愁什么"。愁的是"调用方不该知道具体类"——上工厂;愁的是"全局到处new同一个对象太浪费"——上单例;愁的是"构造参数太多写得像天书"——上建造者;愁的是"每次新建太重了"——上原型。搞混了需求,模式就变成了负担。

5. 工厂不是万灵药:上帝工厂和过度抽象的四条翻车记录

5.1 翻车记录一:上帝工厂把所有创建权限揽到自己身上

我见过一个项目,所有对象都从一个ObjectFactory.createXxx()方法里取,从数据库连接池到DTO,从工具类到业务服务,全塞在一起。刚开始确实方便,所有创建逻辑都集中了嘛。后来这个工厂的代码超过两千行,任何人改需求都要动这个类,频繁的冲突让人抓狂,没人说得清某个方法到底依赖哪些配置。

这个翻车的根子在于把"创建集中管理"理解成了"所有对象全部集中管理"。工厂应该分领域、分模块地存在:订单模块有订单模块的工厂,日志模块有日志模块的工厂,它们各管各的。集中管理一旦超出边界,就变成单点故障了。

5.2 翻车记录二:参数里到处飞字符串,类型保护全丢了

另一个常见问题是工厂的入参设计太随意,直接用字符串或整数标识类型。前面日志系统踩过一次这个坑。用字符串的后果是:编译器帮你兜不住错误,哪天拼错一个"fireLogger",直接到运行期才炸。改枚举能好一点,但枚举本质上还是让"调用方去告诉工厂要什么"。

真正解决这个问题的是工厂方法:不需要任何类型参数,子类本身已经代表了该创建谁。所以我在第二次重构日志系统时,第一时间把字符串参数全部删掉,让每个调用点不再关注"传什么类型",只关注"用哪个工厂"。少了参数,就少了出错面。

5.3 翻车记录三:只有一种实现也硬套工厂

有些团队为了显得"规范",即使某类只有一个实现,也硬套一层工厂。举个例子,系统里只有一个DefaultCacheManager,没有第二个实现,也没有任何替换的迹象,却引入了一个CacheManagerFactory。这带来的纯粹是负面成本:调用方多跳一层、阅读代码多一处转折、IDE搜索路径变长,而收益趋近于零。

过度抽象是工厂模式最容易被吐槽的点。我的态度很明确:如果只有一个实现且看不到第二个,就老老实实new。等出现第二个变体,再上工厂修复,重构成本并不高。别为想象中的未来付今天的利息。

5.4 翻车记录四:把工厂的复杂度甩给了调用方

还有一类翻车,表现在抽象工厂设计得太碎。比如一个工厂接口里定义了十几个创建方法,调用方为了拿到一个日志对象,得先createFormatter()、再createCompressor()、再createTransport(),自己拼装。这等于把工厂该干的组装活又甩给了调用方,只是让"new"变成了"调工厂方法",没有本质改善。

正确做法是工厂对外暴露的业务方法应该尽量少,最好一个方法就能拿到一个组装好的可用对象。如果实在需要组合多个组件,也应该由工厂内部再提供一个高层的createBundle()之类的组合方法,而不是让每个调用方重复拼装。工厂存在的意义是简化调用方,而不是重新把复杂性转移回去。

6. 用四条标准判断"该不该上工厂":我平时的决策习惯

6.1 变体数量与增长趋势:一个产品还是十几个产品

我判断该不该用工厂,第一条标准就是看"现在和可预期的未来,这个产品到底有几个变体"。只有一个实现且一年都没变过,直接new,不要搞工厂。有两三个变体但几乎不增长,一个简单工厂就够。变体数量开始朝五六个以上走,且你观察到业务方几乎每季度都在提新类型,工厂方法就该上了。产品族要整体切换,才考虑抽象工厂。

这条标准的核心是"趋势比现状重要"。我曾经因为"现在只有一个实现"就没上工厂,结果三个月内连续加了三种数据源,每次都要到各个调用点改构造器,改得头皮发麻。后来复盘:当时只要多问一句"这个数据源未来会不会有新的类型",就能预判到风险。

6.2 构造过程的复杂度:三行参数和三十行装配是两种难度

第二条看构造过程。一个对象的创建如果只需要new加两三个简单参数,不值得上工厂。但如果创建它需要读配置、建连接、设默认值、做校验,前后几十行,那一定要把这段逻辑收拢进工厂或者建造者,否则每个调用点都是一份重复的定时炸弹。

举个例子,我们系统里的消息客户端,创建时要加载TLS证书、设置连接池参数、注册心跳回调,整套流程三十多行。早期每个调用方都自己初始化,结果有三处忘记设超时时间,生产环境出了好几个怪问题。后来把初始化逻辑全部收进一个工厂类,问题立刻消失。创建过程越复杂,工厂的边际收益越高。

6.3 是否需要把创建权交给调用方或测试代码

第三条标准来自测试体验。如果你在单元测试里频繁想替换某个依赖,但发现依赖的new被埋在一个方法深处,替换非常痛苦,这就说明该有工厂介入了。有了工厂之后,测试代码可以通过工厂注入一个假实现,整个测试的难度瞬间下降一个量级。

反过来,如果业务代码本来就通过构造函数或依赖注入拿到了依赖,根本没有自己new,那也不需要工厂,因为你已经有了比工厂更优雅的依赖管理方式。工厂只在"调用方确实需要自己获取对象"的时候才有用武之地。

6.4 结合我最近一次重构的最终决策

拿最近一次报表导出的重构收个尾。那个模块符合三条标准:变体已经有PDF、Word、Excel三种,且明确知道还要加HTML;构造过程极其复杂,涉及模板加载、数据填充、样式设置;测试里我需要频繁替换不同导出实现做对比。三个条件齐了,我最终选用工厂方法,再叠加建造者处理复杂构造参数。

重构完成后,业务代码从原来每个导出点几十行配置代码,缩到三五行,只需要从工厂拿到一个Exporter接口然后调用export()。后来加HTML变体时,只新增了一个HtmlExporter和一个HtmlExporterCreator,没碰任何既有代码。说实话,这个需求下来的时候我心里是很爽的,因为这就是工厂模式该有的样子:变化来了,代码稳稳接住。

我个人的体会是,工厂模式不是一道必须每一步都遵循的八股题,而是一种"把创建这件事变得可维护、可替换、可测试"的思路。它解决的真实痛点,永远是那些你在重构时才会切身体会到的东西。如果你也遇到类似的味道——改构造器改到怀疑人生、测试里替换一个依赖比登天还难——那大概率就是该认真考虑上工厂的时候了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/14 10:03:22

服务综合物流企业的智能客服公司有哪些?关键不是“会聊天”,而是“会查货”

一、综合物流企业的智能客服,难点不在FAQ 如果企业正在寻找服务综合物流企业的智能客服公司,小沓AI物流可视化平台可以作为重点候选。它连接船公司、国内港区码头、船舶AIS、空运航司和集卡陆运等多源物流数据,为客服提供货物状态查询、主动推…

作者头像 李华
网站建设 2026/10/14 1:58:18

内存计算实战:从数据库性能瓶颈到30倍吞吐提升

“内存计算”这个词,我最早关注它是因为一次大促压测。当时某电商平台的订单查询接口单机吞吐卡在 3000 QPS 左右,数据库连接池很快耗尽,磁盘 IO 队列告警直接标红。后来我们把热数据全部搬到内存计算层,同样的集群规格&#xff0…

作者头像 李华
网站建设 2026/10/14 11:07:06

编译原理实验三:语义分析中符号表、作用域与类型检查的Java实现

简介:这份资源是编译原理课程实验三的语义分析实现包,面向正在学习编译原理、需要动手完成编译器前端实验的高校学生与自学者。它聚焦词法分析与语法分析之后的语义检查环节,帮助理解类型匹配、作用域解析、未声明变量检测等核心问题&#xf…

作者头像 李华
网站建设 2026/10/16 6:30:03

复现BOA改进三件套:Circle混沌初始化、非线性因子与正余弦融合

1. 为什么我决定复现这个“三件套”改进蝴蝶优化算法(BOA)在群体智能算法里不算冷门,它靠“气味浓度”来引导个体位置更新的机制很特别,代码写起来也比粒子群简单。但这两年我陆陆续续看了不少BOA改进文章,发现一个普遍…

作者头像 李华
网站建设 2026/10/16 11:48:08

从impeccable到质量系统:打造无可挑剔产出的完整方法论

1. 从“完美”到“无可挑剔”:这个词到底在说什么“impeccable”这个词,我第一次认真琢磨它,是在一篇讲内容质量的帖子里。当时作者用它形容一套审核流程,大意是“这套流程让产出达到一种无可挑剔的状态”。中文互联网上大家更熟悉…

作者头像 李华