「这是我参与2022首次更文挑战的第3天,活动详情查看:2022首次更文挑战」
最近有同事小薛在看mybatis的源码,在看源码的过程中发现,源码中经常会有“奇怪的代码”,如接口:
org.apache.ibatis.reflection.factory.ObjectFactory中的create方法,追到最后只是实例化了一下对象,如下图:
如上图所示,学过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、接口要遵循单一职责原则,不要包揽很多功能,毕竟谁也不想扩展没必要的实现
完,感谢观看