[設計模式] 靜態代理居然能解決這種問題,我驚訝了!

2022-09-13 12:00:17

前言

23種設計模式都會了嗎?今天講一下靜態代理模式的實戰場景。

代理模式給某一個物件提供一個代理物件,並由代理物件控制對原物件的參照。通俗的來講代理模式就是我們生活中常見的中介。

舉個例子來說明:假如說我現在想買一輛二手車,雖然我可以自己去找車源,做質量檢測等一系列的車輛過戶流程,但是這確實太浪費我得時間和精力了。我只是想買一輛車而已為什麼我還要額外做這麼多事呢?於是我就通過中介公司來買車,他們來給我找車源,幫我辦理車輛過戶流程,我只是負責選擇自己喜歡的車,然後付錢就可以了。

為什麼要用代理模式?

中介隔離作用:在某些情況下,一個客戶類不想或者不能直接參照一個委託物件,而代理類物件可以在客戶類和委託物件之間起到中介的作用,其特徵是代理類和委託類實現相同的介面。


解決問題

這篇文章借用 FreeSql.Cloud 開源專案的程式碼,講解代理模式的實際應用和收益,以及需要注意的地方。

FreeSql 是 c#.NET 功能強大的 ORM 框架,定義了 IFreeSql 介面,主要針對單個 ConnectionString 生產 ORM 操作物件。

跨多資料庫的時候,不同的 ConnectionString 需要生成多個 IFreeSql 原始物件,如果是多租戶場景每個租戶 ConnectionString 都不相同的情況下,就需要建立 N個 IFreeSql 原始物件。

FreeSql.Cloud 正是為了跨多資料庫的問題而產生,它可以解決:

1、FreeSqlCloud 實現多庫版 IFreeSql 介面,從使用習慣上保持與單庫版 IFreeSql 一致;

2、執行時,FreeSqlCloud 可動態新增或刪除多個 ConnectionString 對應的 IFreeSql;

3、FreeSqlCloud 儲存多租戶 IFreeSql,最後活躍時間 > 10分鐘的租戶,釋放對應 IFreeSql 減少記憶體開銷;

4、FreeSqlCloud 支援隨時 Change 切換到對應的 IFreeSql 進行操作;


代理模式實戰(一)Scoped FreeSqlCloud 多庫版本

IFreeSql 是一個極為嚴格、簡單,且功能強大的介面,我們一直在嚴格控制 API 氾濫增長,氾濫的 API 在後續改造時非常痛苦。

正因為它的簡單定義,讓我們有機會使用到代理模式實現新的 IFreeSql 實現類 FreeSqlCloud。

public class FreeSqlCloud : IFreeSql
{
    IFreeSql CurrentOrm => ...; //請看後面

    public IAdo Ado => CurrentOrm.Ado;
    public IAop Aop => CurrentOrm.Aop;
    public ICodeFirst CodeFirst => CurrentOrm.CodeFirst;
    public IDbFirst DbFirst => CurrentOrm.DbFirst;
    public GlobalFilter GlobalFilter => CurrentOrm.GlobalFilter;

    public void Transaction(Action handler) => CurrentOrm.Transaction(handler);
    public void Transaction(IsolationLevel isolationLevel, Action handler) => CurrentOrm.Transaction(isolationLevel, handler);
    public ISelect<T1> Select<T1>() where T1 : class => CurrentOrm.Select<T1>();
    public ISelect<T1> Select<T1>(object dywhere) where T1 : class => Select<T1>().WhereDynamic(dywhere);
    public IDelete<T1> Delete<T1>() where T1 : class => CurrentOrm.Delete<T1>();
    public IDelete<T1> Delete<T1>(object dywhere) where T1 : class => Delete<T1>().WhereDynamic(dywhere);
    public IUpdate<T1> Update<T1>() where T1 : class => CurrentOrm.Update<T1>();
    public IUpdate<T1> Update<T1>(object dywhere) where T1 : class => Update<T1>().WhereDynamic(dywhere);
    public IInsert<T1> Insert<T1>() where T1 : class => CurrentOrm.Insert<T1>();
    public IInsert<T1> Insert<T1>(T1 source) where T1 : class => Insert<T1>().AppendData(source);
    public IInsert<T1> Insert<T1>(T1[] source) where T1 : class => Insert<T1>().AppendData(source);
    public IInsert<T1> Insert<T1>(List<T1> source) where T1 : class => Insert<T1>().AppendData(source);
    public IInsert<T1> Insert<T1>(IEnumerable<T1> source) where T1 : class => Insert<T1>().AppendData(source);
    public IInsertOrUpdate<T1> InsertOrUpdate<T1>() where T1 : class => CurrentOrm.InsertOrUpdate<T1>();
}

如上程式碼,若 CurrentOrm 返回值是原始 IFreeSql 物件,即會代理呼叫原始資料庫 ORM 操作方法。方法不多,功能卻強大,代理模式應用起來就會很輕鬆。

由於多個 ConnectionString 的原因,我們需要定義一個字典儲存多個原始 IFreeSql 物件:

    readonly Dictionary<string, IFreeSql> _orms = new Dictionary<string, IFreeSql>();

我們已經有了 _orms,還缺什麼??還缺一個當前 string _dbkey,有了它我們的 CurrentOrm 方法才知道怎麼獲取對應的 IFreeSql:

    string _dbkey;
    IFreeSql CurrentOrm => _orms[_dbkey]; //測試不糾結程式碼安全

    //切換資料庫
    IFreeSql Change(string db)
    {
        _dbkey = db;
        return _orms[_dbkey];
    }
    //新增 IFreeSql
    FreeSqlCloud Add(string db, IFreeSql orm)
    {
        if (_dbkey == null) _dbkey = db;
        _orms.Add(db, orm);
        return this;
    }

至此我們基於 Scoped 生命週期的 FreeSqlCloud 就完成了,DI 程式碼大概如下:

public void ConfigureServices(IServiceCollection services)
{
    var db1 = new FreeSqlBuilder().UseConnectionString(DataType.Sqlite, "data source=db1.db").Build();
    var db2 = new FreeSqlBuilder().UseConnectionString(DataType.Sqlite, "data source=db2.db").Build();
    services.AddScoped(provider =>
    {
        var cloud = new FreeSqlCloud();
        cloud.Add("db1", db1);
        cloud.Add("db2", db2);
        return cloud;
    });
}

代理模式實戰(二)Singleton FreeSqlCloud 多庫版本

實戰(一)我們實現了 Scoped 版本,可是其實專案中 Singleton 單例才是高效能的保證,特別是多租戶場景,每次 new FreeSqlCloud 不止還要回圈 Add 那麼多次,實屬浪費!!!

其實單例並非難事,只需要將 _dbkey 型別修改成 AsyncLocal,這個型別多執行緒是安全的,有關它的原理請看資料:https://www.cnblogs.com/eventhorizon/p/12240767.html

    AsyncLocal<string> _dbkey;
    IFreeSql CurrentOrm => _orms[_dbkey.Value];

    IFreeSql Change(string db)
    {
        _dbkey.Value = db;
        return _orms[_dbkey];
    }
    FreeSqlCloud Add(string db, IFreeSql orm)
    {
        if (_dbkey.Value == null) _dbkey.Value = db;
        _orms.Add(db, orm);
        return this;
    }

至此我們就完成了一個多執行緒安全的代理模式實現,因此我們只需要在 Ioc 注入之前 Register 好原始 IFreeSql 物件即可:

public void ConfigureServices(IServiceCollection services)
{
    var db1 = new FreeSqlBuilder().UseConnectionString(DataType.Sqlite, "data source=db1.db").Build();
    var db2 = new FreeSqlBuilder().UseConnectionString(DataType.Sqlite, "data source=db2.db").Build();
    var cloud = new FreeSqlCloud();
    cloud.Add("db1", db1);
    cloud.Add("db2", db2);

    services.AddSingleton(cloud);
}

代理模式實現(三)Singleton FreeSqlCloud 多庫多租戶版本

如上,我們使用字典儲存多個 IFreeSql 原始物件,在數量不多的情況下是可行的。

但是如果我們做的是多租戶系統,那麼數量很可能達到幾百,甚至上千個 IFreeSql 物件,並且這些租戶不全都是活躍狀態。

因此我們需要一種釋放機制,當租戶最後活躍時間 > 10分鐘,釋放 IFreeSql 資源,減少記憶體開銷;

我們可以參照 IdleBus 元件解決該問題,IdleBus 空閒物件管理容器,有效組織物件重複利用,自動建立、銷燬,解決【範例】過多且長時間佔用的問題。有時候想做一個單例物件重複使用提升效能,但是定義多了,有的又可能一直空閒著佔用資源。

IdleBus 專門解決:又想重複利用,又想少佔資源的場景。

此時我們只需要修改內部實現部分程式碼如下:

public class FreeSqlCloud : IFreeSql
{
    IdleBus<IFreeSql> _orms = new IdleBus<string, IFreeSql>();
    AsyncLocal<string> _dbkey;
    IFreeSql CurrentOrm => _orms.Get(_dbkey.Value);

    IFreeSql Change(string db)
    {
        _dbkey.Value = db;
        return this;
    }
    FreeSqlCloud Register(string db, Func<IFreeSql> create) //注意 create 型別是 Func
    {
        if (_dbkey.Value == null) _dbkey.Value = db;
        _orms.Register(db, create);
        return this;
    }
    //...
}

public void ConfigureServices(IServiceCollection services)
{
    var cloud = new FreeSqlCloud();
    cloud.Add("db1", () => new FreeSqlBuilder().UseConnectionString(DataType.Sqlite, "data source=db1.db").Build());
    cloud.Add("db2", () => new FreeSqlBuilder().UseConnectionString(DataType.Sqlite, "data source=db1.db").Build());

    services.AddSingleton(cloud);
}

代理模式實現(四)跟隨切換資料庫的倉儲物件

1、靜態倉儲物件

FreeSql.Repository 物件建立時固定了原始 IFreeSql,因此無法跟隨 FreeSqlCloud Change 切換資料庫。

注意:是同一個物件範例建立之後,無法跟隨切換,建立新物件範例不受影響。

因為要在 Repository 建立之前,先呼叫 fsql.Change 切換好資料庫。

2、動態倉儲物件

但是。。。仍然有一種特殊需求,Repository 在建立之後,仍然能跟隨 fsql.Change 切換資料庫。

實戰中 Scoped 生命同期可能有多個 Repository 物件,因此切換 cloud 即改變所有 Repository 物件狀態,才算方便。

var repo1 = cloud.GetCloudRepository<User>();
var repo2 = cloud.GetCloudRepository<UserGroup>();
cloud.Change("db2");
Console.WriteLine(repo1.Orm.Ado.ConnectionString); //repo -> db2
Console.WriteLine(repo2.Orm.Ado.ConnectionString); //repo -> db2
cloud.Change("db1");
Console.WriteLine(repo1.Orm.Ado.ConnectionString); //repo -> db1
Console.WriteLine(repo2.Orm.Ado.ConnectionString); //repo -> db1

我們仍然使用了代理模式,IBaseRepository 介面定義也足夠簡單:

提示:關鍵看 CurrentRepo 的獲取

class RepositoryCloud<TEntity> : IBaseRepository<TEntity> where TEntity : class
{
    readonly FreeSqlCloud _cloud;
    public RepositoryCloud(FreeSqlCloud cloud)
    {
        _cloud = cloud;
    }

    public IBaseRepository<TEntity> CurrentRepo => ...; //請看後面
    public IUnitOfWork UnitOfWork
    {
        get => CurrentRepo.UnitOfWork;
        set => CurrentRepo.UnitOfWork = value;
    }

    public IFreeSql Orm => CurrentRepo.Orm;
    public Type EntityType => CurrentRepo.EntityType;
    public IDataFilter<TEntity> DataFilter => CurrentRepo.DataFilter;
    public ISelect<TEntity> Select => CurrentRepo.Select;
    public IUpdate<TEntity> UpdateDiy => CurrentRepo.UpdateDiy;
    public ISelect<TEntity> Where(Expression<Func<TEntity, bool>> exp) => CurrentRepo.Where(exp);
    public ISelect<TEntity> WhereIf(bool condition, Expression<Func<TEntity, bool>> exp) => CurrentRepo.WhereIf(condition, exp);

    public void Attach(TEntity entity) => CurrentRepo.Attach(entity);
    public void Attach(IEnumerable<TEntity> entity) => CurrentRepo.Attach(entity);
    public IBaseRepository<TEntity> AttachOnlyPrimary(TEntity data) => CurrentRepo.AttachOnlyPrimary(data);
    public Dictionary<string, object[]> CompareState(TEntity newdata) => CurrentRepo.CompareState(newdata);
    public void FlushState() => CurrentRepo.FlushState();

    public void BeginEdit(List<TEntity> data) => CurrentRepo.BeginEdit(data);
    public int EndEdit(List<TEntity> data = null) => CurrentRepo.EndEdit(data);

    public TEntity Insert(TEntity entity) => CurrentRepo.Insert(entity);
    public List<TEntity> Insert(IEnumerable<TEntity> entitys) => CurrentRepo.Insert(entitys);
    public TEntity InsertOrUpdate(TEntity entity) => CurrentRepo.InsertOrUpdate(entity);
    public void SaveMany(TEntity entity, string propertyName) => CurrentRepo.SaveMany(entity, propertyName);

    public int Update(TEntity entity) => CurrentRepo.Update(entity);
    public int Update(IEnumerable<TEntity> entitys) => CurrentRepo.Update(entitys);

    public int Delete(TEntity entity) => CurrentRepo.Delete(entity);
    public int Delete(IEnumerable<TEntity> entitys) => CurrentRepo.Delete(entitys);
    public int Delete(Expression<Func<TEntity, bool>> predicate) => CurrentRepo.Delete(predicate);
    public List<object> DeleteCascadeByDatabase(Expression<Func<TEntity, bool>> predicate) => CurrentRepo.DeleteCascadeByDatabase(predicate);
}

如上程式碼關鍵實現部分 CurrentRepo,我們定義了字典儲存多個 IBaseRepository 原始倉儲物件:

因為一個 CloudRepository 物件會建立 1-N 個 IBaseRepository 原始物件,在不使用 cloud.Change(..) 方法的時候只會建立 1 個,最多建立 cloud.Registers 數量,真實場景中不會有人在同一個業務把所有 db 都切換個遍。

    readonly Dictionary<string, IBaseRepository<TEntity>> _repos = new Dictionary<string, IBaseRepository<TEntity>>();
    protected void ForEachRepos(Action<IBaseRepository<TEntity>> action)
    {
        foreach (var repo in _repos.Values) action(repo);
    }
    public void Dispose()
    {
        ForEachRepos(repo => repo.Dispose());
        _repos.Clear();
    }
    
    protected IBaseRepository<TEntity> CurrentRepo
    {
        get
        {
            var dbkey = _cloud._dbkey.Value;
            if (_repos.TryGetValue(dbkey, out var repo) == false)
            {
                _repos.Add(dbkey, repo = _cloud.Use(dbkey).GetRepository<TEntity>());
                if (_dbContextOptions == null) _dbContextOptions = repo.DbContextOptions;
                else
                {
                    repo.DbContextOptions = _dbContextOptions;
                    if (_asTypeEntityType != null) repo.AsType(_asTypeEntityType);
                    if (_asTableRule != null) repo.AsTable(_asTableRule);
                }
            }
            return repo;
        }
    }

    Type _dbContextOptions;
    public DbContextOptions DbContextOptions
    {
        get => CurrentRepo.DbContextOptions;
        set => ForEachRepos(repo => repo.DbContextOptions = value);
    }
    Type _asTypeEntityType;
    public void AsType(Type entityType)
    {
        _asTypeEntityType = entityType;
        ForEachRepos(repo => repo.AsType(entityType));
    }
    Func<string, string> _asTableRule;
    public void AsTable(Func<string, string> rule)
    {
        _asTableRule = rule;
        ForEachRepos(repo => repo.AsTable(rule));
    }

由於 DbContextOptions、AsType、AsTable 比較特殊,需要將多個原始倉儲物件傳播設定,程式碼如上。

最終還要為 CloudRepository 建立擴充套件方法:

public static IBaseRepository<TEntity> GetCloudRepository<TEntity>(this FreeSqlCloud cloud)
    where TEntity : class
{
    return new RepositoryCloud<TEntity>(cloud);
}

結語

Repository 是一種非常方便做設計的模式,FreeSql 還有很多一些其他設計模式的應用,如果有興趣以後找機會再寫文章。


作者是什麼人?

作者是一個入行 18年的老批,他目前寫的.net 開源專案有:

開源專案 描述 開源地址 開源協定
FreeIM 聊天系統架構 https://github.com/2881099/FreeIM MIT
FreeRedis Redis SDK https://github.com/2881099/FreeRedis MIT
csredis https://github.com/2881099/csredis MIT
FightLandlord 鬥DI主網路版 https://github.com/2881099/FightLandlord 學習用途
FreeScheduler 定時任務 https://github.com/2881099/FreeScheduler MIT
IdleBus 空閒容器 https://github.com/2881099/IdleBus MIT
FreeSql ORM https://github.com/dotnetcore/FreeSql MIT
FreeSql.Cloud 分散式tcc/saga https://github.com/2881099/FreeSql.Cloud MIT
FreeSql.AdminLTE 低程式碼後臺生成 https://github.com/2881099/FreeSql.AdminLTE MIT
FreeSql.DynamicProxy 動態代理 https://github.com/2881099/FreeSql.DynamicProxy 學習用途

需要的請拿走,這些都是最近幾年的開源作品,以前更早寫的就不發了。