开闭原则的理解

212 阅读5分钟

「这是我参与2022首次更文挑战的第3天,活动详情查看:2022首次更文挑战image.png 最近有同事小薛在看mybatis的源码,在看源码的过程中发现,源码中经常会有“奇怪的代码”,如接口:org.apache.ibatis.reflection.factory.ObjectFactory中的create方法,追到最后只是实例化了一下对象,如下图:

image.png 如上图所示,学过Java的应该不难看出这里只是调用了jdk自带的实例化方法。 同事十分不解,为什么实例化一下的事情要搞个接口,还要弄个实现类,最后就只是简单的掉了jdk自带的方法,如果只是为了处理异常和简化开发操作不是写个工具类(utils)不就可以了吗?何必这么大阵仗。
听了同事的吐槽,我想起了当年的自己也是这个想法,现在回想起来当时的自己是真的naive!!!
其实作为CURD工程师,很少有机会写基础的通用框架包供其他同事使用,每天都是开发web功能,虽然一直提倡面向接口编程,但是仍旧不胜了解接口的作用,毕竟往往开发web的service和repository的接口实现只有一个。这里我们抛开web的场景,站在框架开发者的角度,来看看接口到底为我们提供怎样了的能力。

实现场景一

现在假设我们作为公司的核心开发人员,要为公司开发一个类似mybatis的框架为项目的持久层提供数据访问和操作的能力,其中有个功能是处理数据库的返回值。在mybatis中,要根据resultMap标签指定的返回值类型实例化对应的实体类,objectFactory帮助提供了一部分能力。如果我们要实现这块功能,我们可以有如下的代码实现:

public class RepositoryHelperImpl implements RepositoryHelper {
    public <T> List<T> queryAndHandleResult(String sql, Class<T> handleType) {
       // 各种查询
       List<Map<String, Object>> queryValue = queryList(sql);
       List<T> result = new ArrayList<>();
       for (Map<String, Object> objectKeyValueMap : queryValue) {
          // 这里直接调用了jdk的实例化方法
          T t = handleType.newInstance();
          result.add(t);
          // 处理字段映射  
          ....
       }
       return result;
    }
}

实现场景二

当我们开发完成之后,交付到其它开发组使用,开始可能没有问题,但是过了不久,其他组的成员发现不行,实例化调用的是无参构造,但是有些类没有无参构造,需要添加支持。于是我们有了如下实现:

public class RepositoryHelperImpl implements RepositoryHelper {
    public <T> List<T> queryAndHandleResult(String sql, Class<T> handleType) {
       // 各种查询
       ....
       List<Map<String, Object>> queryValue = queryList(sql);
       List<T> result = new ArrayList<>();
       for (Map<String, Object> objectKeyValueMap : queryValue) {
          T t;
          // 处理字段映射
          Constructor<T> constructor = handleType.getConstructor();
          if (constructor == null) {
             t = handleType.newInstance();
          } else {
             Object key1 = objectKeyValueMap.get("key1");
             Object key2 = objectKeyValueMap.get("key2");
             constructor = handleType.getConstructor(Type1.class, Type2.class);
             t = constructor.newInstance(key1, key2);
          }
          result.add(t);
       }
       ....
       return result;
    }
}

我们又完成了开发,开开心心的交付给其他组使用去了。这样又过不久,其他组又提了,需要提供工厂模式实例化对象,这个时候我们肯定是不耐烦了,但是又无可奈何去修改代码,提供具体的实现。
在这里我们需要区分两个对象:
1、框架开发者:即本场景中我们所处的职位,负责为其他组提供基础设施框架。
2、框架使用者:接触不到框架源码,只能使用jar包提供的api,在本场景中,对框架提供能力无法进行扩展,因为这部分被我们写死在代码里面了。

接口抽取

接下来讲讲作为一个考虑周全的框架的处理方式。其实我们已经知道了,mybatis把这部分逻辑抽象为接口,即ObjectFactory接口,实例化的工作委托ObjectFactory提供的接口api去做。代码看起来如下:

public class RepositoryHelperImpl implements RepositoryHelper {

    ObjectFactory objectFactory;

    public void setObjectFactory(ObjectFactory objectFactory) {
       this.objectFactory = objectFactory;
    }
    public void init() {
       // 没有设置对象实例化工厂则取默认实现
       if (objectFactory == null) {
          objectFactory = new DefaultObjectFactory();
       }
    }

    public <T> List<T> queryAndHandleResult(String sql, Class<T> handleType) {
       // 各种查询
       ....
       List<Map<String, Object>> queryValue = queryList(sql);
       List<T> result = new ArrayList<>();
       for (Map<String, Object> objectKeyValueMap : queryValue) {
          // 调用接口方法实例化对象
          T t = objectFactory.createObject(objectKeyValueMap, handleType);
          // 处理字段映射
          result.add(t);
       }
       ....
       return result;
    }
}

看见了吗?对象的实例化操作被接口封装了,我们没有在代码里面写死具体的逻辑,并提供默认实现支持具体的需求。这时候我们交付其他组去使用,如果他们对对象实例化方式不满,想让你修改对应代码,这时候你可以大方的让他们自己去实现。看见setObjectFactory方法了吗?虽然其他组的成员改不了源码,但是他们可以自己实现ObjectFactory,通过setObjectFactory方法替换掉这块逻辑。
看到这,你应该明白我想说啥了。对,这符合设计模式的开闭原则:对扩展开放,对修改封闭,通过接口,我们成功将易变的逻辑抽离了出去。

总结

开闭原则要求我们能够识别系统中容易变动的需求逻辑,并将这些逻辑以接口的形式暴露给使用方,让使用方有方法影响框架的逻辑。其实作为一个框架,只能是承担系统中的一部分内容,需求总是变动的,框架无法承包所有的功能实现,对于变动的需求,也无法做到实时跟进,如果不将这些经常变动的代码抽离出去,框架的使用者就只能拿源码自己改,这违反了开闭原则,而且对使用者来讲,这种方式是极不友好的。要做到对扩展开放,对修改关闭,我的建议如下:
1、对于你不确定的逻辑,可以抽取成接口
2、接口要有默认实现
3、要提供方法替换默认的实现逻辑
4、接口要遵循单一职责原则,不要包揽很多功能,毕竟谁也不想扩展没必要的实现
完,感谢观看