一文搞懂四种Wrapper方法快速重构你的代码

7,226 阅读10分钟

sdk对外暴露的接口不合理

我们经常使用一些sdk来完成我们的需求,但往往有些sdk对外暴露的接口并不合理,再加上如果这个sdk本身处于快速迭代期,每次变更某些api的话,业务方如果使用的地方较多,那么批量修改 其实也是比较麻烦的。

看看如何针对上述情况做一些改进

看一段示例代码:

public class SchoolSystemSdk {
    /**
     * 存储学生
     *
     * @param name 姓名
     * @param age  年纪
     * @param No   学号
     */
    public void saveStudent(String name, int age, int No) {

    }

    /**
     * 存储教师
     *
     * @param name          教师姓名
     * @param studentNumber 学生数量
     * @param schoolName    学校名称
     */
    public void saveTeacher(String name, int studentNumber, String schoolName) {

    }
}

public class TestMain {
    public static void main(String[] args) {
        SchoolSystemSdk schoolSystemSdk=new SchoolSystemSdk();
        schoolSystemSdk.saveStudent("wuyue",18,12345);
        schoolSystemSdk.saveTeacher("wuyanzu",18,"南京大学");
    }
}

问题就是这个sdk对外提供的函数参数过多,没有封装性,且可以很容易想到日后这个sdk如果新增参数的话,我们的业务代码肯定要批量修改调用的地方,很是蛋疼。但是我们又没有权限修改sdk的源码(暂不考虑asm等字节码技术)

下面进行修改: 首先定义一下学生和老师

class Student {
    String name;
    int age;
    int No;
}

class Teacher {
    String name;
    int studentNumber;
    String schoolName;
}

再定义一个接口 ,注意接口的参数

public interface ISchoolSystem {
    void saveStudent(Student student);

    void saveTeacher(Teacher teacher);
}

然后定义我们的主类 注意类的声明

//extends了sdk的类 且实现了接口
public class SchoolSystemWrapper extends SchoolSystemSdk implements ISchoolSystem {
    @Override
    public void saveStudent(Student student) {
        saveStudent(student.name, student.age, student.No);
    }

    @Override
    public void saveTeacher(Teacher teacher) {
        //懒癌发作 就此省略
    }
}

最后调用

public class TestMain {
    public static void main(String[] args) {
        ISchoolSystem iSchoolSystem = new SchoolSystemWrapper();
        //懒癌发作  这边构造函数 就没有传参了,大家自行领会意图
        iSchoolSystem.saveStudent(new Student());
        iSchoolSystem.saveTeacher(new Teacher());
    }
}

很简单的几步 就可以把我们的痛点全部解决了。 即解决了原生sdk对外暴露函数参数不好的问题,也可以解决日后sdk更新时 多处修改的问题,日后有修改也只要在wrapper那里 改一处即可

功能类似的第三方sdk 统一管理

这个场景在业务中也不少见,比如某些需求 我们需要使用不同的第三方sdk 做处理,我们希望收拢这些不同第三方sdk的接口,然后对外暴露出一个简单的接口。

举例说明: 一段文本,我们需要过滤掉黄色信息,少儿不宜的信息,以及不符合政策法规的信息。 让我们看看这段代码应该如何写:

//过滤掉不良的黄色信息
public class AWordsFilter {
    public String filterSexyWords(String text) {
        return "";
    }
}

//过滤掉政治信息
class BWordsFilter {
    public String filterPoliticalWords(String text) {
        return "";
    }
}

//过滤掉工信部给的敏感信息 senseWords 为敏感词
class CWordsFilter {
    public String filterSenselWords(String text, String senseWords) {
        return "";
    }
}

使用他

public class TestMain {
    public static void main(String[] args) {
        //原始文本
        String text = "1231231asdasdasdasda";
        //三种过滤规则
        AWordsFilter aWordsFilter = new AWordsFilter();
        BWordsFilter bWordsFilter = new BWordsFilter();
        CWordsFilter cWordsFilter = new CWordsFilter();
        //过滤成最终想要的信息
        String t1 = aWordsFilter.filterSexyWords(text);
        String t2 = bWordsFilter.filterPoliticalWords(t1);
        String t3 = cWordsFilter.filterSenselWords(t2, "蛤蛤");
    }
}

开始优化代码 首先定义2个接口:

public interface IWordsFilter {
    String filter(String text);
}
public interface IWordsSenseFilter extends IWordsFilter {
    String filter(String text, String sense);
}

然后来实现他们:

public class AWordsFilterAdaptor implements IWordsFilter {
    AWordsFilter aWordsFilter;

    @Override
    public String filter(String text) {
        return aWordsFilter.filterSexyWords(text);
    }
}

class BWordsFilterAdaptor implements IWordsFilter {
    BWordsFilter bWordsFilter;

    @Override
    public String filter(String text) {
        return bWordsFilter.filterPoliticalWords(text);
    }
}

class CWordsFilterAdaptor implements IWordsSenseFilter {
    CWordsFilter cWordsFilter;

    @Override
    public String filter(String text, String sense) {
        return cWordsFilter.filterSenselWords(text, sense);
    }

    @Override
    public String filter(String text) {
        return null;
    }
}

最后统一管理

class FilterManager {
    List<IWordsFilter> filters = new ArrayList<>();

    public void addFilterType(IWordsFilter iWordsFilter) {
        filters.add(iWordsFilter);
    }

    public String filterAll(String text, String sense) {
        String maskText = text;
        for (IWordsFilter iWordsFilter : filters) {
            if (iWordsFilter instanceof IWordsSenseFilter) {
                maskText = ((IWordsSenseFilter) iWordsFilter).filter(maskText, sense);
            } else {
                maskText = iWordsFilter.filter(text);
            }

        }
        return maskText;
    }
}

看下使用效果,是不是可以达到收拢的效果

   String text = "1231231asdasdasdasda";
        FilterManager filterManager=new FilterManager();
        filterManager.addFilterType(new AWordsFilterAdaptor());
        filterManager.addFilterType(new BWordsFilterAdaptor());
        filterManager.addFilterType(new CWordsFilterAdaptor());
        filterManager.filterAll(text,"蛤蛤");

到这里应该可以理解,这种adapter模式的写法,就是特别适合如下场景: 一种事后的补救策略,你无法控制sdk的代码质量,或者说虽然不是sdk代码,但是老代码坑比较多,一时半会不好大改,但是可以通过这种adapter的方式来达到修正你业务代码质量的目标

非功能性需求如何与业务代码做分离

平时开发中我们应该经常碰到类似的需求,比如说做了一些需求,但是要针对这些需求做监控,日志,埋点,统计,鉴权等等,这些东西的开发本质上和我们的业务开发 是分离的,不应该将他们的代码写在一起。那么如何做分离呢? 这里举一个例子

假设我们这里有一个用户的登录注册功能

public class UserController {
    public void login() {

    }

    public void register() {
        
    }
}

现在要求对这个登录注册做一些监控,比如记录一下登录和注册行为发起的时间,记录下用户的id等等。 直接将这些代码写到我们本身的登录注册业务中 是极其不优雅 也是不好维护的。

如果我们懒一点,不想修改我们的UserController源码 那就用extends的方式来做

 class UserControllerProxy extends UserController
{
    @Override
    public void login() {
        //在这里做一些监控的操作 代码省略
        super.login();
    }

    @Override
    public void register() {
        //在这里做一些监控的操作 代码省略
        super.register();
    }
}

勤快一点 ,可以定义接口来做

public class UserController implements IUserCon {
    public void login() {

    }

    public void register() {

    }
}

interface IUserCon {
    void login();

    void register();
}

//数据统计
class DataStatic {
    public void doSome() {
        
    }
}

class UserControllerProxy implements IUserCon {

    public UserControllerProxy(IUserCon iUserCon) {
        this.iUserCon = iUserCon;
    }

    private IUserCon iUserCon;
    private DataStatic dataStatic = new DataStatic();

    @Override
    public void login() {
        //在这里做一些监控的操作 代码省略
        dataStatic.doSome();
        iUserCon.login();
    }

    @Override
    public void register() {
        //在这里做一些监控的操作 代码省略
        dataStatic.doSome();
        iUserCon.register();
    }
}

到这里还没完噢,还有一种情况,就是如果说被我们代理的类页面方法很多,我们如果用上述的方法 来做proxy就真的很愚蠢了,因为你有大量的重复代码 要写在不同的函数中。 又是复制粘贴的做法了。

另外你想,如果说你这种埋点或者是监控类的需求比较多,那被代理的类 就很多了,不止登录注册啊,还有其他模块都要使用,你是不是要手动添加更多的Proxy类?万一哪天这些埋点的方法有变动,想想要改多少地方?

要解决这个问题 也很简单

动态代理 动态生成我们的类,动态的修改Proxy中的代码 即可

public class UserDynamicProxy implements InvocationHandler {

    DataStatic dataStatic;

    public UserDynamicProxy(Object target) {
        this.target = target;
        dataStatic = new DataStatic();
    }

    Object target;

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        dataStatic.doSome();
        Object result = method.invoke(target, args);
        return result;
    }

    public <T> T getProxy() {
        return (T) Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), this);
    }

}

使用起来也比较简单

     UserDynamicProxy dynamicProxy = new UserDynamicProxy(new UserController());
        IUserCon iUserCon = dynamicProxy.getProxy();
        iUserCon.register();

如果你在项目中恰好遇到这种需求,可以多用用动态代理,很强的功能,极简的写法。原理在本篇就不做过多介绍了, 不是本文的重点。有兴趣的可以参见动态代理原理

最后强调一下: 代理模式常用在业务系统中开发一些非功能性需求,比如:监控、统计、鉴权、限流、埋点、日志。我们将这些附加功能与业务功能解耦,放到代理类统一处理,让业务开发的同学只关注业务,不需要关注基础功能。 在不改变原始类接口的条件下,为原始类定义一个代理类,主要目的是控制访问,而非加强功能

java IO 中的设计哲学

java io可能是java 体系里面最复杂的一个模块了,经常用,但是很少有人能讲清楚为啥这么设计,今天我们就扒了它的皮,看看能从中学习到什么

首先看一张老图, 应该很多人都很熟悉了

我们现在尝试读取一个文件,为了提高效率我们使用缓存buffer

InputStream in=new FileInputStream("F:\\Java huashan.pdf");
InputStream bin=new BufferedInputStream(in);

大家有没有发现 这样写有什么异样?为啥一定要用一个inputStream去初始化一个bufferins呢,为啥fileins可以直接传个path就创建,bufferins就不行?

bufferins的构造函数:

看看fileins的构造函数

所以这里问题就来了,为什么bufferins这个类的构造函数是ins作为参数,而不能直接设置成像fileins一样, 直接接受一个path作为参数?

我们直接这样写

 InputStream in=new BufferedFileInputStream("F:\\Java huashan.pdf");

不好吗?

我们再回头看看之前的ins的子类 他是不止一个fileins子类的,他还有其他多个子类。如果我们单独在fileins后面派生一个bufferfins的子类,那么其他子类要使用这个功能 也得派生出对应的bufferins子类。这样就显的非常冗余了。最终的结果就是子类爆炸

所以在java的世界里面是有一个基本设计原则的: 组合大于集成

所以在java io世界里 很多类都不是直接派生自InputStrem 而是持有ins的一个实例

就像一个Wrapper 包裹住他们一样。

细心的人会发现 Bufferins与DataIns 都不是直接extends的ins啊 而是extends的

这又是什么设计?

众所周知的是我们的ins 是一个抽象类

public abstract class InputStream implements Closeable {

所以为了让我们增强功能的io类,比如bufferins datainputs 只实现一些他们增强功能的方法,哪些没有变动的方法自然就放到filterins里面默认实现他了。

这里还会有人问 既然inputstream是一个抽象类,那你说的默认实现为啥不在inputSteream里面写? 为啥要画蛇添足加一个filterins来写? 我们看个图

我们可以想一下,如果我们将默认的实现写在InputStream中,去掉这个filterIns层,

new Bufferins(new FileInput("path"))

这个时候我们Buffins 的那些ins默认的方法 我们必须要重写一遍 形如:

InputStrean ins;

public void f(){
    ins.f()
}

因为如果我们不这么写,那我们bufferins的默认f方法走的就是ins中的默认实现 而不是我们传进来的FileInput实现了。

想清楚这点 我们就可以猜到FilterInput到底是什么作用了

所以你看

FilterInput这个类 帮我们解决的 不就是上述的痛点吗?可以避免我们再手动复制粘贴一次哪些默认的方法调用而已。

分析到这里我们就来总结一下java io世界中 所采用的装饰者wrapper写法 主要用来解决什么问题。

主要解决继承关系过于复杂的问题,通过组合来替代继承。它主要的作用是给原始类添加增强功能。这是判断是否该用装饰器模式的一个重要的依据,他跟Proxy的写法是不同的**,Proxy主要是新增跟原来功能不相干的功能,而装饰者是增强原来的功能,或者说是增加类似的功能,但是带有优化的效果**。除此之外,装饰器模式还有一个特点,那就是可以对原始类嵌套使用多个装饰器。为了满足这个应用场景,在设计的时候,装饰器类需要跟原始类继承相同的抽象类或者接口