zoukankan      html  css  js  c++  java
  • 求同存异:持久化从文件到数据库引发的架构变动

    前一篇博文中,突发奇想地用文件存储实现了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
  • 相关阅读:
    简介.Net对象序列化.txt
    如何在Web页面退出前提示用户保存数据?
    如何将图片存储到数据库中
    页面回车键响应,onkeydown事件
    用C#创建Windows服务(Windows Services)
    解决“Visual Studio 要求设计器使用文件中的第一个类。移动类代码使之成为文件中的第一个类,然后尝试重新加载设计器。”方法
    动态创建htm元素并添加到document中
    如何在Asp.net的Header中添加/title/Meta tages/CSS
    无法打开项目文件,“d:\web\webapp.csproj”,此安装不支持该项目类型
    用Intelligencia.UrlRewriter实现URL重写
  • 原文地址:https://www.cnblogs.com/dudu/p/4660986.html
Copyright © 2011-2022 走看看