文章出處

前一篇博文中,突發奇想地用文件存儲實現了oauth refresh token的持久化。在這篇博文中,我們將面對現實地將文件存儲改為數據庫存儲。既然軟件開發中唯一不變的就是變化本身,那我們主動求變,用變化來驗證代碼的設計是否能隨機應變。

之前使用文件存儲的架構是這樣的:

  • Presentation層-WebAPI:CNBlogsRefreshTokenProvider
  • Application層-接口:IRefreshTokenService
  • Application層-實現:RefreshTokenService
  • Domain層-實體:RefreshToken
  • Repository層-接口:IRefreshTokenRepository
  • Repository層-實現:FileStorage.RefreshTokenRepository

依賴關系是這樣的:

  • Presentation層的CNBlogsRefreshTokenProvider -> Application層的接口IRefreshTokenService + Domain層的實體RefreshToken。
  • Application層的實現RefreshTokenService -> Repository層的接口IRefreshTokenRepository + Domain層的實體RefreshToken。
  • Repository層的實現FileStorage.RefreshTokenRepository -> Domain層的實體RefreshToken。

對于這樣的分層架構,要將文件存儲改為數據庫存儲,看上去似乎很簡單——只需基于數據庫存儲,使用相應的ORM工具(比如EF),實現IRefreshTokenRepository接口,然后將之注入,其它地方無需更改1行代碼。

當我們悠哉悠哉地去寫IRefreshTokenRepository接口的實現Database.RefreshTokenRepository的代碼時,突然發現有些不對勁。

之前基于文件存儲的FireStorage.RefreshTokenRepository的代碼是這么實現的(為了簡化問題,我們只看查詢部分的實現):

public class RefreshTokenRepository : IRefreshTokenRepository
{
    private List<RefreshToken> _refreshTokens;

    public RefreshTokenRepository()
    {
        //...
    }

    public async Task<RefreshToken> FindById(string Id)
    {
        return _refreshTokens.Where(x => x.Id == Id).FirstOrDefault();
    }
}

現在基于Entity Framework寫Database.RrefreshTokenRepository的實現代碼時,也要寫同樣的LINQ查詢代碼(下面代碼中的加粗部分):

public class RefreshTokenRepository : IRefreshTokenRepository
{
    public RefreshTokenRepository()
    {
    }

    public async Task<RefreshToken> FindById(string Id)
    {
        using (var context = new OpenApiDbContext())
        {
            return context.Set<RefreshToken>()
                .Where(x => x.Id == Id).FirstOrDefault();
        }
    }
}

雖然只是1行代碼的重復,但是越看越不對勁。假如復雜一些的項目,有很多LINQ查詢時,有多種持久化方式,還有針對單元測試的mock,這將會造成大量重復代碼。

當一個變化會引發重復代碼時,錯的肯定不是變化本身,而是代碼本身——代碼的設計有問題。現在重復代碼就在眼前,現在不解決,更待何時。

要解決重復代碼問題, 先要看一下相同(重復)代碼之前的不同之處在哪里,然后在表面上看起來的不同之處找出共同點,用接口封裝不同。這就是代碼設計中的求同存異法(注:實際沒有這個方法,寫這篇博文時臆造出來的)。

回到上面的代碼,.Where(x => x.Id == Id).FirstOrDefault(); 之前的不同之處是 _refreshTokens 與 context.Set<RefreshToken>(),前者的類型是 List<RefreshToken>,后者的類型是 System.Data.Entity.DbSet ,這2個不同有什么共同之處呢?

在Visual Studio中按F12鍵向上求索,終于找到了1個共同之處,那就是IQueryable——DbSet實現了IQueryable接口,List可以轉換為IQueryable(通過AsQueryable方法)。既然找到了共同之處,那我們就可以通過它消滅重復代碼,將2個RefreshTokenRepository變成1個RefreshTokenRepository。

public class RefreshTokenRepository : IRefreshTokenRepository
{
    private IQueryable<RefreshToken> _refreshTokens;

    public RefreshTokenRepository()
    {
    }

    public async Task<RefreshToken> FindById(string Id)
    {
        return _refreshTokens.Where(x => x.Id == Id).FirstOrDefault();
    }
}

上面的代碼實現了求同——從2個不同之處找到了共同之處,但如何存異呢?也就是如何根據不同的持久化存儲方式給上面代碼中的_refreshTokens成員變量賦值呢?這又帶來了_refreshTokens的求同存異問題。

這時你有沒有想到,有一個東西就是為求同存異而生,它就是——接口(Interface)。

那我們就引入一個接口來解決_refreshTokens的賦值問題,這個接口暫且叫做IUnitOfWork吧。IUnitOfWork的代碼如下:

public interface IUnitOfWork : IDisposable
{
    IQueryable<TEntity> Set<TEntity>() where TEntity : class;
}

于是RefreshTokenRepository就可以通過IUnitOfWork接口給_refreshTokens賦值:

public class RefreshTokenRepository : IRefreshTokenRepository
{
    private IQueryable<RefreshToken> _refreshTokens;

    public RefreshTokenRepository(IUnitOfWork unitOfWork)
    {
        _refreshTokens = unitOfWork.Set<RefreshToken>();
    }

    public async Task<RefreshToken> FindById(string Id)
    {
        return await _refreshTokens.Where(x => x.Id == Id).FirstOrDefaultAsync();
    }
}

接著我們針對文件存儲的持久化方式,實現一個FileStorageUnitOfWork:

public class FileStorageUnitOfWork : IUnitOfWork
{
    public IQueryable<TEntity> Set<TEntity>() where TEntity : class
    {
        return ReadFromFile<TEntity>().AsQueryable<TEntity>();
    }

    private IList<TEntity> ReadFromFile<TEntity>()
    {
        IList<TEntity> entities = null;
        var jsonFilePath = HostingEnvironment.MapPath(string.Format("~/App_Data/{0}.json", typeof(TEntity)));
        if (File.Exists(jsonFilePath))
        {
            var json = File.ReadAllText(jsonFilePath);
            entities = JsonConvert.DeserializeObject<List<TEntity>>(json);
        }
        if (entities == null) entities = new List<TEntity>();
        return entities;
    }
}

再接著針對數據庫存儲的持久化方式,基于Entity Framework實現一個EfUnitOfWork(EF的映射配置省略):

public class EfUnitOfWork : DbContext, IUnitOfWork
{
    public new IQueryable<TEntity> Set<TEntity>() where TEntity : class
    {
        return base.Set<TEntity>();
    }
}

最后,想用什么持久化方式,就用IOC容器(比如Unity)注入對應的UnitOfWork。

要用文件存儲,就注入FileStorageUnitOfWork:

container.RegisterType<IUnitOfWork, FileStorageUnitOfWork>(new HttpContextLifetimeManager<IUnitOfWork>());

要用數據庫存儲,就注入EfUnitOfWork:

container.RegisterType<IUnitOfWork, EfUnitOfWork>(new HttpContextLifetimeManager<IUnitOfWork>());

這樣,我們就可以輕松地將oauth refresh token的持久化方式從文件存儲換到數據庫存儲,從數據庫存儲換到文件存儲。或者哪天突發奇想換到NoSQL,也是手到擒來的事。

寫了這么多廢話,實際上只是為了一個接口的粉墨登場——IUnitOfWork。為了在持久化方式變化的情況下,保持Repository層的不變,我們引入了IUnitOfWork接口,讓Repositroy依賴IUnitOfWork,將持久化方式封裝在IUnitOfWork的實現中,從而解決了持久化方式變動帶來的重復代碼問題。再次實際體會了:小接口,大力量。

【附】

變化之后的架構如下:

  • Presentation層-WebAPI:CNBlogsRefreshTokenProvider
  • Application層-接口:IRefreshTokenService
  • Application層-實現:RefreshTokenService
  • Domain層-實體:RefreshToken
  • Repository層-接口:IRefreshTokenRepository
  • Repository層-實現:RefreshTokenRepository
  • UnitOfWork層-接口:IUnitOfWork
  • UnitOfWork層-實現:FileStorageUnitOfWork與EfUnitOfWork

文章列表


不含病毒。www.avast.com
arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()