适配器模式-文件服务

2022-05-21  本文已影响0人  求心丶
前言

适配器模式是把一个类的接口变换成客户端所期待的另一中接口,从而使原本因接口不匹配而无法在一起工作的两个类能够在一起工作。
用电器做例子,笔记本电脑的插头一般都是三相的,即除了阳极、阴极外,还有一个地极。而有些地方的电源插座却只有两极,没有地极。电源插座与笔记本电脑的电源插头不匹配使得笔记本电脑无法使用。这时候一个三相到两相的转换器(适配器)就能解决此问题,而这正像是本模式所做的事情(这个例子是我抄来的)。

适配器模式的类型

关于适配器模式的类型这个小节太过枯燥无味,概念定义生搬硬套太过牵强令人难以理解,虽然基本是我复制粘贴的过来的但是仍一度让我恼火到不想继续写下去。建议大家跳过这一部分直接看下面具体的业务场景。

适配器模式有类的适配器和对象的适配器两种不同的形式。

类的适配器模式把适配的类的API转换成目标类的API,假设有一个接口A和一个具体的类B,B不是A的实现类,现在客户端希望在一个A的是实现类C中使用B的功能,最简单的方法就是使C实现A的同时又继承B。这其中涉及到了三个角色

使用适配器模式实现文件服务

最近一个月都在做公司的项目迁移,把集团的项目(包括服务器)迁移到上海这边,我们原有的框架下有一套自研的文件服务实现,是将文件内容byte数组及元数据存放在mongo中,这个自研的文件服务有些年头了而且引入了这个文件服务之后在项目启动时会自动加载mongo连接(无论是否实际使用),重度依赖基础环境,所以趁着这次项目迁移准备完全移除自研的文件服务使用第三方文件服务实现,最终选用了阿里云oss作为第三方文件服务实现,以下就用oss表示新的文件服务。
因为要考虑到历史文件的迁移,文件服务迁移主要分成了两个阶段:
一、文件双写:
增量数据(新上传的文件)双写,同时上传到自研文件服务和oss,同一个文件在自研文件服务和oss中的文件id是一致的,读取文件仍然从自研文件服务中读取(因为存量数据尚未同步至oss中)。
二、存量数据同步:
因为有很多文件id是作为历史数据(存量数据)保存在mysql中的各个业务表中的(比如营业执照、合同等),为了确保切换到oss后历史文件能够正确显示,最简单的方法就是将自研文件服务中的存量数据同步到oss中并且确保文件id一致,这样同步完成之后就能够无缝切换到oss。

在文件双写阶段要实现的目标是 使用新的文件服务依赖替代原有的自研框架的文件服务依赖,对外只暴露新的文件服务接口定义的接口和类(接口和类除了包路径与自研服务不同之外,接口名类名和参数名都完全相同,这样各个依赖方只需要在导包处把包路径替换即可无需改动代码逻辑),所以这个时候就需要我们将自研的文件服务和新的文件服务做一个适配。
以下是代码部分:
自研文件服务:

package com.cube.dp.adapter.fs.custom;

import com.cube.dp.adapter.fs.custom.dto.FileDownloadDto;
import com.cube.dp.adapter.fs.custom.dto.FileDownloadForStreamDto;
import com.cube.dp.adapter.fs.custom.dto.FileUploadDto;
import com.cube.dp.adapter.fs.custom.dto.FileUploadForStreamDto;

/**
 * @author litb
 * @date 2022/5/21 15:32
 * <p>
 * 自研文件服务接口
 */
public interface IFileOperationService {


    /**
     * 上传文件
     *
     * @param dto 参数
     * @return 文件id
     */
    String upload(FileUploadDto dto);

    /**
     * 流式上传文件
     *
     * @param streamDto 参数
     * @return 文件id
     */
    String upload4Stream(FileUploadForStreamDto streamDto);

    /**
     * 下载文件
     *
     * @param fileId 文件id
     * @return 结果
     */
    FileDownloadDto download(String fileId);

    /**
     * 流式下载文件
     *
     * @param fileId 文件id
     * @return 结果
     */
    FileDownloadForStreamDto download4Stream(String fileId);
}

自研文件服务实现类

package com.cube.dp.adapter.fs.custom;

import com.cube.dp.adapter.fs.custom.dto.FileDownloadDto;
import com.cube.dp.adapter.fs.custom.dto.FileDownloadForStreamDto;
import com.cube.dp.adapter.fs.custom.dto.FileUploadDto;
import com.cube.dp.adapter.fs.custom.dto.FileUploadForStreamDto;

/**
 * @author litb
 * @date 2022/5/21 15:48
 * <p>
 * 自研文件服务实现
 */
public class CustomFileOperationServiceImpl implements IFileOperationService {

    @Override
    public String upload(FileUploadDto dto) {
        System.out.println("自研文件服务上传文件...");
        return null;
    }

    @Override
    public String upload4Stream(FileUploadForStreamDto streamDto) {
        System.out.println("自研文件服务流式上传文件...");
        return null;
    }

    @Override
    public FileDownloadDto download(String fileId) {
        System.out.println("自研文件服务下载文件...");
        return null;
    }

    @Override
    public FileDownloadForStreamDto download4Stream(String fileId) {
        System.out.println("自研文件服务流式下载文件...");
        return null;
    }
}

第三方文件服务:

package com.cube.dp.adapter.fs.third;

import com.cube.dp.adapter.fs.third.dto.FileDownloadDto;
import com.cube.dp.adapter.fs.third.dto.FileDownloadForStreamDto;
import com.cube.dp.adapter.fs.third.dto.FileUploadDto;
import com.cube.dp.adapter.fs.third.dto.FileUploadForStreamDto;

/**
 * @author litb
 * @date 2022/5/21 15:32
 * <p>
 * 第三方文件服务接口
 */
public interface IThirdPartyFileOperationService {


    /**
     * 上传文件
     *
     * @param dto 参数
     * @return 文件id
     */
    String upload(FileUploadDto dto);

    /**
     * 流式上传文件
     *
     * @param streamDto 参数
     * @return 文件id
     */
    String upload4Stream(FileUploadForStreamDto streamDto);

    /**
     * 下载文件
     *
     * @param fileId 文件id
     * @return 结果
     */
    FileDownloadDto download(String fileId);

    /**
     * 流式下载文件
     *
     * @param fileId 文件id
     * @return 结果
     */
    FileDownloadForStreamDto download4Stream(String fileId);
}

oss文件服务实现

package com.cube.dp.adapter.fs.third;

import com.cube.dp.adapter.fs.third.dto.FileDownloadDto;
import com.cube.dp.adapter.fs.third.dto.FileDownloadForStreamDto;
import com.cube.dp.adapter.fs.third.dto.FileUploadDto;
import com.cube.dp.adapter.fs.third.dto.FileUploadForStreamDto;

/**
 * @author litb
 * @date 2022/5/21 15:48
 * <p>
 * 自研文件服务实现
 */
public class OssFileOperationServiceImpl implements IThirdPartyFileOperationService {

    @Override
    public String upload(FileUploadDto dto) {
        System.out.println("oss文件服务上传文件...");
        return null;
    }

    @Override
    public String upload4Stream(FileUploadForStreamDto streamDto) {
        System.out.println("oss文件服务流式上传文件...");
        return null;
    }

    @Override
    public FileDownloadDto download(String fileId) {
        System.out.println("oss文件服务下载文件...");
        return null;
    }

    @Override
    public FileDownloadForStreamDto download4Stream(String fileId) {
        System.out.println("oss文件服务流式下载文件...");
        return null;
    }
}

看一下两者的目录结构:


image.png

这里保证了自研文件服务和第三方文件服务的类名、接口完全一致,原因上面已经说过了。
这里因为是为了方便演示放在同一个项目下所以两者的包路径差别不大,实际上两者的包路径可以说是没有任何相似之处。

再复述一下我们在双写阶段的目标:文件双写+对外暴露第三方文件服务定义的接口和方法
,提供如下适配器

package com.cube.dp.adapter;


import com.cube.dp.adapter.fs.third.IThirdPartyFileOperationService;
import com.cube.dp.adapter.fs.third.dto.FileDownloadDto;
import com.cube.dp.adapter.fs.third.dto.FileDownloadForStreamDto;
import com.cube.dp.adapter.fs.third.dto.FileUploadDto;
import com.cube.dp.adapter.fs.third.dto.FileUploadForStreamDto;

/**
 * @author litb
 * @date 2022/5/21 15:28
 * <p>
 * 文件操作适配器
 * 将自研文件服务与第三方文件服务适配
 */
public interface FileOperationAdaptee extends IThirdPartyFileOperationService {

    /**
     * 转换
     *
     * @param dto 参数
     * @return 结果
     */
    default com.cube.dp.adapter.fs.custom.dto.FileUploadDto to(FileUploadDto dto) {
        if (dto == null) {
            return null;
        }
        com.cube.dp.adapter.fs.custom.dto.FileUploadDto fileUploadDto = new com.cube.dp.adapter.fs.custom.dto.FileUploadDto();

        fileUploadDto.setFileName(dto.getFileName());
        fileUploadDto.setFileContent(dto.getFileContent());
        fileUploadDto.setContentType(dto.getContentType());
        fileUploadDto.setMetadata(dto.getMetadata());

        return fileUploadDto;
    }

    /**
     * 转换
     *
     * @param dto 参数
     * @return 结果
     */
    default com.cube.dp.adapter.fs.custom.dto.FileUploadForStreamDto to(FileUploadForStreamDto dto) {
        if (dto == null) {
            return null;
        }
        com.cube.dp.adapter.fs.custom.dto.FileUploadForStreamDto fileUploadDto = new com.cube.dp.adapter.fs.custom.dto.FileUploadForStreamDto();

        fileUploadDto.setFileName(dto.getFileName());
        //输入流一般是不能重复读取的,实际上应该采用byte数组拷贝或者转换成可重复读去的输入流,本案例只是为了演示适配器模式的用法,不严格考虑具体的实现细节了
        fileUploadDto.setFileContent(dto.getFileContent());
        fileUploadDto.setContentType(dto.getContentType());
        fileUploadDto.setMetadata(dto.getMetadata());

        return fileUploadDto;
    }


    /**
     * 转换
     *
     * @param dto 参数
     * @return 结果
     */
    default com.cube.dp.adapter.fs.custom.dto.FileDownloadDto to(FileDownloadDto dto) {
        if (dto == null) {
            return null;
        }
        com.cube.dp.adapter.fs.custom.dto.FileDownloadDto fileUploadDto = new com.cube.dp.adapter.fs.custom.dto.FileDownloadDto();

        fileUploadDto.setFileName(dto.getFileName());
        fileUploadDto.setFileContent(dto.getFileContent());
        fileUploadDto.setContentType(dto.getContentType());
        fileUploadDto.setMetadata(dto.getMetadata());

        return fileUploadDto;
    }

    /**
     * 转换
     *
     * @param dto 参数
     * @return 结果
     */
    default FileDownloadDto to(com.cube.dp.adapter.fs.custom.dto.FileDownloadDto dto) {
        if (dto == null) {
            return null;
        }
        FileDownloadDto fileUploadDto = new FileDownloadDto();

        fileUploadDto.setFileName(dto.getFileName());
        fileUploadDto.setFileContent(dto.getFileContent());
        fileUploadDto.setContentType(dto.getContentType());
        fileUploadDto.setMetadata(dto.getMetadata());

        return fileUploadDto;
    }

    /**
     * 转换
     *
     * @param dto 参数
     * @return 结果
     */
    default com.cube.dp.adapter.fs.custom.dto.FileDownloadForStreamDto to(FileDownloadForStreamDto dto) {
        if (dto == null) {
            return null;
        }
        com.cube.dp.adapter.fs.custom.dto.FileDownloadForStreamDto fileUploadDto = new com.cube.dp.adapter.fs.custom.dto.FileDownloadForStreamDto();

        fileUploadDto.setFileName(dto.getFileName());
        //输入流一般是不能重复读取的,实际上应该采用byte数组拷贝或者转换成可重复读去的输入流,本案例只是为了演示适配器模式的用法,不严格考虑具体的实现细节了
        fileUploadDto.setFileContent(dto.getFileContent());
        fileUploadDto.setContentType(dto.getContentType());
        fileUploadDto.setMetadata(dto.getMetadata());

        return fileUploadDto;
    }

    /**
     * 转换
     *
     * @param dto 参数
     * @return 结果
     */
    default FileDownloadForStreamDto to(com.cube.dp.adapter.fs.custom.dto.FileDownloadForStreamDto dto) {
        if (dto == null) {
            return null;
        }
        FileDownloadForStreamDto fileUploadDto = new FileDownloadForStreamDto();

        fileUploadDto.setFileName(dto.getFileName());
        //输入流一般是不能重复读取的,实际上应该采用byte数组拷贝或者转换成可重复读去的输入流,本案例只是为了演示适配器模式的用法,不严格考虑具体的实现细节了
        fileUploadDto.setFileContent(dto.getFileContent());
        fileUploadDto.setContentType(dto.getContentType());
        fileUploadDto.setMetadata(dto.getMetadata());

        return fileUploadDto;
    }

}

该适配器首先继承了第三方文件服务的定义的接口IThirdPartyFileOperationService,提供了一些将新旧文件服务参数相互转换的默认方法。
再提供一个抽象的双写适配器

package com.cube.dp.adapter;

import com.cube.dp.adapter.fs.custom.IFileOperationService;
import com.cube.dp.adapter.fs.third.IThirdPartyFileOperationService;
import com.cube.dp.adapter.fs.third.dto.FileUploadDto;
import com.cube.dp.adapter.fs.third.dto.FileUploadForStreamDto;

/**
 * @author litb
 * @date 2022/5/21 16:52
 * <p>
 * 抽象文件双写适配器,文件上传时,会先将文件上传只自研文件服务实现,然后再将该文件上传至第三方文件服务实现,
 * 同一个文件上传后在自研文件服务和第三方文件服务内的文件id是一致的
 */
public abstract class AbstractFileDoubleWriteOperationAdaptor implements FileOperationAdaptee {

    private final IFileOperationService fileOperationService;

    private final IThirdPartyFileOperationService thirdPartyFileOperationService;

    public AbstractFileDoubleWriteOperationAdaptor(IFileOperationService fileOperationService,
                                                   IThirdPartyFileOperationService thirdPartyFileOperationService) {
        this.fileOperationService = fileOperationService;
        this.thirdPartyFileOperationService = thirdPartyFileOperationService;
    }

    @Override
    public String upload(FileUploadDto dto) {
        String fileId = fileOperationService.upload(to(dto));
        //使用上传至自研文件服务的文件id作为第三方文件服务上传文件时的key,这样就能够报证同一个文件在两个文件服务的文件id是一致的,这个逻辑这里就不写了
        return thirdPartyFileOperationService.upload(dto);
    }

    @Override
    public String upload4Stream(FileUploadForStreamDto streamDto) {
        String fileId = fileOperationService.upload4Stream(to(streamDto));
        //使用上传至自研文件服务的文件id作为第三方文件服务上传文件时的key,这样就能够报证同一个文件在两个文件服务的文件id是一致的,这个逻辑这里就不写了
        return thirdPartyFileOperationService.upload4Stream(streamDto);
    }

    public IFileOperationService getFileOperationService() {
        return fileOperationService;
    }

    public IThirdPartyFileOperationService getThirdPartyFileOperationService() {
        return thirdPartyFileOperationService;
    }
}

再提供一个文件上传时双写且从自研文件服务中读取文件的适配器

package com.cube.dp.adapter;

import com.cube.dp.adapter.fs.custom.IFileOperationService;
import com.cube.dp.adapter.fs.third.IThirdPartyFileOperationService;
import com.cube.dp.adapter.fs.third.dto.FileDownloadDto;
import com.cube.dp.adapter.fs.third.dto.FileDownloadForStreamDto;

/**
 * @author litb
 * @date 2022/5/21 17:08
 * <p>
 * 上传时双写,读取时从自研文件服务中读取的适配器
 */
public class DoubleWriteAndReadCustomOperationAdaptor extends AbstractFileDoubleWriteOperationAdaptor {

    public DoubleWriteAndReadCustomOperationAdaptor(IFileOperationService fileOperationService,
                                                    IThirdPartyFileOperationService thirdPartyFileOperationService) {
        super(fileOperationService, thirdPartyFileOperationService);
    }

    @Override
    public FileDownloadDto download(String fileId) {
        return to(getFileOperationService().download(fileId));
    }

    @Override
    public FileDownloadForStreamDto download4Stream(String fileId) {
        return to(getFileOperationService().download4Stream(fileId));
    }
}

这里不提供抽象的文件双写适配器AbstractFileDoubleWriteOperationAdaptor直接在DoubleWriteAndReadCustomOperationAdaptor实现双写逻辑也是可以的(因为实际上只能从自研文件服务中读取,这样才能确保增量数据和存量数据都能读取成功)

再提供一个获取文件服务的工厂

package com.cube.dp.adapter;

import com.cube.dp.adapter.fs.custom.CustomFileOperationServiceImpl;
import com.cube.dp.adapter.fs.third.IThirdPartyFileOperationService;
import com.cube.dp.adapter.fs.third.OssFileOperationServiceImpl;

/**
 * @author litb
 * @date 2022/5/21 17:12
 * <p>
 * 文件服务工厂
 */
public class FileOperationFactory {


    public static IThirdPartyFileOperationService getDefault() {
        return getInstance(EnumFileOperationType.DOUBLE_WRITE_AND_READ_FROM_CUSTOM);
    }

    /**
     * 获取对应的文件服务实例
     *
     * @param operationType 类型
     * @return 实例
     */
    public static IThirdPartyFileOperationService getInstance(EnumFileOperationType operationType) {
        switch (operationType) {
            case OSS:
                return new OssFileOperationServiceImpl();
            case CUSTOM:
                return new CustomFileOperationAdaptor(new CustomFileOperationServiceImpl());
            case DOUBLE_WRITE_AND_READ_FROM_CUSTOM:
                return new DoubleWriteAndReadCustomOperationAdaptor(new CustomFileOperationServiceImpl(),
                        new OssFileOperationServiceImpl());
            default:
                throw new IllegalArgumentException("storageType is not support");
        }
    }
}

各个依赖方可以通过getDefault方法获取默认的文件服务实例,在双写阶段提供的是上传时双写+从自研文件服务中读取的实现,在第二阶段存量文件同步完成后升级一下第三方文件服务的版本完全移除自研文件服务的依赖并默认提供第三方文件服务实例即可,各个依赖方引入新的版本重新发布一下项目即可,改动程度相对较小。

假设有以下客户端依赖了文件服务:
一、文件双写+存量数据同步阶段

package com.cube.dp.adapter;

import com.cube.dp.adapter.fs.third.IThirdPartyFileOperationService;
import com.cube.dp.adapter.fs.third.dto.FileUploadDto;

/**
 * @author litb
 * @date 2022/5/21 17:16
 * <p>
 * 假装这是一个项目
 */
public class ProjectClient {

    public static void main(String[] args) {
        IThirdPartyFileOperationService operationService =
                FileOperationFactory.getInstance(EnumFileOperationType.DOUBLE_WRITE_AND_READ_FROM_CUSTOM);
        operationService.upload(new FileUploadDto());

        operationService.download("this is fileId");
    }
}

模拟下文件上传及下载,控制台输出如下:

自研文件服务上传文件...
oss文件服务上传文件...
自研文件服务下载文件...

可以看出,上传时是双写,读取时是从自研文件服务中读取。

二、存量数据同步完成之后
在存量数据同步完成之后,这个时候自研文件服务和oss中的存量数据和增量数据都是完全一致的,可以无缝切换到oss文件服务了,完全移除自研文件服务。

package com.cube.dp.adapter;

import com.cube.dp.adapter.fs.third.IThirdPartyFileOperationService;
import com.cube.dp.adapter.fs.third.dto.FileUploadDto;

/**
 * @author litb
 * @date 2022/5/21 17:16
 * <p>
 * 假装这是一个项目
 */
public class ProjectClient {

    public static void main(String[] args) {
        IThirdPartyFileOperationService operationService =
                //切换到oss实现,这一步在文件服务工程下完成即可,切换实现并完全移除自研文件服务后升级打包让各个依赖方重新引入
                FileOperationFactory.getInstance(EnumFileOperationType.OSS);
        operationService.upload(new FileUploadDto());

        operationService.download("this is fileId");
    }
}

模拟下文件上传和下载,控制台输出如下:

oss文件服务上传文件...
oss文件服务下载文件...

而且,考虑到不可控因素,还提供了基于第三方文件服务接口定义但是由自研文件服务提供实现的适配器

package com.cube.dp.adapter;

import com.cube.dp.adapter.fs.custom.IFileOperationService;
import com.cube.dp.adapter.fs.third.dto.FileDownloadDto;
import com.cube.dp.adapter.fs.third.dto.FileDownloadForStreamDto;
import com.cube.dp.adapter.fs.third.dto.FileUploadDto;
import com.cube.dp.adapter.fs.third.dto.FileUploadForStreamDto;

/**
 * @author litb
 * @date 2022/5/21 16:23
 * <p>
 * 文件适配器,将第三方文件服务与自研文件服务适配
 */
public class CustomFileOperationAdaptor implements FileOperationAdaptee {

    private final IFileOperationService customFileOperationService;

    public CustomFileOperationAdaptor(IFileOperationService customFileOperationService) {
        this.customFileOperationService = customFileOperationService;
    }

    @Override
    public String upload(FileUploadDto dto) {
        return customFileOperationService.upload(to(dto));
    }

    @Override
    public String upload4Stream(FileUploadForStreamDto streamDto) {
        return customFileOperationService.upload4Stream(to(streamDto));
    }

    @Override
    public FileDownloadDto download(String fileId) {
        return to(customFileOperationService.download(fileId));
    }

    @Override
    public FileDownloadForStreamDto download4Stream(String fileId) {
        return to(customFileOperationService.download4Stream(fileId));
    }
}

假设要突然完全切换到自研文件服务,在第三方文件服务项目中切换到自研文件重新升级打包即可,各个项目重新引入新版本即可,无需回滚代码(主要是包路径调整)。

上述适配器模式的类图如下所示:


文件服务-适配器模式类图.png

表达能力欠缺一直是我的诸多缺点之一,不知道我上面一顿啰嗦有没有把我的想法表达清楚,参考代码也许更易理解一点,附上链接:适配器模式代码

总结

说一下为什么中途写到适配器模式类型的定义时让我觉得突然不想再写了,就像倚天屠龙记里张三丰教张无忌太极拳和太极剑一样,张无忌看了一遍之后虽然全部都忘记了但是最后却能够娴熟的运用出来,忘与不忘在我看来其中的差别其实就是无形有形的区别,有形只是生搬硬套,而无形却是融会贯通;设计模式难学的主要原因就是人为的界定了许多界限,当我在看完适配器类型的定义之后我把我上面写的代码往这个定义去套,尝试去解释这里面的一些概念,但是最终我发现连我自己都难以说服,所以我决定不再纠结这些具体的概念和定义,只是把我的想法表达出来就行了。

最后,说一些我个人最近的一些感悟,我写代码的时间不算长,最开始时写代码的时候,我觉得代码是难以控制的,不确定在什么地方会出问题,总是害怕出bug;不知道是上个月啥时候开始,我突然觉得我是能够控制住代码的,能够参照一部分源码写出一部分我觉得还不错的代码(起码看起来还不错),这应该还是处于有形的阶段;但是,我觉得后面至少应该还有一个阶段,当我写出一段代码后我会由衷的感叹:这段代码是这样不是因为我想要这样写,而是这段代码原本就应该是这个样子,就算是再让我写一千遍一万遍,它也应该就是这个样子。

上一篇 下一篇

猜你喜欢

热点阅读