当前位置: 首页 > article >正文

《重构-》

一、代码坏的味道

  • 神秘命名
    • ​​​​​代码应该直观明了。要深思熟虑如何给函数、模块、变量和类命名,使它们能清晰地表明
      自己的功能和用法。
  • 重复代码
    • 一旦有重复代码存在,阅读这些重复的代码时你就必须加倍仔细,留意其间细微的差异。如果要修改重复代码,你必须找出所有的副本来修改。
    • 最单纯的重复代码就是“同一个类的两个函数含有相同的表达式”。这时候你需要做的就是采用提炼函数提炼出重复的代码,然后让这两个地点都调用被提炼出来的那一段代码。如果重复代码只是相似而不是完全相同,请首先尝试用移动语句重组代码顺序,把相似的部分放在一起以便提炼。如果重复的代码段位于同一个超类的不同子类中,可以使用函数上移来避免在两个子类之间互相调用。
  • 过长函数
    • 小函数具有更好的阐释力、更易于分享、更多的选择
    • 每当感觉需要以注释来说明点什么的时候,我们就把需要说明的东西写进一个独立函数中,并以其用途(而非实现手法)命名。关键不在于函数的长度,而在于函数 做什么 如何做 ”之间的语义距离。
    • 如果函数内有大量的参数和临时变量,它们会对你的函数提炼形成阻碍。如果你尝试运用提炼函数 ,最终就会把许多参数传递给被提炼出来的新函数,导致可读性几乎没有任何提升。此时,你可以经常运用以查询取代临时变量来消除这些临时元素。引入参数对象 保持对象完整则可以将过长的参数列表变得更简洁一些。
    • ​​​​​​如果你已经这么做了,仍然有太多临时变量和参数,那就应该以命令取代函数
    • 如何确定该提炼哪一段代码?一个很好的技巧是:寻找注释。它们通常能指出代码用途和实现手法之间的语义距离。如果代码前方有一行注释,就是在提醒你:可以将这段代码替换成一个函数,而且可以在注释的基础上给这个函数命名。就算只有一行代码,如果它需要以注释来说明,那也值得将它提炼到独立函数中去。条件表达式和循环常常也是提炼的信号。你可以使用分解条件表达式处理条件表达式。对于庞大的 switch 语句,其中的每个分支都应该通过提炼函数变成独立的函数调用。如果有多个 switch 语句基于同一个条件进行分支选择,就应该使用以多态取代条件表达式。至于循环,你应该将循环和循环内的代码提炼到一个独立的函数中。如果你发现提炼出的循环很难命名,可能是因为其中做了几件不同的事。如果是这种情况,请勇敢地使用拆分循环 将其拆分成各自独立的任务。
  • 过长参数列表
    • ​​​​​​如果可以向某个参数发起查询而获得另一个参数的值,那么就可以使用以查询取代参数去掉这第二个参数。如果你发现自己正在从现有的数据结构中抽出很多数据项,就可以考虑使用保持对象完整手法,直接传入原来的数据结构。如果有几项参数总是同时出现,可以用引入参数对象将其合并成一个对象。如果某个参数被用作区分函数行为的标记,可以使用移除标记参数。使用类可以有效地缩短参数列表。如果多个函数有同样的几个参数,引入一个类就尤为有意义。你可以使用函数组合成类,将这些共同的参数变成这个类的字段。
  • 全局数据
    • ​​​​​​全局数据的问题在于,从代码库的任何一个角落都可以修改它,而且没有任何机制可以探测出到底哪段代码做出了修改。一次又一次,全局数据造成了那些诡异的 bug,而问题的根源却在遥远的别处,想要找到出错的代码难于登天。全局数据最显而易见的形式就是全局变量,但类变量和单例(singleton)也有这样的问题。首要的防御手段是封装变量,每当我们看到可能被各处的代码污染的数据,这总是我们应对的第一招。你把全局数据用一个函数包装起来,至少你就能看见修改它的地方,并开始控制对它的访问。随后,最好将这个函数(及其封装的数据)搬移到一个类或模块中,只允许模块内的代码使用它,从而尽量控制其作用域
  • 可变数据
    • ​​​​​封装变量来确保所有数据更新操作都通过很少几个函数来进行,使其更容易监控和演进。如果一个变量在不同时候被用于存储不同的东西,可以使用拆分变量将其拆分为各自不同用途的变量,从而避免危险的更新操作。使用移动语句提炼函数尽量把逻辑从处理更新操作的代码中搬移出来,将没有副作用的代码与执行数据更新操作的代码分开。设计 API 时,可以使用将查询函数和修改函数分离确保调用者不会调到有副作用的代码,除非他们真的需要更新数据。我们还乐于尽早使用移除设值函数——有时只是把设值函数的使用者找出来看看,就能帮我们发现缩小变量作用域的机会。如果可变数据的值能在其他地方计算出来,这就是一个特别刺鼻的坏味道。它不仅会造成困扰、bug 和加班,而且毫无必要。消除这种坏味道的办法很简单,使用以查询取代派生变量即可。如果变量作用域只有几行代码,即使其中的数据可变,也不是什么大问题;但随着变量作用域的扩展,风险也随之增大。可以用函数组合成类或者函数组合成变换来限制需要对变量进行修改的代码量。如果一个变量在其内部结构中包含了数据,通常最好不要直接修改其中的数据,而是用将引用对象改为值对象令其直接替换整个数据结构。
  • 发散式变化
  • 霰弹式修改
    • 如果每遇到某种变化,你都必须在许多不同的类内做出许多小修改
    • 这种情况下,你应该使用搬移函数 搬移字段 把所有需要修改的代 码放进同一个模块里。如果有很多函数都在操作相似的数据,可以使用函数组合成 。如果有些函数的功能是转化或者充实数据结构,可以使用函数组合成 变换。如果一些函数的输出可以组合后提供给一段专门使用这些计算结果的逻辑,这种时候常常用得上拆分阶段。面对霰弹式修改,一个常用的策略就是使用与内联( inline )相关的重构 ——如内联函数或是内联类——把本不该分散的逻辑拽回一处。完成内联之后,你可能会闻到过长函数或者过大的类的味道,不过你总可以用与提炼相关的重构手法将其拆解成更合理的小块。即便如此钟爱小型的函数和类,我们也并不担心在重构的过程中暂时创建一些较大的程序单元
  • 依恋情结
  • 数据泥团
    • 数据项就像小孩子,喜欢成群结队地待在一块儿。你常常可以在很多地方看到相同的三四项数据:两个类中相同的字段、许多函数签名中相同的参数。这些总是绑在一起出现的数据真应该拥有属于它们自己的对象。首先请找出这些数据以字段形式出现的地方,运用提炼类将它们提炼到一个独立对象中。然后将注意力转移到函数签名上,运用引入参数对象保持对象完整为它瘦身。这么做的直接好处是可以将很多参数列表缩短,简化函数调用。是的,不必在意数据泥团只用上新对象的一部分字段,只要以新对象取代两个(或更多)字段,就值得这么做。 一个好的评判办法是:删掉众多数据中的一项。如果这么做,其他数据有没有因而失去意义?如果它们不再有意义,这就是一个明确信号:你应该为它们产生一个新对象。 我们在这里提倡新建一个类,而不是简单的记录结构,因为一旦拥有新的类,你就有机会让程序散发出一种芳香。得到新的类以后,你就可以着手寻找依恋情结这可以帮你指出能够移至新类中的种种行为。这是一种强大的动力:有用的类被创建出来,大量的重复被消除,后续开发得以加速,原来的数据泥团终于在它们的小社会中充分发挥价值
  • 基本类型偏执
    • 大多数编程环境都大量使用基本类型,即整数、浮点数和字符串等。一些库会引入一些小对象,如日期。但我们发现一个很有趣的现象:很多程序员不愿意创建对自己的问题域有用的基本类型,如钱、坐标、范围等。于是,我们看到了把钱当作普通数字来计算的情况、计算物理量时无视单位(如把英寸与毫米相加)的情况以及大量类似 if (a < upper && a > lower)这样的代码。字符串是这种坏味道的最佳培养皿,比如,电话号码不只是一串字符。一个体面的类型,至少能包含一致的显示逻辑,在用户界面上需要显示时可以使用。用字符串来代表类似这样的数据是如此常见的臭味,以至于人们给这类变量专门起了一个名字,叫它们类字符串类型stringly typed)变量。你可以运用以对象取代基本类型将原本单独存在的数据值替换为对象,从而走出传统的洞窟,进入炙手可热的对象世界。如果想要替换的数据值是控制条件行为的类型码,则可以运用以子类取代类型码加上以多态取代条件表达式的组合将它换掉。如果你有一组总是同时出现的基本类型数据,这就是数据泥团的征兆,应该运用炼类引入参数对象来处理。
  • 重复的switch
    • 在不同的地方反复使用同样的 switch 逻辑(可能是以 switch/case 语句的形式,也可能是以连续的 if/else 语句的形式)。重复的switch 的问题在于:每当你想增加一个选择分支时,必须找到所有的 switch,并逐一更新。多态给了我们对抗这种黑暗力量的武器,使我们得到更优雅的代码库。
  • 循环语句
    • 管道操作(如stream流)可以帮助我们更快地看清被处理的元素以及处理它们的动作
  • 冗赘的元素
    • ​​​​​程序元素(如类和函数)能给代码增加结构,从而支持变化、促进复用或者哪怕只是提供更好的名字也好,但有时我们真的不需要这层额外的结构。可能有这样一个函数,它的名字就跟实现代码看起来一模一样;也可能有这样一个类,根本就是一个简单的函数。这可能是因为,起初在编写这个函数时,程序员也许期望它将来有一天会变大、变复杂,但那一天从未到来;也可能是因为,这个类原本是有用的,但随着重构的进行越变越小,最后只剩了一个函数。不论上述哪一种原因,请让这样的程序元素庄严赴义吧。通常你只需要使用内联函数或是内联类。如果这个类处于一个继承体系中,可以使用折叠继承体系
       
  • 夸夸其谈通用性
    • 如果你的某个抽象类其实没有太大作用,请运用折叠继承体系。不必要的委托可运用内联函数内联类除掉。如果函数的某些参数未被用上,可以用改变函数声明去掉这些参数。如果有并非真正需要、只是为不知远在何处的将来而塞进去的参数,也应该用改变函数声明去掉。如果函数或类的唯一用户是测试用例,这就飘出了坏味道“夸夸其谈通用性”。如果你发现这样的函数或类,可以先删掉测试用例,然后使用移除死代码
  • 临时字段
    • ​​​​​​​​​​​​​​有时你会看到这样的类:其内部某个字段仅为某种特定情况而设。这样的代码让人不易理解,因为你通常认为对象在所有时候都需要它的所有字段。在字段未被使用的情况下猜测当初设置它的目的,会让你发疯。请使用提炼类给这个可怜的孤儿创造一个家,然后用搬移函数把所有和这些字段相关的代码都放进这个新家。也许你还可以使用引入特例在“变量不合法”的情况下创建一个替代对象,从而避免写出条件式代码。
  • 过长的消息链
    • ​​​​​​​​​​​​​​如果你看到用户向一个对象请求另一个对象,然后再向后者请求另一个对象,然后再请求另一个对象……这就是消息链。在实际代码中你看到的可能是一长串取值函数或一长串临时变量。采取这种方式,意味客户端代码将与查找过程中的导航结构紧密耦合。一旦对象间的关系发生任何变化,客户端就不得不做出相应修改。这时候应该使用隐藏委托关系。你可以在消息链的不同位置采用这种重构手法。理论上,你可以重构消息链上的所有对象,但这么做就会把所有中间对象都变成“中间人”。通常更好的选择是:先观察消息链最终得到的对象是用来干什么的,看看能否以提炼函数把使用该对象的代码提炼到一个独立的函数中,再运用搬移函数把这个函数推入消息链。如果还有许多客户端代码需要访问链上的其他对象,同样添加一个函数来完成此事。
  • 中间人
    • ​​​​​​​​​​​​​​对象的基本特征之一就是封装——对外部世界隐藏其内部细节。封装往往伴随着委托。但是人们可能过度运用委托。你也许会看到某个类的接口有一半的函数都委托给其他类,这样就是过度运用。这时应该使用移除中间人,直接和真正负责的对象打交道。如果这样“不干实事”的函数只有少数几个,可以运用内联函数把它们放进调用端。如果这些中间人还有其他行为,可以运用以委托取代超类或者以委托取代子类把它变成真正的对象,这样你既可以扩展原对象的行为,又不必负担那么多的委托动作。
  • 内幕交易
    • ​​​​​​​​​​​​​​软件开发者喜欢在模块之间建起高墙,极其反感在模块之间大量交换数据,因为这会增加模块间的耦合。在实际情况里,一定的数据交换不可避免,但我们必须尽量减少这种情况,并把这种交换都放到明面上来就应该用搬移函数搬移字段减少它们的私下交流。如果两个模块有共同的兴趣,可以尝试再新建一个模块,把这些共用的数据放在一个管理良好的地方;或者用隐藏委托关系,把另一个模块变成两者的中介。继承常会造成密谋,因为子类对超类的了解总是超过后者的主观愿望。如果你觉得该让这个孩子独立生活了,请运用以委托取代子类或以委托取代超类让它离开继承体系
  • 过大的类
    • ​​​​​​​​​​​​​​如果想利用单个类做太多事情,其内往往就会出现太多字段。一旦如此,重复代码也就接踵而至了。你可以运用提炼类将几个变量一起提炼至新类内。提炼时应该选择类内彼此相关的变量,将它们放在一起。通常,如果类内的数个变量有着相同的前缀或后缀,这就意味着有机会把它们提炼到某个组件内。有机会把它们提炼到某个组件内。如果这个组件适合作为一个子类,你会发现提炼超类或者提炼子类往往比较简单。有时候类并非在所有时刻都使用所有字段。若果真如此,你或许可以进行多次提炼。和“太多实例变量”一样,类内如果有太多代码,也是代码重复、混乱并最终走向死亡的源头。最简单的解决方案是把多余的东西消弭于类内部。如果有 5 个“百行函数”,它们之中很多代码都相同,那么或许你可以把它们变成 5 个“十行函数”和 10 个提炼出来的“双行函数”。观察一个大类的使用者,经常能找到如何拆分类的线索。看看使用者是否只用到了这个类所有功能的一个子集,每个这样的子集都可能拆分成一个独立的类。一旦识别出一个合适的功能子集,就试用提炼类、提炼超类或是以子类取代类型码将其拆分出来。
  • 异曲同工的类
    • ​​​​​​​​​​​​​​使用类的好处之一就在于可以替换:今天用这个类,未来可以换成用另一个类。但只有当两个类的接口一致时,才能做这种替换。可以用改变函数声明将函数签名变得一致。但这往往还不够,请反复运用搬移函数将某些行为移入类中,直到两者的协议一致为止。如果搬移过程造成了重复代码,或许可运用提炼超类补偿一下
  • 纯数据类
    • ​​​​​​​​​​​​​所谓纯数据类是指:它们拥有一些字段,以及用于访问(读写)这些字段的函数,除此之外一无长物。这样的类只是一种不会说话的数据容器,它们几乎一定被其他类过分细琐地操控着。这些类早期可能拥有 public 字段,若果真如此,你应该在别人注意到它们之前,立刻运用封装记录将它们封装起来。对于那些不该被其他类修改的字段,请运用移除设值函数。然后,找出这些取值/设值函数被其他类调用的地点。尝试以搬移函数把那些调用行为搬移到纯数据类里来。如果无法搬移整个函数,就运用提炼函数产生一个可被搬移的函数。纯数据类常常意味着行为被放在了错误的地方。也就是说,只要把处理数据的行为从客户端搬移到纯数据类里来,就能使情况大为改观。但也有例外情况,一个最好的例外情况就是,纯数据记录对象被用作函数调用的返回结果,比如使用拆分阶段之后得到的中转数据结构就是这种情况。这种结果数据对象有一个关键的特征:它是不可修改的。不可修改的字段无须封装,使用者可以直接通过字段取得数据,无须通过取值函数。
  • 被拒绝的遗赠
    • ​​​​​​​​​​​​​​子类应该继承超类的函数和数据。但如果它们不想或不需要继承,又该怎么办呢?
      它们得到所有礼物,却只从中挑选几样来玩!按传统说法,这就意味着继承体系设计错误。你需要为这个子类新建一个兄弟类,再运用函数下移字段下移把所有用不到的函数下推给那个兄弟。这样一来,超类就只持有所有子类共享的东西。你常常会听到这样的建议:所有超 类都应该是抽象( abstract)的。 既然使用 “传统说法”这个略带贬义的词,你就可以猜到,我们不建议你这么做,起码不建议你每次都这么做。我们经常利用继承来复用一些行为,并发现这可以很好地应用于日常工作。这也是一种坏味道,我们不否认,但气味通常并不强烈,所以我们说,如果 被拒绝的遗赠”正在引起困惑和问题,请遵循传统忠告。但不必认为你每次都得那么做。十有八九这种坏味道很淡,不值得理睬。如果子类复用了超类的行为(实现),却又不愿意支持超类的接口, “被拒绝的遗赠”的坏味道就会变得很浓烈。拒绝继承超类的实现,这一点我们不介意;但如果拒绝支持超类的接口,这就难以接受了。既然不愿意支持超类的接口,就不要虚情假意地糊弄继承体系,应该运用以委托取代子类 或者以委托取代超类 彻底划清界限
  • 注释
    • ​​​​​​​​​​​​​​如果你需要注释来解释一块代码做了什么,试试提炼函数;如果函数已经提炼出来,但还是需要注释来解释其行为,试试用改变函数声明为它改名;如果你需要注释说明某些系统的需求规格,试试引入断言

二、第一组重构

  • 提炼函数
  • 内联函数
  • 提炼变量
  • 内联变量
  • 改变函数声明
  • 封装变量
  • 变量改名
  • 引入参数对象
  • 函数组合成类
  • 函数组合成变换
  • 拆分阶段

三、封装

  • 封装记录
  • 封装集合
  • 对象取代基本类型
  • 查询代替临时变量
  • 提炼类
  • 内联类
  • 隐藏委托关系
  • 移除中间人
  • 替换算法

四、搬移特性

五、重新组织数据

六、简化条件逻辑

七、重构API

八、处理继承关系

  • 函数上移
    • 如果某个函数在各个子类中的函数体都相同,这就是最显而易见的函数上移适用场合
    • 检查待提升函数,确定它们是完全一致的。 如果它们做了相同的事情,但函数体并不完全一致,那就先对它们进行重构,直到其函数体完全一致。检查函数体内引用的所有函数调用和字段都能从超类中调用到。如果待提升函数的签名不同,使用改变函数声明将那些签名都修改为你想要在超类中使用的签名。在超类中新建一个函数,将某一个待提升函数的代码复制到其中。
  • 字段上移
    • 如果各子类是分别开发的,或者是在重构过程中组合起来的,你常会发现它们拥有重复特性,特别是字段更容易重复。这样的字段有时拥有近似的名字,但也并非绝对如此。判断若干字段是否重复,唯一的办法就是观察函数如何使用它们。如果它们被使用的方式很相似,我就可以将它们提升到超类中去。
    • 针对待提升之字段,检查它们的所有使用点,确认它们以同样的方式被使用。如果这些字段的名称不同,先使用变量改名为它们取个相同的名字。
  • 构造函数本体上移
    • 如果超类还不存在构造函数,首先为其定义一个。确保让子类调用超类的构造函数。使用移动语句将子类中构造函数中的公共语句移动到超类的构造函数调用语句之后。逐一移除子类间的公共代码,将其提升至超类构造函数中。对于公共代码中引用到的变量,将其作为参数传递给超类的构造函数。
    • class party {
          public party(String name) {
              this.name = name;
          }
      }
      class Employee extends party {
          public party(String name, String id) {
              super(name);
              this.id= id;
          }
      }
      class Department extends party {
          public party(String name, String staff) {
              super(name);
              this.staff= staff;
          }
      }
  • 函数下移
    • 如果超类中的某个函数只与一个(或少数几个)子类有关,那么最好将其从超类中挪走,放到真正关心它的子类中去。这项重构手法只有在超类明确知道哪些子类需要这个函数时适用。如果超类不知晓这个信息,那我就得用以多态取代条件表达式 ,只留些共用的行为在超类。
    • 将超类中的函数本体移到需要此函数的子类中
  • 字段下移
    • 如果某个字段只被一个子类(或者一小部分子类)用到,就将其搬移到需要该字段
      的子类中。
  • 以子类取代类型码
    • 软件系统经常需要表现“相似但又不同的东西”,比如员工可以按职位分类(工程师、经理、销售),订单可以按优先级分类(加急、常规)。表现分类关系的第一种工具是类型码字段——根据具体的编程语言,可能实现为枚举、符号、字符串或者数字。类型码的取值经常来自给系统提供数据的外部服务。大多数时候,有这样的类型码就够了。但也有些时候,我可以再多往前一步,引入子类。继承有两个诱人之处。首先,你可以用多态来处理条件逻辑。如果有几个函数都在根据类型码的取值采取不同的行为,多态就显得特别有用。引入子类之后,我可以用以多态取代条件表达式来处理这些函数。另外,有些字段或函数只对特定的类型码取值才有意义,例如 销售目标 只对“销售”这类员工才有意义。此时我可以创建子类,然后用字段下移把这样的字段放到合适的子类中去。
    • abstract class Shape {
          abstract void draw();
      }
      class Circle extends Shape {
          @Override
          void draw() {
              System.out.println("Drawing a circle");
          }
      }
      class Rectangle extends Shape {
          @Override
          void draw() {
              System.out.println("Drawing a rectangle");
          }
      }
      public class Main {
          public static void main(String[] args) {
              Shape[] shapes = new Shape[2];
              shapes[0] = new Circle();
              shapes[1] = new Rectangle();
              for (Shape shape : shapes) {
                  shape.draw();
              }
          }
      }

http://www.kler.cn/a/556106.html

相关文章:

  • 力扣LeetCode: 2209 用地毯覆盖后的最少白色砖块
  • 基于windows的docker-desktop安装kubenetes以及dashboard
  • 【消息队列】认识项目
  • 信创浪潮下,以 OpManager筑牢安全运维防线
  • TDengine 产品组件: taosd
  • Apache Spark 的主要特点
  • 【Python爬虫(40)】分布式爬虫:数据一致性的破局之道
  • Python的那些事第二十八篇:数据分析与操作的利器Pandas
  • 基于 DeepSeek+AutoGen 的智能体协作系统
  • 游戏引擎学习第111天
  • 算法1-4 数楼梯
  • C++栈与队列:数据结构的“单行道”与“流水线
  • k8s ssl 漏洞修复
  • 深入剖析 Vue 的计算属性与侦听属性:差异、应用及最佳实践
  • CDGA|企业数据治理实战:从疏通“信息河”到打造优质“数据湖”
  • TCP三次握手 四次挥手:一场“确认眼神”与“礼貌告别”的对话
  • DeepSeek 助力 Vue 开发:打造丝滑的缩略图列表(Thumbnail List)
  • 【数据库系统概论】第6章 (三)数据依赖的公理系统
  • 深度解析应用层协议-----HTTP与MQTT(涵盖Paho库)
  • 【UCB CS 61B SP24】Lecture 4 - Lists 2: SLLists学习笔记