接 设计之附件上传与下载

1. 前言

距离上一篇已经四个月了,该组件也在公司内部广泛使用。正所谓“唯一的不变就是变化本身”,随着应用范围的增大,意料之外的需求也就随之产生了。比如最近碰到的一个。感觉最终的解决方案比较有代表性,所以在这里表述下,希望对他人能有所帮助。再次声明本人对设计的理解完全是靠自己一点点悟出来的,所以有什么纰漏在所难免,还望不吝赐教。

需求是这样的,在最初的设计中,我们在进行配置文件设计时,上传和下载的地址被设置为只允许设置为一个。但最近这个需求就是需要颠覆这个要求。详细的业务场景就不说了,主要的需求就是允许设置多个地址,然后在下载时,从前往后遍历,直到找到为止。

2. 思考

经过近一个小时的冷静和调整,在脑海里转了无数次之后,终于大概地将应对本次需求的方案敲定。考虑大概如下:
1. 首先这段逻辑肯定不能写死在主体逻辑里面。
2. 个人也不希望增加一个额外的配置项,才迭代到第二个版本就需要作出这样的抉择,显得框架的适应性太差了。
3. 本次的需求其实是非常具体的,我们应该站在一个更高的层面上看待这个问题。也就是说我们要设计出一个通用的场景,本次需求只是该通用场景下的特殊情形。

3. 解决方案

3.1 契约

基于上面的思路,于是有了如下的接口定义

/**
 * 抉择者
 * @author LQ
 *
 */
public interface KanqBasePathDeterminer {
    /**
     * @param currentOperater 当前进行附件操作的操作实例
     * @param invocation 直接来自Mybatis, 封装了本次进行附件操作的方法的相关信息
     * @return 最终挑选出的根路径
     */
    String determine(AffixOperater currentOperater,Invocation invocation);
}

回顾整个需求,我们:
1. 最终所关心的是满足要求的BasePath,
2. 而引起变化的则是在一堆外部的变化因素影响。
3. 因此我们最终敲定了以上的接口。

顺势就有了如下三个实现类:

3.2 KanqBasePathDeterminerSingle实现类

等同于需求升级前的版本。直接从配置文件中读取相应的值作为BasePath。

/**
 * <p>配置文件中只有一个显式的基路径 : <setting name="affixBasePath" value="E:/tmp/logs/" />
 * <p>此时直接从配置文件中读取即可
 * @author LQ
 *
 */
class KanqBasePathDeterminerSingle implements  KanqBasePathDeterminer, IConfigAware{

    private ConfigFile config;

    @Override
    public String determine(AffixOperater currentOperater, Invocation invocation) {
        return config.getAffixBasePath();
    }

    @Override
    public void setConfig(ConfigFile config) {
        this.config = config;       
    }
}
3.3 KanqBasePathDeterminerSimpleMulti实现类

该实现类就是满足当前需求的版本。

相关配置文件

<settings>
    <setting name="affixPathType" value="file" />
    <!-- 多个路径之间使用 ; 进行分割。 -->
    <setting name="affixBasePath" value="E:/tmp/;E:/;D:/" />
</settings>

具体实现类

/**
 * <p>配置文件中有 ; 分割的多个基路径 : <setting name="affixBasePath" value="E:/tmp/logs/;E:/;D:/" />
 * @author LQ
 *
 */
class KanqBasePathDeterminerSimpleMulti implements  KanqBasePathDeterminer, IConfigAware{

    private ConfigFile config;

    @Override
    public String determine(AffixOperater currentOperater, Invocation invocation) {
        final Method method = invocation.getMethod();
        final String methodName = method.getName();
        final String affixBasePath = config.getAffixBasePath();
        final String[] splitArr = affixBasePath.split(";");     
        // 上传操作, 取第一个路径
        // FIXME 这里依赖于方法名, 这样并不好....
        if (methodName.contains("upload")) {            
            return splitArr[0];
        }


        final String relativePath = (String)invocation.getArgs()[0];


        for (final String basePath : splitArr) {
            if (currentOperater.isExist(FilenameUtil.concat(basePath, relativePath))) {
                return basePath;
            }
        }

        return null;

    }

    @Override
    public void setConfig(ConfigFile config) {
        this.config = config;       
    }
}
3.4 KanqBasePathDeterminerCustom实现类

完全自定义逻辑。由用户通过实现KanqBasePathDeterminer接口来完成完全自主的自定义逻辑。

/**
 * <p>配置文件中有 . 分割的完整类名 : <setting name="affixBasePath" value="com.xx.yy.zzDeterminer" />
 * <p> 完全由用户自定义
 * @author LQ
 *
 */
class KanqBasePathDeterminerCustom implements  KanqBasePathDeterminer, IConfigAware{

    private ConfigFile config;

    @Override
    public String determine(AffixOperater currentOperater, Invocation invocation) {
        final String affixBasePath = config.getAffixBasePath();
        // 实例化用户的自定义类
        final KanqBasePathDeterminer customDeterminer = ClassUtil.<KanqBasePathDeterminer>newInstance(affixBasePath);       
        return config.config(customDeterminer).determine(currentOperater,invocation);       
    }

    @Override
    public void setConfig(ConfigFile config) {
        this.config = config;       
    }
}
3.5 KanqBasePathDeterminerFactory

我们最终使用工厂模式隐藏构建细节。对外只暴露最开始定义的接口,将实现的细节隐藏起来,这也是为什么会将三个实现类的访问修饰符设置为package的原因。

/**
 * 工厂模式
 * @author LQ
 *
 */
public class KanqBasePathDeterminerFactory {
    public static KanqBasePathDeterminer match(final ConfigFile config){
        final String affixBasePath = config.getAffixBasePath();
        // 如果 affixBasePath有以下表现 ,我们可能就需要在运行时才决定真正上传/下载的基本路径  
        //    1. 以 ; 分割, 则猜测为 多路径抉择,               -- 此时我们会将其规约为下面这种情况
        //    2. 以 . 分割, 则猜测有自定义的实现规则来进行路径抉择 -- 此时为自定义类实现       
        if (affixBasePath.contains(";")) {
            return new KanqBasePathDeterminerSimpleMulti();
        }else if(affixBasePath.contains(".") && ClassUtil.isPresent(affixBasePath, null)){
            return new KanqBasePathDeterminerCustom();
        }else{
            return new KanqBasePathDeterminerSingle();
        }


    }
}
2.6 并入主体逻辑

AffixOperaterManager中,在将主体逻辑分发给各个附件操作实例前,挑选出满足本次附件操作要求的BasePath。

这里就只出下载相关的实现逻辑,上传的思路是一样的。

@Override
public KanqResource download(String path) {
    // 必须在这里先处理一次, 再传递给下面的方法;
    path = preDealPath(path);
    final String finalPath = determineFinalFullath(path, "download", new Object[] { path });
    return affixOperaterAdapter.doDwonload(finalPath);
}

/**
 * 预处理
 * @param path
 * @return
 */
private String preDealPath(String path) {
    while (path.startsWith("/") || path.startsWith("\\")) {
        path = path.substring(1);
    }
    return path;
}

private final String determineFinalFullath(String path, final String methodName, Object[] args) {       
    final String basePath = determineKanqBasePath(methodName, args);
    final String finalPath = FilenameUtil.concat(basePath, path);

    return finalPath;
}

private String determineKanqBasePath(final String methodName, Object[] args) {
    KanqBasePathDeterminer determiner = KanqBasePathDeterminerFactory.match(configFile);
    determiner = configFile.config(determiner);

    Class<?>[] parameterTypes = new Class<?>[args.length];
    for (int i = 0; i < args.length; i++) {
        Object arg = args[i];
        parameterTypes[i] = arg.getClass();
    }

    final Method method = ReflectUtil.getMethod(this.getClass(), methodName, parameterTypes);       

    Invocation invocation = new Invocation(this, method, args);
    return determiner.determine(affixOperaterAdapter, invocation);
}

3. 回顾

刚刚接到这个需求时,内心还是有点小激动的,毕竟检验之前的设计的机会终于来了,一个好的构架的最基本也是最重要的要素之一就是能从容面对需求的变更,少量的改动就能在确保现有功能完整的前提下适应新需求的增加和改变

在真正开始之后突然觉得好陌生,最开始还以为是好几个月没碰了,代码有些生疏的缘故。细细回想之下,发现是因为过去两周都在编写其他框架的说明文档和给同事的培训文档,以及一些公司马上要使用到的新的框架和组件的了解。已经快一个月没有进行架构和设计的思考总结了。现世报也太快了吧。

4. Links

  1. 设计之附件上传与下载