之前做了一个用于excel导入导出的包, 定义了一些接口, 然后基于 NPOI EPPlus MiniExcel 做了三种实现
接口大概长下面这样(现在可以在接口里面写静态函数了!)
public interface IExcelReader { // 根据一些条件返回下面的实现 public static IExcelReader GetExcelReader(string filePath, ) { } }
然后有对应三种实现
public class NPOIReader: IExcelReader {} public class EPPlusReader: IExcelReader {} public class MiniExcel: IExcelReader {}
在使用时
using var reader = IExcelReader.GetExcelReader("ExcelReader.xlsx", <一堆杂七杂八的条件>)
根据需要获取实例, 而不必去管什么 NPOI EPPlus MiniExcel
用起来可以极大的降低心智负担, 也可以使用我认为比较 "人性化" 的操作
这一堆东西都是写在一起的, 然后碰到了一些我比较在意的问题
IExcelReader
, 那我必定需要同时修改对应的三个实现, 但是由于三个实现写在一起, 我必须将三个实现都改完测完, 然后才能push发包, 我认为这样是不好的因为这样那样的问题, 我开始考虑拆包了
一开始的想法是
先把统一的接口和操作什么的东西抽出来, 做成一个Core包
然后 NPOI EPPlus MiniExcel 相关的实现做成三个包, 都引用这个 Core
如果代码中只用IExcelReader
这样的接口进行操作, 可以在不改变代码的前提下, 通过更换包引用(比如NPOI的包改为MiniExcel的包)轻松改变实现, 达成不同的效果
但由于Core包是被引用的, 所以理论上来说IExcelReader
并不能像之前那样直接创建这三种实例
碰到这种"我知道, 但是身不由己"的情况, 我想到了用委托来做
// (大概是这么个感觉, 实际上我现在用的是字典) public static List> Selector;
在Core中搞一个静态委托集合, 然后在那三个包中将创建对象的委托加到这个集合里, 之后在使用IExcelReader.GetExcelReader("**.xlsx")
时, 就可以通过这个委托集合获取到对应的实现了
以上是我的大致思路
我最先想到的就是静态构造函数
毕竟微软的文档上说了
静态构造函数用于初始化任何静态数据,或执行仅需执行一次的特定操作。 将在创建第一个实例或引用任何静态成员之前自动调用静态构造函数。
看描述还挺符合我的想法, 然后就有了如下代码
public class NPOIExcelReader : IExcelReader { static NPOIExcelReader() { Selector.Add((path) => { // 假装下面做了一堆事情 // ...... return new NPOIExcelReader(path); }); } }
看着好像还行, 试了一下结果GG
如果我只是使用IExcelReader.GetExcelReader("**.xlsx")
, 则无法触发这个构造函数, 除非我在这之前调用一次NPOIExcelReader
, 但这与我的设想差挺多的, 所以暂时放弃了这个方案, 另寻他法
我觉得可能是因为class
太 "低" 了, 所以才无法触发静态构造函数
然后我又想到了ModuleInitializer
, 感觉这个总比class
"高"一些, 不知道能不能实现我的想法
internal class Init { [ModuleInitializer] public static void InitSelector() { Selector.Add((path) => { // 假装下面做了一堆事情 // ...... return new NPOIExcelReader(path); }); } }
在NPOI包里写完上面的初始化之后我又尝试了一次, 结果还是GG......
碰到了类似的问题, 如果不调用NPOI包内的东西, 则无法初始化
后来查看了AppDomain.CurrentDomain.GetAssemblies()
, 发现程序运行时并没有加载 NPOI包 的程序集, 我觉得可能是因为这个原因才导致扑街的
所以尝试在Core中用反射获取程序集(因为在代码中使用了IExcelReader.GetExcelReader
, 所以可以触发Core包的ModuleInitializer
初始化), 然后使用AppDomain.CurrentDomain.load
来加载
public static class Init { [ModuleInitializer] public static void InitCellReader() { var files = Directory.GetFiles(AppDomain.CurrentDomain.BaseDirectory, "我那几个包的实现.dll"); if (files.IsEmpty()) return; var newAsses = files.Select(item => Assembly.LoadFrom(item)).ToList(); newAsses.ForEach(item => AppDomain.CurrentDomain.Load(item.FullName)); } }
运行之后打个断点, 确实执行了, 也确实加载到AppDomain.CurrentDomain
中了, 但是...还是没用, 全都木大木大了
既然发现问题出在 "不调用就不初始化" 上, 那我就调用一下...
基于上面的第三种尝试, 尝试创建NPOI包中的实现, 能不能创建无所谓, 重要的是摆出一副 "我要调你" 的感觉, 然后初始化自己动起来
还是写在Core包中
public static class Init { [ModuleInitializer] public static void InitCellReader() { var files = Directory.GetFiles(AppDomain.CurrentDomain.BaseDirectory, "我那几个包的实现.dll"); if (files.IsEmpty()) return; var newAsses = files.Select(item => Assembly.LoadFrom(item)).ToList(); newAsses.ForEach(item => AppDomain.CurrentDomain.Load(item.FullName)); var types = newAsses.SelectMany(s => s.GetTypes().Where(item => item.HasInterface(typeof(IExcelReader)))); types.ForEach(item => { try { Activator.CreateInstance(item); } catch { } }); } }
然后配合其他包的Init
, 最后终于算是实现了我想要的效果
但是实现的方式太丑陋了...不知道有没有更好, 更优雅的方式可以在运行或者编译的时候完成这个初始化操作