求同存异:持久化从文件到数据库引发的架构变动

前一篇博文中,突发奇想地用文件存储实现了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 _refreshTokens.Where(x => x.Id == Id).FirstOrDefault();
    }
}

接着我们针对文件存储的持久化方式,实现一个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
时间: 2024-11-05 14:46:06

求同存异:持久化从文件到数据库引发的架构变动的相关文章

JMS服务器ActiveMQ的初体验并持久化消息到MySQL数据库中

JMS服务器ActiveMQ的初体验并持久化消息到MySQL数据库中 一.JMS的理解JMS(Java Message Service)是jcp组织02-03年定义了jsr914规范(http://jcp.org/en/jsr/detail?id=914),它定义了消息的格式和消息传递模式:消息包括:消息头,消息扩展属性和消息体,其结构看起来与SOAP非常的相似,但一般情况下,SOAP主要关注远程服务调用,而消息则专注于信息的交换:消息分为:消息生产者,消息服务器和消息消费者.生产者与消费者之间

hibernate:持久化类映射文件

<?xml version="1.0"?> <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd"> <hibernate-mapping package="com.myschool.

运用PDO存储将图片、音频文件存入数据库

在数据库中创建表格的时候,有一个字段为image,用来保存图片,那么其类型就是blob,关于blob,百度百科是这样描述的 BLOB (binary large object),二进制大对象,是一个可以存储二进制文件的容器. 在计算机中,BLOB常常是数据库中用来存储二进制文件的字段类型. BLOB是一个大文件,典型的BLOB是一张图片或一个声音文件,由于它们的尺寸,必须使用特殊的方式来处理(例如:上传.下载或者存放到一个数据库). 根据Eric Raymond的说法,处理BLOB的主要思想就是

为什么NTFS删除超过4G大文件或数据库文件后FILE RECORD大小表现为0

为什么NTFS删除超过4G大文件或数据库文件后FILE RECORD大小表现为0? 答:NTFS删除一个文件,必须要完成如下几个流程,才算完结: 1.更改文件系统$bitmap,释放空间 2.更改$mft filerecord项的属性为删除 3.更改$mft:$bitmap的位图信息为0,释放这个filerecord的占用空间 4.清除目录链表中关于本文件的item信息. 这个流程是理想状态下的处理规则,但实际上,最头疼的是OS要考虑这个问题:如果在上述4个步骤中出现中断(如突然断电.死机等),

相克军_Oracle体系_随堂笔记016-参数文件及数据库的启动和关闭

参数文件: spfile<SID>.ora    动态参数文件,是二进制文件,9i以后引入并建议使用 init<SID>.ora    静态参数文件,是文本文件 动态参数,部分参数可以动态修改.建议9i以后就使用spfile. 数据库启动三个阶段: nomount(根据参数文件配置参数启动实例) mount(根据参数文件中记录的控制文件位置打开控制文件) open(根据控制文件信息打开数据文件,redo日志文件,open数据库) 注:nomount之前就可以根据pfile创建spf

报表的数据组织:文件还是数据库?

在报表开发项目中,报表的源数据可以放置在数据库中,也可以放在文件里.比如,一个互联网公司的网站运营报表系统,公司注册用户的基本信息来自于网站系统,使用的是Oracle数据库:用户操作数据来自于网站系统的日志文件,是文本文件.一般的做法是将用户操作数据从文本文件中导入到Oracle中,再用SQL语句去提取和计算数据. 那么,将报表的数据全部放到数据库中是否是最佳做法?可不可以把报表的数据全部或者部分放到文件系统中呢?这两种做法各有什么优缺点呢? 这里我们比较一下,报表工具结合Java程序访问数据文

卸载exchange server 2010过程中,无法删除公共文件夹数据库

如果您的企业Exchange Server 只有一台,这时公共文件夹也在这台上边,而这时你再删除时,将发现报错了,不让删除.   此时,在"公共文件夹数据库"的属性中已经不见"数据库副本转移"的界面了,因为只有一台嘛. 这时如何解决呢?可以通过ADSI编辑器删除,方法如下: 在ADSI编辑器中,定位至如下位置: CN=Configuration,DC=Doubi,DC=ren   CN=Services  CN=Microsoft Exchange  CN=EXCH

SQLServer2008R2 mdf文件还原数据库

偶然遇到要用mdf文件restore数据库,试了2个小时才弄出来,百度查出来的我试了都不太好用,或者是我没理解. 下面把我用的记录一下,以防忘记. 工具:SQLServer 2008R2 步骤: 1.创建一个与mdf文件同名的数据库 2.将新创建的数据库宕机(detach), 3.在数据库文件中找到对应的数据库文集和对应的log文集,删除. 4.将需要还原的文件复制到对应的目录下. 5.将该文件attach到数据库上. 6.OK了.

C/C++ 中头文件相互包含引发的问题

今天下午遇到一个头文件相互包含而导致的编译问题,花了我不少时间去调试没找到问题,最后晚上跟师兄讨论不少时间,突然有所顿悟! 问题重现 我把问题脱离于项目简单描述一下:我写了一个函数 bool func(ClassA* CA) 需要加到项目中,我就把这个函数的声明放到 head1.h 中,函数参数类型 ClassA 定义在另一个头文件 head2.h 中,因此我需要在 head1.h 中包含 head2.h:而 head2.h 中之前又包含了 head1.h,这样就构成了一种头文件相互包含的场景.