記一次dotnet拆分包,並希望得大佬指點

記一次dotnet拆分包,並希望得大佬指點

之前做了一個用於excel導入導出的包, 定義了一些接口, 然後基於 NPOI EPPlus MiniExcel 做了三種實現

接口大概長下面這樣(現在可以在接口裏面寫靜態函數了!)

public interface IExcelReader
{
    // 根據一些條件返回下面的實現
    public static IExcelReader GetExcelReader(string filePath, <params>)
    {
    }
}

然後有對應三種實現

public class NPOIReader: IExcelReader
{}

public class EPPlusReader: IExcelReader
{}

public class MiniExcel: IExcelReader
{}

在使用時

using var reader = IExcelReader.GetExcelReader("ExcelReader.xlsx", <一堆雜七雜八的條件>)

根據需要獲取實例, 而不必去管什麼 NPOI EPPlus MiniExcel

用起來可以極大的降低心智負擔, 也可以使用我認為比較 “人性化” 的操作

這一堆東西都是寫在一起的, 然後碰到了一些我比較在意的問題

  1. 如果我只是更新了NPOI包的實現, 然後push了一個新的版本, 這就相當於其他的實現也被”升級”了, 儘管另外的實現沒有任何變化, 我認為這樣是不好的
  2. 如果我只想使用 MiniExcel 的內容, 但由於三個實現寫在了一起, NPOI 和 EPPlus 會被一起引入, 我認為這樣是不好的
  3. 如果我修改了接口 IExcelReader, 那我必定需要同時修改對應的三個實現, 但是由於三個實現寫在一起, 我必須將三個實現都改完測完, 然後才能push發包, 我認為這樣是不好的

因為這樣那樣的問題, 我開始考慮拆包了

初步構想

一開始的想法是

先把統一的接口和操作什麼的東西抽出來, 做成一個Core包

然後 NPOI EPPlus MiniExcel 相關的實現做成三個包, 都引用這個 Core

如果代碼中只用 IExcelReader 這樣的接口進行操作, 可以在不改變代碼的前提下, 通過更換包引用(比如NPOI的包改為MiniExcel的包)輕鬆改變實現, 達成不同的效果

但由於Core包是被引用的, 所以理論上來說 IExcelReader 並不能像之前那樣直接創建這三種實例

碰到這種”我知道, 但是身不由己”的情況, 我想到了用委託來做

// (大概是這麼個感覺, 實際上我現在用的是字典)
public static List<Func<string, IExcelReader>> Selector;

在Core中搞一個靜態委託集合, 然後在那三個包中將創建對象的委託加到這個集合里, 之後在使用 IExcelReader.GetExcelReader("**.xlsx") 時, 就可以通過這個委託集合獲取到對應的實現了

以上是我的大致思路

第一種嘗試-靜態構造函數

我最先想到的就是靜態構造函數

畢竟微軟的文檔上說了

靜態構造函數用於初始化任何靜態數據,或執行僅需執行一次的特定操作。 將在創建第一個實例或引用任何靜態成員之前自動調用靜態構造函數。

看描述還挺符合我的想法, 然後就有了如下代碼

public class NPOIExcelReader : IExcelReader
{
    static NPOIExcelReader()
    {
        Selector.Add((path) =>
        {
            // 假裝下面做了一堆事情
            // ......
            return new NPOIExcelReader(path);
        });
    }
}

看着好像還行, 試了一下結果GG

如果我只是使用 IExcelReader.GetExcelReader("**.xlsx"), 則無法觸發這個構造函數, 除非我在這之前調用一次 NPOIExcelReader, 但這與我的設想差挺多的, 所以暫時放棄了這個方案, 另尋他法

第二種嘗試-ModuleInitializer

我覺得可能是因為 class 太 “低” 了, 所以才無法觸發靜態構造函數

然後我又想到了 ModuleInitializer, 感覺這個總比 class “高”一些, 不知道能不能實現我的想法

internal class Init
{
    [ModuleInitializer]
    public static void InitSelector()
    {
        Selector.Add((path) =>
        {
            // 假裝下面做了一堆事情
            // ......
            return new NPOIExcelReader(path);
        });
    }
}

在NPOI包里寫完上面的初始化之後我又嘗試了一次, 結果還是GG……

碰到了類似的問題, 如果不調用NPOI包內的東西, 則無法初始化

第三種嘗試-AppDomain.CurrentDomain.Load

後來查看了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 中了, 但是…還是沒用, 全都木大木大了

絕望的嘗試-反射+Activator

既然發現問題出在 “不調用就不初始化” 上, 那我就調用一下…

基於上面的第三種嘗試, 嘗試創建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, 最後終於算是實現了我想要的效果

但是實現的方式太醜陋了…不知道有沒有更好, 更優雅的方式