我们项目中一段典型的代码,问题明显
- 违反职责单一原则(一个method做了4件事)且 updateFromMQ命名过于抽象模糊
- 面向过程平铺直叙(变量作用域大、难以维护)
@Override
public void updateFromMQ(String compress) {
try {
JSONObject object = JSON.parseObject(compress);
if (StringUtils.isBlank(object.getString("type")) || StringUtils.isBlank(object.getString("mobile")) || StringUtils.isBlank(object.getString("data"))){
throw new AppException("MQ返回参数异常");
}
logger.info(object.getString("mobile")+"<<<<<<<<<获取来自MQ的授权数据>>>>>>>>>"+object.getString("type"));
Map map = new HashMap();
map.put("type",CrawlingTaskType.get(object.getInteger("type")));
map.put("mobile", object.getString("mobile"));
List<CrawlingTask> list = baseDAO.find("from crt c where c.phoneNumber=:mobile and c.taskType=:type", map);
redisClientTemplate.set(object.getString("mobile") + "_" + object.getString("type"),CompressUtil.compress( object.getString("data")));
redisClientTemplate.expire(object.getString("mobile") + "_" + object.getString("type"), 2*24*60*60);
//保存成功 存入redis 保存48小时
CrawlingTask crawlingTask = null;
// providType:(0:新颜,1XX支付宝,2:ZZ淘宝,3:TT淘宝)
if (CollectionUtils.isNotEmpty(list)){
crawlingTask = list.get(0);
crawlingTask.setJsonStr(object.getString("data"));
}else{
//新增
crawlingTask = new CrawlingTask(UUID.randomUUID().toString(), object.getString("data"),
object.getString("mobile"), CrawlingTaskType.get(object.getInteger("type")));
crawlingTask.setNeedUpdate(true);
}
baseDAO.saveOrUpdate(crawlingTask);
//保存芝麻分到xyz
if ("3".equals(object.getString("type"))){
String data = object.getString("data");
Integer zmf = JSON.parseObject(data).getJSONObject("taobao_user_info").getInteger("zm_score");
Map param = new HashMap();
param.put("phoneNumber", object.getString("mobile"));
List<Dperson> list1 = personBaseDaoI.find("from xyz where phoneNumber=:phoneNumber", param);
if (list1 !=null){
for (Dperson dperson:list1){
dperson.setZmScore(zmf);
personBaseDaoI.saveOrUpdate(dperson);
AppFlowUtil.updateAppUserInfo(dperson.getToken(),null,null,zmf);
}
}
}
//查询多租户表 身份认证、淘宝认证 为0 置为1
} catch (Exception e) {
logger.error("更新my MQ授权信息失败", e);
throw new AppException(e.getMessage(),e);
}
}
重构过程:
- 读代码 识别出其中包含的4段逻辑;
- 提取模板抽象类;
- 扩展模板类填空实现;
abstract class AbsUpdateFromMQ {
public final void doProcess(String jsonStr) {
try {
JSONObject json = doParseAndValidate(jsonStr);
cache2Redis(json);
saveJsonStr2CrawingTask(json);
updateZmScore4Dperson(json);
} catch (Exception e) {
logger.error("更新my MQ授权信息失败", e);
throw new AppException(e.getMessage(), e);
}
}
protected abstract void updateZmScore4Dperson(JSONObject json);
protected abstract void saveJsonStr2CrawingTask(JSONObject json);
protected abstract void cache2Redis(JSONObject json);
protected abstract JSONObject doParseAndValidate(String json) throws AppException;
}
填空实现
@SuppressWarnings({ "unchecked", "rawtypes" })
public void processAuthResultDataCallback(String compress) {
new AbsUpdateFromMQ() {
@Override
protected void updateZmScore4Dperson(JSONObject json) {
//保存芝麻分到xyz
if ("3".equals(json.getString("type"))){
String data = json.getString("data");
Integer zmf = JSON.parseObject(data).getJSONObject("taobao_user_info").getInteger("zm_score");
Map param = new HashMap();
param.put("phoneNumber", json.getString("mobile"));
List<Dperson> list1 = personBaseDaoI.find("from xyz where phoneNumber=:phoneNumber", param);
if (list1 !=null){
for (Dperson dperson:list1){
dperson.setZmScore(zmf);
personBaseDaoI.saveOrUpdate(dperson);
AppFlowUtil.updateAppUserInfo(dperson.getToken(),null,null,zmf);
}
}
}
}
@Override
protected void saveJsonStr2CrawingTask(JSONObject json) {
Map map = new HashMap();
map.put("type",CrawlingTaskType.get(json.getInteger("type")));
map.put("mobile", json.getString("mobile"));
List<CrawlingTask> list = baseDAO.find("from crt c where c.phoneNumber=:mobile and c.taskType=:type", map);
CrawlingTask crawlingTask = null;
// providType:(0:xx,1yy支付宝,2:zz淘宝,3:tt淘宝)
if (CollectionUtils.isNotEmpty(list)){
crawlingTask = list.get(0);
crawlingTask.setJsonStr(json.getString("data"));
}else{
//新增
crawlingTask = new CrawlingTask(UUID.randomUUID().toString(), json.getString("data"),
json.getString("mobile"), CrawlingTaskType.get(json.getInteger("type")));
crawlingTask.setNeedUpdate(true);
}
baseDAO.saveOrUpdate(crawlingTask);
}
@Override
protected void cache2Redis(JSONObject json) {
redisClientTemplate.set(json.getString("mobile") + "_" + json.getString("type"),CompressUtil.compress( json.getString("data")));
redisClientTemplate.expire(json.getString("mobile") + "_" + json.getString("type"), 2*24*60*60);
}
@Override
protected JSONObject doParseAndValidate(String json) throws AppException {
JSONObject object = JSON.parseObject(json);
if (StringUtils.isBlank(object.getString("type")) || StringUtils.isBlank(object.getString("mobile")) || StringUtils.isBlank(object.getString("data"))){
throw new AppException("MQ返回参数异常");
}
logger.info(object.getString("mobile")+"<<<<<<<<<获取来自MQ的授权数据>>>>>>>>>"+object.getString("type"));
return object;
}
}.doProcess(compress);
}
为什么用这种看上去有点复杂的设计来重构
- 重构方案1 拆分private小方法
- 通常情况下 我们对第一段代码的重构 会把它拆出4个private方法;
- 但是这样的问题仍然很明显——方法泛滥;试想一个Service中的每个public方法都拖家带口的跟着一群private方法,而且彼此都可见,结果就是大多数项目的现状——API泛滥,代码混乱 纠缠不清;
- 重构方案2 拆分或调用其它Service组件
- Spring的广泛使用 让程序员可以轻松愉快的面向接口编程 依赖注入,看起来似乎改善了架构;
- 但是这样做其实只是“伪解耦”,只不过把private转移到了另外一个组件的public
- 其实这并没有解耦(即便再加一层Interface、面向接口编程),A调B B调C C又调EFG的架构 并不比前一种更灵活 更解耦,反而破坏了内聚性(后面专门解释),更糟糕的是增加了一堆bean和interface导致了API泛滥成灾!
- 一吐槽起这个事我就停不下来,有太多人根本不思考 知其然不知其所以然,跟着当年spring的例子程序照葫芦画瓢 每个bean都搞一个接口一个实现,根本不去思考为什么要用接口,你有第二个实现吗?你有多重策略算法吗?spring当初是为了演示框架依赖注入和解耦的能力写的例子,Rod Johnson要是知道你们这样生搬硬套不知作何感想。总之abcde一堆bean互相调根本不是解耦,是完完全全的复杂僵化和错误的设计,或者根本不配称为设计,连包办一切的上帝类都不如,是一种自欺欺人 适得其反的荒唐行为。
- 接口、public方法绝不是越多越好,只有真的需要时 它们才是好的,否则就是滥用:
- 只有真的存在可复用性时,才应该增加一个public方法;否则应尽量用作用域更小的private和protected,避免API泛滥 减少其他人调用api时的选择和确认成本;
- 只有真的存在多个实现时(或通过RPC对外提供API时),才应该用接口;省掉接口类可以减少很多阅读代码和维护代码的成本;
- 著名的“奥卡姆剃刀原则”在这里仍然适用,而且非常重要——如无必要勿增实体!
- 如无必要,勿用接口和public,违背奥卡姆剃刀原则 滥用接口和public的现象在我们的行业中普遍存在,造成了巨大的不必要的浪费!
- 根据著名的Broken Window破窗原理,一些看似无关紧要的小的破损 杂乱,很快会蔓延开变成大量的破损和杂乱,最终变得不可收拾。所以前面说的这些问题不是夸大其词 吹毛求疵,很多项目真的就是这样一点点烂掉的;而大多数项目中 那种小的破损 杂乱其实随处可见——无用的接口、啰嗦的参数列表、错误的不合适的命名(方法 变量 类型 url。。。。)、不一致的代码格式和缩进、大段注释掉的代码、错误的注释、dead code、hard coding、大段大段重复的代码、被吃掉的exception;
- 你可能会说 这难道不是所有项目的常态吗 很多项目不也好好的活着呢吗? 首先 不是所有,其次 就算是大多数 也不意味着这是应该的,否则你就不要抱怨程序员加班多 没地位,不要抱怨前面程序员留下了烂摊子,因为你活该,这些待遇都是你应得的!
- 如果你发现你的团队成员工作不认真 没有质量意识,原因很可能出现在你们团队的工作环境和工作产物上,两者可以说互为因果——差的人制造差的工作环境和产品,反过来 差的工作环境和产品也令团队里的人变差——因为从他们加入这个团队那天起 他们看到的环境和产品的样子 就在暗示他们就在潜移默化的影响他们;
- 每一行烂代码都在不停的对他们说:“hi 新来的程序员,别紧张,这个项目没那么重要 可以随便搞 没人在乎质量 质量无所谓 别太当真,哈哈哈。。。”
- 重构方案3 使用TemplateMethod设计模式 完美解决了这些问题:
- 重构出的产物1:1个abstract内部类 包含1个public方法 4个abstract protected方法;
- 重构出的产物2:1个匿名内部类 重写4个protected方法;
- 所有方法和变量的作用域最小化,对组件中的其它代码不造成任何负担!
- 逻辑被拆分成了两个level——抽象的流程逻辑封装在抽象父类的final方法中,具体的步骤实现逻辑封装在具体子类的4个protected方法;
- 这正是高内聚的本质——一个level的逻辑应该被放置在一处!
- 这也是OCP开闭原则的本质——对扩展开放对修改关闭——子类可以方便的扩展父类给出符合需要的实现,同时 高阶逻辑算法被封装在父类的final方法中——无法被子类覆盖和修改!
- 同时,抽象内部类和匿名内部类的使用 避免了类的泛滥;
- 这里的关键问题就是方法和变量的作用域,作用域越小维护成本越低,反之亦然!
- Refactory重构的定义:
- 在不改变组件外部可查行为的前提下 改善代码的内部质量;
- 代码质量的一个最重要指标——可维护性(可读性+易修改性)
- TemplateMethod是一个非常有用的设计模式,恰当的使用即可得到好的OO设计——符合OCP开闭原则、高内聚低耦合、职责单一原则等面向对象原则;
- SpringFramework中大量的使用了这个设计,例如JdbcTemplate、TransactionTemplate、JedisTemplate、HibernateTemplate等;