重构第一章java语言解读

开篇

📚《重构》在线观看地址戳这里
🔔注意:强烈建议懂javascript的看书,java的可以结合着看看这篇文章。我写Blog也是记录整理自己的思路,有问题欢迎在评论区讨论,谢谢💝。

⚡️示例分析

  🎄场景

  设想有一个戏剧演出团,演员们经常要去各种场合表演戏剧。通常客户(customer)会指定几出剧目,而剧团则根据观众(audience)人数及剧目类型来向客户收费。该团目前出演两种戏剧:悲剧(tragedy)和喜剧(comedy)给客户发出账单时,剧团还会根据到场观众的数量给出“观众量积分”。

  🚬分析构建程序基本框架

  根据上面的信息提取出类的结构和关系,这是第一步请务必搞懂。不过不用担心结合我贴心的图解分析,一定要结合图解和分析看懂哦。
  🌊分析一下:其实上面那段描述,要我们做的事就是给订了剧目的“顾客”发“账单”而已,只不过在发账单的时候还会送点“积分”。好了知道要干什么了,那是怎么干的呐?上面说收费是由观众人数剧目类型决定的,积分是由观众人数决定的。
场景示例:顾客张三订了西游记,三国演义,红楼梦三个剧目,他不能只订剧不买座位吧,他订的剧目有多少观众他就要向剧院要多少座位,剧院再根据这个观众人数(剧院要提供的座位)和这个剧目的类型向这个顾客收钱。再根据这个观众人数给顾客送点“积分”
提取出类信息

  • 剧目类(Play)应该包含剧目的名字name,和类型type。用一个Map<String, Play> 可以把剧目的数据组织起来。
  • 订单(Invoice)应该包含顾客,剧目,观众人数。怎么组织?每个剧目都会有不同的观众人数,提取出剧目id观众人数组织为表演类(Performance)。这也是前面的剧目(Play)数据以Map组装的原因—方便根据剧目id取到剧目。订单类(Invoice)就是由顾客(String)和表演(Performance)组成。上面是一个顾客下单的场景,有多个顾客下单的话就用List<Invoice>组装数据。
    对象结构和关系图
      上面三个实体类的java代码
    Play类代码
    Performance类代码
    Invoice类代码
      组装数据和核心逻辑—根据观众人数和剧目类型向顾客收费,根据观众人数给积分
    组装数据和核心逻辑
      ❌看完上面的代码,我们的核心逻辑在statement方法中,这个方法快40行代码。不花点时间是真的看不明白,如果你看明白了要给加个需求呢!比如说这是用纯文本打印的,如果要打印一个html版本呢?几乎没法复用需要重写一个函数,大家注意这个需求后面会有实现。。如果代码中很多这种长长的方法,我想你不会觉得写这个代码的人很牛,让你看或者修改这种代码准备提跑路咯,写出机器能看懂的代码很简单,但是能写出人能看懂的代码才是真的厉害。
      🔦初始代码的链接戳这里国内网速不好的戳这里

  🚿提炼函数

  ✏️对于下图这种长长的方法我们一般可以运用提炼函数的重构手法使逻辑更清晰。提炼函数就是将关注点不同的代码块抽取成一个独立的函数,按它所干的事情给它命名。(关注点可以理解为方法中的一段相对独立的逻辑代码块)
在这里插入图片描述
  ✏️第一个引起我们注意的是中间的那段switch段,我们就想着把它抽取出来。如果我将这块代码提炼到自己的一个函数里,这个函数需要用到哪些变量?type,thisAmount,perf,这三个变量中type和perf是没有做修改操作的,只是代码逻辑中要获取他们的值,可以直接通过形参传过去即可。thisAmount做了修改操作,且后续的代码中也需要用到这个变量,后面要用怎么办?函数返回值返回它不就行了嘛!可以将thisAmount的初始化也放到提炼后的函数中。
  接下来我们做一下提炼函数的重构,顺便提一下在java的IDE中可以选中代码块按alt+ctrl+m直接提炼函数,按它所干的事情给它命名。
提炼函数
  💡提炼后变成虽然总的代码量有些增加,但是statement函数的长度缩短了将近一倍只有21行了。提炼后的函数你也可以取一个更明确的函数名getAmountByTypeAndAudience这里我就取的getAmount这样短一些,上面的更明确一些就可以做到见名知意。这样在看statement函数时看到函数名getAmountByTypeAndAudience可以得知意图,看代码时会跟容易理解。
  完成提炼函数手法后,我会看看提炼出来的函数,看是否能进一步提升其表达能力。一般我做的第一件事就是给一些变量改名,使它们更简洁,比如将 thisAmount 重命名为 result。可以永远将函数的返回值命名为“result”,这样我一眼就能知道它的作用。
修改变量名

  🔍 查询取代临时变量

  当我分解一个长函数时,我喜欢将 play 这样的变量移除掉,因为它们创建了很多具有局部作用域的临时变量,这会使提炼函数更加复杂。这里我要使用的重构手法是以查询取代临时变量。
以查询取代临时变量
  这个重构手法应该是比较常见的,也比较简单,平时我们使用到了可能也没有这么察觉。这可以减少不必要的临时变量,提炼函数时也会更方便(因为提炼函数需要考虑使用到的变量,越少肯定更方便)。顺带提一下,有人可能会有性能上的疑惑,没有重构前获取type只需要调用一次函数,而现在重构了要调用多次函数,是否会在性能上有损耗。学过编译原理的应该知道这里的性能损耗几乎是可以不计的。学编译原理去😱

  上面的代码去除注释后如下图,忽略记录程序结果的result会更简洁,这里的result为了后面打印结果所以多处拼接,所以显的比较复杂。
内联变量0

  🎉简化形参

  🔎再观察getAmountByTypeAndAudience()的第二个形参,没错还是那个段函数调用获取到type的形参。观察一下可以发现只需要perf就可以得到这个形参,然而第一个形参就是把perf传过来了,这样函数内部就可以获取到type,第二个形参自然就可以省去了,上代码直观明了。
简化形参

  🌀内联变量

  📋函数形参处理完了,处理完 getAmountByTypeAndAudience的参数后,我回过头来看一下它的调用点在statement中调用后。它被赋值给一个临时变量thisAmount,之后就不再被修改,因此我又采用内联变量手法内联它。thisAmount只在累加totalAmount时使用过(忽略result这个展示程序运行结果的变量),上代码。
内联变量
  通过类型和观众人数收费的函数getAmountByTypeAndAudience,相关的重构基本完成。用到了提炼函数,查询取代临时变量,简化形参,内联变量等重构手法。现在我们把目光聚焦到根据观众人数获取积分这里虽然是根据观众人数获取积分,但是喜剧类型可以加分。和最初的场景有些出入(刚开始场景里的描述时根据观众人数,书的锅!饶命😨)但是不影响我们重构。

  💧提炼函数-积分

  🔆现在我们对,根据观众人数和剧目类型获取积分这个段逻辑进行提炼函数。对下图中的6到10行代码逻辑提炼函数。代码如下图所示。
提炼积分函数
  消除代码中的注释和更改getVolumeCreditsByAudienceAndType函数中的volumeCredits变量为result,因为这个变量是函数的返回值,我习惯命名为result。现在statement的代码会变得更加清爽。
请添加图片描述
  对根据观众人数和剧目类型获取积分这个段逻辑进行提炼函数,和前面的根据观众人数和剧目类型来收费的逻辑代码块的提炼相比较是否简单很多😉,这得益于前期的用查询取代临时变量,内联变量等手法消除了大部分的临时变量。

  🐾拆分循环

  👉下一个重构目标是 volumeCredits。处理这个变量更加微妙,因为它是在循环的迭代过程中累加得到的。第一步,就是应用拆分循环将 volumeCredits 的累加过程分离出来。
请添加图片描述
  分离出来后是这样的。
请添加图片描述
  拆分循环后可以用提炼函数来消除临时变量volumeCredits了。提炼函数后把原来用到volumeCredits的位置都替换为提炼的函数getTotalVolumeCredits(内联变量)。getTotalVolumeCredits中的返回值变量我还是习惯的命名为result。操作完的代码如下图。
请添加图片描述
  👌总结
  使用拆分循环,分离出累加过程;
  使用提炼函数,提炼出计算总数的函数;
  使用内联变量,完全移除中间变量。

  我想你也看到了和vulumeCredits变量类似的totalAmount变量了👍,我们可以用上面几步相同的重构手法进行重构。这样就可以消除临时变量totalAmount了。
请添加图片描述
  现在的代码结构是这样的
计算部分重构初步
  现在代码结构已经好多了。顶层的 statement 函数现在只剩不到10行代码,而且它处理的都是与打印详单相关的逻辑。与计算相关的逻辑从主函数中被移走,改由一组函数来支持。每个单独的计算过程和详单的整体结构,都因此变得更易理解了。

  🙌拆分阶段

  🐳现在计算金额和积分的逻辑重构的差不多了。现在,我可以更多关注我要修改的功能部分了也就是前文提到的,为这张详单提供一个打印HTML 版本。不管怎么说,现在改起来更加简单了。因为计算代码已经被分离出来,我只需要为顶部的几行代码实现一个 HTML 的版本。问题是,这些分解出来的函数嵌套在打印文本详单的函数中。无论嵌套函数组织得多么良好,我总不想将它们全复制粘贴到另一个新函数中。我希望同样的计算函数可以被文本版详单和 HTML 版详单共用。

  🔅这里我的目标是将逻辑分成两部分:一部分计算详单(不管是文本详单还是HTML详单都是打印数据的)所需的数据,另一部分将数据打印成文本或 HTML。第一阶段会创建一个中转数据结构(第二阶段需要打印的数据),再把它传递给第二阶段。
  分析:首先我们看一下第二阶段打印需要的数据有哪些。请看图。
拆分阶段分析1
  看上图可知打印的这部分需要用到,总金额,总积分,用户,List<Performance>四个。因此可以构建出一个中间数据结构R.代码如下图。请添加图片描述
  现在我们可以封装第一阶段了。第一阶段就是封装计算结果,以便后面传给第二段。
第一阶段
  第一阶段提供数据第二阶段就需要用形参接收。然后数据从data中获取就行了。
第二阶段

  有人可能会有疑惑这有什么用呢!看下这张图你肯定就明白了。对的就是让计算阶段通过一个中间结构可以做到重用。
main
  🍬小结:现在第一个需求打印html格式的详单,在拆分阶段重构手法的辅助下轻松完成了。重构后代码行数相比于重构前的行数确实增加了,这主要是将代码抽取到函数里带来的额外包装成本。虽然代码的行数增加了,但重构也带来了代码可读性的提高。额外的包装将混杂的逻辑分解成可辨别的部分,分离了详单的计算逻辑与样式。这种模块化使我更容易辨别代码的不同部分,了解它们的协作关系。虽说言以简为贵,可演化的软件却以明确为贵。通过增强代码的模块化,我可以轻易地添加 HTML 版本的代码,而无须重复计算部分的逻辑。

  🍃多态消除分支逻辑

  💐接下来我将注意力集中到下一个特性改动:支持更多类型的戏剧,以及支持它们各自的价格计算和观众量积分计算。对于现在的结构,我只需要在计算函数里添加分支逻辑即可。计算金额和积分的两个函数都体现了,戏剧类型在计算分支的选择上起着关键的作用——但这样的分支逻辑很容易随代码堆积而腐坏。
多态替换if-else
  计算逻辑随着剧目类型的不同表现出不同的行为,可以用多态的方式重构这段逻辑。抽象出如下的类结构。就是定义标准接口,抽象父类中实现通用方法(比如calCredits可以被定义通用逻辑两个类都可以复用),这样子类可以复用抽象父类中的方法,子类和要和父类表现不同的逻辑就重写父类的方法即可(比如calAmount不同类型的行为不同需要各自重写方法)。
多态重构1
  代码如下
抽象类结构
  提供一个根据剧目类型码获取类型的工厂。这里List<ICalculate> calculateList会注入上面编写的这个实现类。上面两个实现类的Order是注入顺序。用Map<String, ICalculate>存储,这样外部获取的时候很方便,传入一个类型名代码即可。我这里的类型代码都是硬编码的,你可以把这些常量统一抽取到一个常量类中。我后面也会这么做。
计算工厂
  重构后的效果,如下图所示。
多态替换重构
  🔔新结构带来的好处是,不同戏剧种类的计算各自集中到了一处地方。如果大多数修改都涉及特定类型的计算,像这样按类型进行分离就很有意义。当添加新剧种时,只需要添加一个子类,注入到工厂中就行了。
  现在要增加一个历史类型的剧目的需求就非常简单了,创建历史计算类后工厂会自动注入,别忘了增加历史类的数据哦。代码如下所示。
新增历史数据
  🔆总结:本章的重构有 3 节点,分别是:
     1. 将原函数分解成一组嵌套的函数—》主要用提炼函数的重构手法
     2. 应用拆分阶段分离计算逻辑与输出格式化逻辑—》主要用拆分阶段的重构手法
     3. 以及为计算器引入多态性来处理计算逻辑—》主要用的多态和工厂重构手法

  一般来说,重构早期的主要动力是尝试理解代码如何工作。通常你需要先通读代码,找到一些感觉,然后再通过重构将这些感觉从脑海里搬回到代码中。清晰的代码更容易理解,使你能够发现更深层次的设计问题,从而形成积极正向的反馈环。当然,这个示例仍有值得改进的地方,但是和最初相比我已经很满足了。一般优化代码结构的流程如下。
优化流程
  注意:代码中很多用的是静态方法是因为跑程序方便,后面在最终代码中都去掉了。
  最后重构好的代码链接附上戳这里国内网速不好的戳这里

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值