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

崩壞的領航員發表於2022-04-03

記一次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, 最後終於算是實現了我想要的效果

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

相關文章