90%的C#项目用静态Create方法+switch表达式即可;仅当需成组创建关联对象(如UI组件族)时才用抽象工厂,且工厂内部须通过IServiceProvider获取实例而非new。

别急着套 GoF 模板,先看你的场景是否真需要工厂模式——90% 的 C# 项目用一个静态 Create 方法 + switch 表达式就足够了。
什么时候该写工厂类,而不是直接 new?
当你反复在业务代码里看到类似这样的片段:
if (type == "pdf") return new PdfExporter(); else if (type == "csv") return new CsvExporter(); else throw new NotSupportedException();
——这就是信号:创建逻辑开始污染业务主线。此时应该抽离,但不等于必须立刻建 AbstractExporterFactory 和五个子类。
- 如果只有 2–4 种类型,且不会频繁增删,用
static class ExporterFactory+switch表达式最轻量 - 如果类型由配置文件或用户输入决定(比如
appsettings.json里的"exporter": "xlsx"),工厂内部应读取配置后分发,但配置解析必须提前完成,不能每次调用都去读文件 - 如果不同环境要加载不同实现(如测试时用
FakeExporter),工厂方法本身得支持注入IServiceProvider,而不是自己new
C# 8+ 推荐的简单工厂写法:用 switch 表达式代替 if-else
传统 if-else 容易漏分支、难维护;switch 表达式强制覆盖所有枚举值,编译期就能捕获遗漏。
- 返回类型必须是公共接口或基类,比如
IExporter,不能是object - 枚举定义要包含全部可能值,
_ => throw分支不可省略 - 不要在 case 分支里塞构造函数参数逻辑,例如
new PdfExporter(options)—— 这会让工厂变重;参数应由上层传入,或通过 DI 获取
示例:
public static IExporter CreateExporter(ExportFormat format) =>
format switch
{
ExportFormat.Pdf => new PdfExporter(),
ExportFormat.Csv => new CsvExporter(),
ExportFormat.Xlsx => new XlsxExporter(),
_ => throw new ArgumentException($"Unknown export format: {format}")
};和 DI 容器共存时最容易踩的坑
你已经在 Program.cs 里注册了 services.AddScoped<IExporter, PdfExporter>(),但工厂里又写了 new PdfExporter() —— 这会导致:
- 依赖未注入(比如
PdfExporter构造函数里需要的IFileService是 null) - 生命周期错乱(注册为
Scoped,但new出来的是瞬态对象) -
IDisposable不会被自动释放
正确做法:工厂内部通过 IServiceProvider 获取实例,而不是自己 new:
public class ExporterFactory
{
private readonly IServiceProvider _sp;
public ExporterFactory(IServiceProvider sp) => _sp = sp;
<pre class='brush:php;toolbar:false;'>public IExporter GetExporter(ExportFormat format) =>
format switch
{
ExportFormat.Pdf => _sp.GetRequiredService<PdfExporter>(),
ExportFormat.Csv => _sp.GetRequiredService<CsvExporter>(),
_ => throw new ArgumentException(...)
};}
抽象工厂真正该出手的时刻
只有当你需要**成组创建相互关联的对象**,且这些对象必须来自同一“家族”时,才轮到抽象工厂。比如:
- UI 渲染器:同时要创建
IButton、ITextBox、IDialog,且它们必须都是 WinUI 风格或 Web 风格 - 数据访问层:一组配套的
ISqlExecutor、IParameterBinder、ISchemaReader,分别对应 SqlServer 或 PostgreSQL
这时别手写一堆工厂类,优先用 DI 注册策略:
services.AddSingleton<IUiFactory>(sp =>
IsWinUI() ? new WinUiFactory(sp) : new WebUiFactory(sp));注意:每个具体工厂内部仍应通过 sp.GetRequiredService<T>() 拿实例,而不是 new。
最易被忽略的一点:工厂类本身若带缓存(比如用 ConcurrentDictionary 存预编译的导出器实例),它被注册为 Singleton 时,内部集合必须线程安全;否则并发调用会静默损坏状态。


















