基于Crystal Reports与DevExpress的报表生成工厂类放置位置咨询
解决工厂模式下避免不必要引用的问题
你的核心问题是:在主应用仅引用包含IReport和IReportFactory的Default项目的前提下,让工厂能够创建Crystal Reports和DevExpress的具体报表实例,同时不让主应用间接依赖这两个报表库。下面是两种最实用的解决方案:
方案一:利用依赖注入(DI)解耦
这种方式适合使用DI容器的项目(比如ASP.NET Core、WPF等),核心思路是把具体报表类的注册逻辑从Default项目移到启动层,让工厂通过DI容器获取实例,而不是自己直接创建。
步骤1:修改Default项目中的ReportFactory
让工厂依赖DI容器和一个扩展名到报表类型的映射字典,去掉硬编码的创建逻辑:
public sealed class ReportFactory : IReportFactory { private readonly IServiceProvider _serviceProvider; private readonly Dictionary<string, Type> _reportTypeMap; public ReportFactory(IServiceProvider serviceProvider, Dictionary<string, Type> reportTypeMap) { _serviceProvider = serviceProvider; _reportTypeMap = reportTypeMap ?? throw new ArgumentNullException(nameof(reportTypeMap)); } public IReport Get(string extension) { var lowerExt = extension.ToLower(); if (!_reportTypeMap.TryGetValue(lowerExt, out var reportType)) { throw new ArgumentException($"不支持的报表扩展名:{extension}", nameof(extension)); } var report = _serviceProvider.GetService(reportType) as IReport; if (report == null) { throw new InvalidOperationException($"无法创建类型为{reportType.Name}的报表实例"); } return report; } }
步骤2:在启动项目中注册具体实现
主应用(或专门的启动配置项目)直接引用两个报表类库,然后在DI容器中注册报表实例和工厂:
// 以ASP.NET Core为例,在Program.cs中注册 var builder = WebApplication.CreateBuilder(args); // 注册具体报表类 builder.Services.AddScoped<IReport, CrystalReportImpl>(); builder.Services.AddScoped<IReport, DevExpressReportImpl>(); // 注册工厂,并传入扩展名映射 builder.Services.AddSingleton<IReportFactory>(sp => new ReportFactory( sp, new Dictionary<string, Type> { { "rpt", typeof(CrystalReportImpl) }, { "rpx", typeof(DevExpressReportImpl) } } ));
这样一来,Default项目完全不依赖具体报表库,主应用虽然引用了报表库,但这是启动配置的必要操作,不会污染业务逻辑层的依赖。
方案二:插件模式(动态加载)
如果希望主应用也完全不引用两个报表库,可以用反射动态加载报表类库,把具体报表做成独立的插件dll。
步骤1:定义扩展属性(用于标记报表对应的扩展名)
在Default项目中添加一个自定义属性,用来标记具体报表类对应的扩展名:
[AttributeUsage(AttributeTargets.Class, Inherited = false)] public class ReportExtensionAttribute : Attribute { public string Extension { get; } public ReportExtensionAttribute(string extension) { Extension = extension.ToLower(); } }
步骤2:修改Default项目中的ReportFactory
让工厂在初始化时扫描指定目录下的插件dll,通过反射加载实现了IReport的类型:
public sealed class ReportFactory : IReportFactory { private readonly Dictionary<string, Type> _reportTypeCache = new(); public ReportFactory(string pluginDirectory) { if (!Directory.Exists(pluginDirectory)) { throw new DirectoryNotFoundException($"插件目录不存在:{pluginDirectory}"); } LoadPluginReports(pluginDirectory); } private void LoadPluginReports(string pluginDirectory) { foreach (var dllPath in Directory.GetFiles(pluginDirectory, "*.dll")) { try { var assembly = Assembly.LoadFrom(dllPath); // 扫描所有实现IReport的非抽象类 foreach (var type in assembly.GetTypes() .Where(t => typeof(IReport).IsAssignableFrom(t) && !t.IsInterface && !t.IsAbstract)) { // 获取标记的扩展名 var extAttr = type.GetCustomAttribute<ReportExtensionAttribute>(); if (extAttr == null) continue; if (!_reportTypeCache.ContainsKey(extAttr.Extension)) { _reportTypeCache.Add(extAttr.Extension, type); } } } catch (Exception ex) { // 处理加载异常,比如日志记录 Console.WriteLine($"加载插件失败:{dllPath},错误:{ex.Message}"); } } } public IReport Get(string extension) { var lowerExt = extension.ToLower(); if (!_reportTypeCache.TryGetValue(lowerExt, out var reportType)) { throw new ArgumentException($"不支持的报表扩展名:{extension}", nameof(extension)); } // 创建实例(如果需要依赖注入,可以用ActivatorUtilities.CreateInstance) return (IReport)Activator.CreateInstance(reportType); } }
步骤3:在具体报表类库中标记属性
在Crystal Reports和DevExpress的报表实现类上添加属性:
// Crystal报表类库中 [ReportExtension("rpt")] public class CrystalReportImpl : IReport { // 实现IReport接口的方法 } // DevExpress报表类库中 [ReportExtension("rpx")] public class DevExpressReportImpl : IReport { // 实现IReport接口的方法 }
步骤4:主应用配置插件目录
主应用只需要把两个报表类库的dll放到指定的插件目录,然后初始化工厂:
// 主应用中初始化工厂 var pluginDir = Path.Combine(AppContext.BaseDirectory, "ReportPlugins"); var reportFactory = new ReportFactory(pluginDir); // 使用工厂创建报表 var crystalReport = reportFactory.Get("rpt");
这种方式下,主应用和Default项目都不需要引用具体的报表库,完全实现了解耦,还能动态添加新的报表类型。
选择建议
- 如果你的项目已经在使用DI容器,优先选方案一,实现简单且易于维护。
- 如果需要动态扩展报表类型,或者完全隔离主应用与报表库的依赖,选方案二。
内容的提问来源于stack exchange,提问作者Ivan-Mark Debono
相关产品推荐
相关产品推荐

