用重构指导Clean Code(二):依恋情结和switch语句

书接上回,我们继续聊如何用重构指导Clean Code。

在Clean Code的3.4节中有这样一段代码(代码清单3-4)。(第3章主要讲的是函数,而3.4节讨论的是switch语句。)

public Money calculatePay(Employee e) throws InvalidEmployeeType {switch (e.type) {case COMMISSIONED:return calculateCommissionedPay(e);case HOURLY:return calculateHourlyPay(e);case SALARIED:return calculateSalariedPay(e);default:throw new InvalidEmployeeType(e.type);}
}

Bob大叔细数了这段代码的几处问题:

  1. 太长,当出现新的Employee类型时,还会变得更长

  2. 明显做了不止一件事

  3. 违反了单一权责原则,因为有好几个修改它的理由

  4. 违反了开放闭合原则,因为每当添加新类型时,就必须修改之

  5. 最麻烦的是到处皆有类似结构的函数

其中1和4说的是一回事儿,每当新增加一种类型的Employee,就必须在这个switch里添加新的case。2和3说的是一回事儿,这个方法为三种类型的Employee计算费用,任何一种类型的计算逻辑发生变化,都要修改这个计算方法(或者对应的计算方法)。

对于第5点,Bob大叔的意思是说,calculatePay(Employee e)这个方法所在的类中还会有类似isPayDay(Employee e, Date date)deliverPay(Employee e, Money pay)这样的方法,由于Employee具有不同的类型(type),那么这些方法中肯定也会出现类似的switch语句。

讲到这里还是非常清晰的,然而Bob大叔接下来又开始魔幻操作,直接给出了解决方案:

将switch语句埋到抽象工厂底下,不让任何人看到。该工厂使用switch语句为Employee的派生物创建适当的实体,而不同的函数,如calculatePayisPaydaydeliverPay等,则藉由Employee接口多态地接受派遣。

哪里来的抽象工厂?Employee怎么就有了派生类?再一看代码

public abstract class Employee {public abstract boolean isPayday();public abstract Money calculatePay();public abstract void deliverPay(Money pay);
}
-----------------
public interface EmployeeFactory {
public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType;
}
-----------------
public class EmployeeFactoryImpl implements EmployeeFactory {public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType {switch (r.type) {case COMMISSIONED:return new CommissionedEmployee(r) ;case HOURLY:return new HourlyEmployee(r);case SALARIED:return new SalariedEmploye(r);default:throw new InvalidEmployeeType(r.type);}}
}

妙啊,6啊,Bob大叔简直是神啊!

然而,面对同样的问题代码,我们能做出这样的修改吗?我们能构造出Employee的继承体系吗?能抽象出EmployeeFactory吗?反正对我来说,仅看Clean Code是做不到的。Bob大叔的技炫得神乎其神,但无奈只有结果,没有过程,让初学者不明所以。所以,这个时候就要祭出神器《重构》了。

《重构》要求我们在重构代码之前,先识别坏味道。然后针对你想去消除的那个坏味道,按照相应的重构手法,一步一步让这个坏味道消失。我曾经在真实的遗留系统中演练过“坏味道驱动的重构“(SDR,Smell Driven Refactoring,这是我发明的一个词儿),效果还是不错的。

还是上面有问题的代码,我们看看按照《重构》应该怎么重构:

public Money calculatePay(Employee e) throws InvalidEmployeeType {switch (e.type) {case COMMISSIONED:return calculateCommissionedPay(e);case HOURLY:return calculateHourlyPay(e);case SALARIED:return calculateSalariedPay(e);default:throw new InvalidEmployeeType(e.type);}
}

这里的坏味道很多,最一目了然的可能就是”Switch Statements(Switch惊悚现身)“,当然还有Bob大叔指出的违反SRP和OCP,但在坏味道中,我们管它叫做”Divergent Change(发散式变化)“。然而要消除这两个坏味道都不是那么容易,我只想从最简单的开始。什么是最简单的呢?

首先calculatePay方法有一个Employee类型的参数,除此之外,它没有使用该方法所在类的任何成员。同理可以推断该方法所使用的其他三个方法应该也只是使用Employee,不会使用该方法所在类的成员。这时,”Feature Envy(依恋情结)“的坏味道就显现出来了。

有一种经典气味是:函数对某个类的兴趣高过对自己所处类的兴趣。这种孺慕之情最通常的焦点便是数据。无数次经验里,我们看到某个函数为了计算某个值,从另一个对象那儿调用几乎半打的取值函数。

对于该方法,岂止是半打,简直就是全部。老马不但指出了问题,还给出了方案:

疗法显而易见:把这个函数移至另一个地点。你应该使用"Move Method(搬移函数)把它移到它该去的地方”

没什么好说的,挪呗。将calculateCommisstionPaycalculateHourlyPaycalculateSalariedPay方法,以及它自己全部挪到Employee中(常用IDE都支持重构,这里不再赘述):

public Money calculatePay() throws InvalidEmployeeType {switch (this.type) {case COMMISSIONED:return this.calculateCommissionedPay();case HOURLY:return this.calculateHourlyPay();case SALARIED:return this.calculateSalariedPay();default:throw new InvalidEmployeeType(this.type);}
}

样一来,该方法只使用Employee自己的数据,”依恋情结“的坏味道不见了。

是祸躲不过,Switch这个坏味道早晚得面对。

从本质上说,switch语句的问题在于重复。你常会发现同样的switch语句散布于不同地点。如果要为它添加一个新的case子句,就必须找到所有switch语句并修改它们。

这也正是Bob大叔所说的”最麻烦的是到处皆有类似结构的函数“。好在老马一如既往给出了解决方案:

面向对象中的多态概念可为此带来优雅的解决办法。

switch语句常常根据类型码进行选择,你要的是“与该类型码相关的函数或类”,……使用Replace Type Code with Subclasses 或Replace Type Code with State/Strategy。一旦这样完成继承结构之后,你就可以运用Replace Conditional with Polymorphism了。

由于我们已经通过消除”依恋情结“将方法移动到了Employee中,所以接下来只需要从类型码入手就好了。这里我们选择使用Replace Type Code with Subclass(以子类取代类型码)这个重构手法。打开《重构》相关章节一看,给出的例子简直跟我们这里的一模一样,两位大师是商量好的吧?

abstract int getType();
abstract Money calculatePay();static Employee create(int type) {switch (type) {case COMMISSIONED:return new CommissionedEmployee();case HOURLY:return new HourlyEmployee();case SALARIED:return new SalariedEmployee();default:throw new InvalidEmployeeType(type);}
}

怎么样?是不是与Bob大叔的幻改异曲同工?(EmployeeRecord之类的属于细枝末节,不再赘述。)

除此之外,老马还给出了消除类型码的其他方案(Replace Type Code with State/Strategy),以及彻底消除switch的办法(Replace Conditional with Polymorphism)。

这样细致入微的教导,是不是比直接告诉你答案要实用得多?

事实上对于上例这种有很多坏味道的代码,你完全可以按个人喜好来选择不同的重构顺序,得到的结果可能千差万别,也可能殊途同归,但肯定都算是不错的设计。

这就是代码的魅力。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/306393.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

算法题目——杨辉三角问题

思路: #include<iostream> #include<cstdio> #include<cstring> #

cascade down_Cascaded CNN 方法寻找人脸关键点

Cascaded CNN 方法寻找人脸关键点论文笔记阅读论文第一阶段阅读论文&#xff0c;大约两天大体阅读完论文 Deep Convolutional Network Cascade for Facial Point Detection。感觉还是比较缺乏论文阅读经验&#xff0c;但是比以前快了很多。主要阅读论文 intro、method 和 model…

了解一下HTTP1.1 Pipelining技术

为什么谈HTTP1.1 Pipelining呢&#xff1f;主要问题根源还是来源于Beetlex参加了techempower的测试。先看一下以下两项测试的结果&#xff1a;以上分别是.net平台的Json和Plaintext的测试结果&#xff0c;其实Plaintext最高能跑700多万RPS已经完全超了对网络IO读写损耗的认知&a…

springboot2 多线程写入数据_解决SpringBoot项目使用多线程处理任务时无法通过@Autowired注入bean问题...

{"moduleinfo":{"card_count":[{"count_phone":1,"count":1}],"search_count":[{"count_phone":7,"count":7}]},"card":[{"des":"EDAS 是一个应用托管和微服务管理的PaaS平台…

算法题目——子序列和问题(poj-3061)(尺取法)

题目链接:POJ-3061 题意:给定一个序列,使得其和大于或等于S,求最短的子序列长度。 问题分析: 1.首先序列都是正整数,当子序列和大于等于S时,已经没有必要再将右端点继续向右移动。因为再向右移动,序列的长度一定会大于此时的长度 2.所以,当子序列和小于S时,右端点向…

读书 | 数字化转型的道与术(下)

【数字化转型】| 作者 / Edison Zhou这是EdisonTalk的第313篇学习总结 最近在阅读钟华老师的新作《数字化转型的道与术》&#xff0c;记录和总结了一些学习笔记和感想&#xff0c;整理成文分享与你&#xff0c;本文为下半部分&#xff0c;希望能对也在参与数字化转型的各位童鞋…

算法题目——读书知识点统计问题(POJ-3320)(尺取法)

题目链接:poj-3320 问题:杰西卡是一个非常可爱的女孩,受到许多男孩的追捧。最近她有个问题。期末考试快到了,但她几乎没花什么时间。如果她想通过考试,她必须掌握一本厚厚的教科书中包含的所有思想。那本教科书的作者和其他作者一样,对这些观点极为挑剔,因此有些观点被…

aop springboot 传入参数_Springboot添加AOP打印请求参数

1. 引入依赖org.springframework.bootspring-boot-starter-aop2. 写切面切面类需要加Aspect和Component注解package com.test.demo.aspect;import java.util.Map;import javax.servlet.http.HttpServletRequest;import org.aspectj.lang.JoinPoint;import org.aspectj.lang.Pro…

GraphQL:面对复杂类型

GraphQL 既是一种用于 API 的查询语言也是一个满足你数据查询的运行时。GraphQL 对你的 API 中的数据提供了一套易于理解的完整描述&#xff0c;使得客户端能够准确地获得它需要的数据&#xff0c;而且没有任何冗余&#xff0c;也让 API 更容易地随着时间推移而演进&#xff0c…

算法与数据结构——并查集

文章推荐:【算法与数据结构】—— 并查集 例子: 数据结构——最小生成树之克鲁斯卡尔算法(Kruskal) 1.2 并查集思想(重点) 我们可以把每个连通分量看成一个集合,该集合包含了连通分量的所有点。而具体的连通方式无关紧要,好比集合中的元素没有先后顺序之分,只有“属于”…

eclipse 输入卡顿_7个小技巧,解决eclipse卡顿问题

eclipse作为开发工具&#xff0c;每天都要使用&#xff0c;你肯定遇到过eclipse卡到想哭的时刻&#xff0c;严重影响开发效率啊&#xff01;如果内存条不要钱&#xff0c;那就加内存吧&#xff01;一个不够加两个&#xff01;当然这都是玩笑话&#xff0c;如果不花钱也能解决问…

为什么建议学生积极参与开源项目?

喜欢就关注我们吧&#xff01;“ 你参与过开源项目吗&#xff1f;” 随着开源技术对信息产业的积极影响进一步扩大&#xff0c;越来越多的面试官将是否参与开源项目作为考核开发者能力的一项基本指标。对于求职者来说&#xff0c;参与开源能为简历增色不少 &#xff0c;增加赢得…

mysql 左连接 怎么走索引_数据库索引、左连接、右连接、等值连接

在MySQL中&#xff0c;主要有四种类型的索引&#xff0c;分别为&#xff1a;B-Tree索引&#xff0c;Hash索引&#xff0c;Fulltext索引(MyISAM 表)和R-Tree索引&#xff0c;本文讲的是B-Tree索引。一、Mysql索引主要有两种结构&#xff1a;BTree索引和Hash索引(a) Innodb存储引…

微服务技术栈及分享计划

前言上一篇对微服的演变、优缺点进行了概述&#xff0c;对于业务复杂项目&#xff0c;微服务算是比较合适的解决方案&#xff1b;对于咱们开发者来说&#xff0c;有好的解决方案肯定要跟进学习&#xff0c;但不能盲目追崇流行技术&#xff0c;目的还是为了解决问题。这里就把As…

PAT乙级题目——1002写出这个数

问题分析&#xff1a;1.数据比较大&#xff0c;并且输入数据时没有停顿&#xff0c;所有使用字符串来存储数据 string str""; cin>>str;2.使用字符串数组来存储每个数字的拼音 string S[10]{"ling","yi","er","san"…

northstar机器人编程_《机器人构建实战》——导读

前言机器人构建实战机器人是21世纪发展最为迅速、应用前景最为广阔的科学技术领域之一。机器人技术综合运用了基础科学和应用工程技术的最新成果&#xff0c;是一个国家科技发展水平和国民经济现代化、信息化的重要标志&#xff0c;是世界强国重点发展的高技术之一。近年来&…

Linux性能挖潜的隐藏招数:内核CPU亲和性参数调整

作者&#xff1a;李彬&#xff0c;赵雪枫&#xff0c;金融科技工程师&#xff0c;架构师社区特邀作者&#xff01;应用服务性能调优&#xff0c;是每个系统投产前都需要关注的问题&#xff0c;系统及软件层面的调优方法均有大量文章介绍&#xff0c;但在所有招数使出后&#xf…

PAT乙级——1001害死人不偿命的(3n+1)猜想(太简单)

#include<iostream> using namespace std;int main(){int n;int ans0;//记录走过的步数 cin>>n;while(n!1){if(n%20){ans;nn/2;}else{ans;n(3*n1)/2;}}cout<<ans;return 0; }

c4d如何把文字贴在物体表面_C4D在简模上整体和局部贴贴图的方法

引&#xff1a;可能常常碰到一个问题&#xff0c;有了一个模型一个简单的模型&#xff0c;也有了基本材质&#xff0c;想在这个材质上面加点文字内容&#xff0c;比方说一个盒子外面的简单的标签&#xff0c;或者其它&#xff0c;当然你可以用C4D模块中的3D Paint画&#xff0c…

好的重构方法才能摆脱“屎山”

大家好&#xff0c;我是Z哥。最近在整理一些项目&#xff0c;所以相关的文章写的多了些。之前的相关文章有《聊聊单元测试》&#xff0c;感兴趣的话可以点击文末链接去阅读。这次整理项目的时候&#xff0c;做了比较多的codereview和重构。好久没做这么高强度了重构了&#xff…