在前一篇博文中,突發奇想地用文件存儲實現了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
文章列表