一次Java烂代码重构之旅——Template Method设计模式的经典实践

我们项目中一段典型的代码,问题明显

  • 违反职责单一原则(一个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等;

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
/* * 原始需求背景: * 网宿CDN要按月收取客户的服务费用,根据流量的大小、 * 服务的类型等,收取不同的费用,收费规则如下: * web应用:1000元/M * 流媒体应用:1000元/M*0.7 * 下载应用:1000元/M*0.5 * 月末打印报表时,要罗列每个用户每个频道的费用、客户总费用, * 还要打印该客户的重要性指数,重要性指数=网页流/100+下载流量/600; * * 需求变更场景: * 系统已经开发出来了,接下来,运维部门现在希望对系统做一点修改, * 首先,他们希望能够输出xml,这样可以被其它系统读取和处理,但是, * 这段代码根本不可能在输出xml的代码中复用report()的任何行为,唯一 * 可以做的就是重写一个xmlReport(),大量重复report()中的行为,当然, * 现在这个修改还不费劲,拷贝一份report()直接修改就是了。 * 不久,成本中心又要求修改计费规则,于是我们必须同时修改xmlReport() * 和report(),并确保其一致性,当后续还要修改的时候,复制-黏贴的问题就 * 浮现出来了,这造成了潜在的威胁。 * 再后来,客服部门希望修改服务类型和用户重要性指数的计算规则, * 但还没决定怎么改,他们设想了几种方案,这些方案会影响用户的计费规则, * 程序必须再次同时修改xmlReport()和report(),随着各种规则变得越来越复杂, * 适当的修改点越 来越难找,不犯错误的机会越来越少。 * 现在,我们运用所学的OO原则和方法开始进行改写吧。 */

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值