2017 October 16 —— improvement; java

重构与模式精读

重构与模式(Refactoring to Patterns)

简化与泛化两章占用了本书一半的篇幅,是重中之重,需要时时温故而知新,多思考,多总结,多结合实际情况去体会作者的思考过程;

实际情况中通过分析,期望以重构为手段实现各类模式的优雅使用.在我看来,模式的本质是章法,有了章法则心中不乱,手上不慌,借助章法但又不能局限于章法,有了章法可以站在前人的肩膀上看的更远,局限于章法却又往往容易脱离实际落个生搬硬套的俗套

实际情况中,究竟是需要实现模式? 还是趋近于模式就值得分析情况了,但无论如何重构的一些手段与情况确实是非常重要的,如何去拓展功能?拓展他人代码?从旧的代码中添加新的业务逻辑而又尽可能的保留原有结构的完整性,尽可能的减慢代码的腐朽程度,甚至让代码重新焕发生机.

创建

事实上创建这一章节是我们通常最常用的实践,非常有必要一读再读;

从解决问题的角度看待实现方式:

Create Method: 解决构造函数过多导致的迷惑性,以及构造函数天生的表达能力不足性问题,同时避免了语言中构造函数的局限性问题;

普通的 CreateMethod 是利用对象中的静态 Create 函数构建对应目标对象;而进一步当 Create 函数较多时,我们可以将 Create 函数提取到独立的 Factory 中进而完成简单工厂形式的优化;(重构与模式这本书好就好在问题的解决时层层递进式,当解决一个问题时,引入一个新的问题,一步步彻底把重构这件事说清楚,每个模式究竟是解决一类什么样的问题)

由于静态 Create 函数的非标准化构建,静态的 Create 函数可能存在于任意地方就等同于直接 new 对象,进而导致对象的创建蔓延问题;(创建蔓延问题:通过层层传递创建对象导致对象创建的逻辑蔓延到整个系统各层,极大约束了系统了扩展性以及整体逻辑性)

当 Factory 的逻辑进一步复杂化时,甚至 Factory 本身开始出现了诸多重复构建代码时,我们可以进一步抽象 Factory ,也就是抽象的抽象,进而完成抽象工厂的实现;

针对抽象的抽象更好的实践则是利用工厂的多态,面向接口而非面向实现;

Builder: 利用链式封装构建对象时的可选策略,减少了多参数策略构建时容易出错遗漏的问题,以及在客户端与构造参数之间实现松耦合;需要注意的是 Builder 的使用是否意义明确,是否足够简单,是否迷惑了使用者?

Singleton 的错误使用: 仔细的考虑对象的可见范围,可见性,而不是笼统的一概用单例去简化之间的思考,去思考是否真的需要单例去暴露对象在整个系统中;

始终要记住单例是伴随整个应用生命周期之中,会成为应用上下文Context的一部分,过多的单例会复杂化上下文;

单例最大的简化就是在多层级之间的对象传递,而不用再系统中将对象层层传递,但不使用单例则可能将系统构建的更具弹性,对象之间的关系更加清晰,对象之间的协作性也更加明确;

简化

组合方法

提炼函数:那个函数包含哪些代码?命名是否能够真实表达代码逻辑?意图要被真实而清晰表达

提炼函数带来的性能问题? 是否真实的带来性能问题需要 profile 确认 - 原则等到需要优化时再进行优化;

统一逻辑层次,更抽象更清晰:一个方法分解为一组行为时,需要保证行为在细节的相似层面.行为的层面?高级别与低级别,如 打开文件/关闭文件;

如: guard clause : if(true) return 提前声明出口

组合方法借助抽取方法的手段是简单,但直接且有效的重构途径

策略简化

大多数时候复杂的业务场景都伴随着复杂的 if/else 语句,正如有人透漏百度无人驾驶超过10万的 if/else, 或许带着些许夸张,但对于驾驶的复杂程度来说当然也是可信的;

复杂的条件逻辑是最常导致复杂度上升的地方之一

事实上,随着业务的推进,算法的逻辑总是在增长,这类条件逻辑更是迅速膨胀,并最终成为业务遗留代码;

条件通常决定算法的实际变体类型,而其精简方案通常是分解条件以及精简逻辑结合组合方法;而在实际应用中又有 组合与继承的实现方式,继承则将算法在子类实现,而组合则利用一个个特殊类进行业务处理;通常继承与多态总是相伴,多态给类的实现带来了充分的灵活性与扩展性,而又通过继承保留了父类的约束;而借助组合的方式则利用策略的一个实现,并在算法的实现过程中将实际算法实现委托给具体策略执行,并将上下文环境注入到策略中,以便策略利用外部环境选择具体策略

注入上下文环境是策略实现的关键:究竟是将环境对象注入以便直接获取到环境的所有属性与方法,还是将环境对象的属性一一借助 setter 注入到策略是需要考量的;

核心装饰

装饰为核心功能赋予了良好的后期扩展性,保护了类职责的清晰,逻辑的清晰;事实上,在通常的业务中,一旦涉及到核心功能的扩展以及更改,如果简单的在旧类中增添新的代码,新的字段,新的逻辑,新的职责加入后将导致原有类的复杂度几何倍增,类的意义开始不明,职责界限开始混杂,更麻烦的是这些新加入的逻辑只是在某些条件下才会触发;

而装饰模式,则很好的解决了这一问题,装饰的功能逻辑独立存在,在特定的逻辑需要时,则通过装饰核心类,通过核心类的包装对象完成对应的业务逻辑,当脱去外衣,核心类还是本来的面貌;

透明的包装

装饰应该实现所有被包装类中被暴露的方法,从而达到被包类无感知自身被包装,而如果被包装类的暴露方法过多,将导致装饰者需要实现过多冗余函数才能达到透明包装的目的,因而核心类的暴露应该是精简的;

包装事实上是将一个类中的复杂逻辑代码分离到了各个装饰器中,当然这就提升了代码理解的复杂度,毕竟无论如何代码被分散了,逻辑的分散还带来了调试的复杂度提升问题,这都是需要考量的问题.

装饰的变种–代理,事实上二者的实现非常相近,但实现的意图则大不相同,代理意在保护目标对象不被破坏,屏蔽对象被外部直接真实访问;而装饰则是行为的添加与扩展;

重构中装饰模式与策略模式的竞争关系:去除特殊情况或选择性行为相关联的条件逻辑,且通过将行为搬移的方式达到去除关联性的目的;而具体情况下究竟使用何种方式则需要因地制宜,实际情况实际分析;

State 替换状态

Commond 替换条件

当某些条件分支下的决策代码过于复杂时,将导致理解困难,而当这种复杂决策过多时,将导致整体系统的复杂性大大提升,进而导致维护困难;而对于较剪短的条件策略,如果强行生搬硬套会增加系统复杂度,也是不可取的;

通过提炼函数,以及提炼抽象类,实现 Handler 的构建, Handler 保存了外部执行策略的相关对应关系,约定好传入什么参数,excute 执行什么命令,如果简单从替换条件分支看似乎 Commond 有点类似策略,但与策略需要处理对于上下文环境的感知不同, Commond 不关心外部调用者,客户端也不关心命令的接受者如何实现,只负责发出命令,本质上命令是一种将请求参数化的表达形式;正如用户不必关心电脑如何开关机,只需要发起开关机命令即可;

所以说书中的 commond 替换条件其实是指最简单的形式,而这样的形式又其实有多重途径可以实现,我认为书中的这一节是模糊而不明确的,没有标明命令模式的精髓;

if(按下电源){
    开机// 冗余逻辑亦或是封装逻辑
}

命令模式通过接口约束命令对象的封装实现,每个具体的命令实现对象对应客户端的一种参数化请求,再利用第三者对象进行封装命令的管理,这其实也是一种请求者与实现者的彻底解耦;

而除此之外,命令模式更重要的是命令的序列化执行以及命令执行的撤销处理等;

通常命令的撤销分为: 反操作式执行(撤销时执行命令中的反向命令),以及存储恢复式(恢复命令执行之前的系统状态)

泛化

从特殊到通用: 去除冗余代码,简化逻辑

模版方法:

去除同一类层次结构中子类所包含的相似函数中的冗余重复代码 – 沉淀到父类;通过统一相似子类函数的方法签名,进而提取泛化上移到父类中进行处理;从而不变量在父类被公用,而留下变动在子类进行特殊处理;

Template Method 的不变行为包含: 1. 被调用方法与其被调用顺序 2.子类必须重写的抽象方法 3.子类可能重写的钩子函数

模版方法通常还借助 factoryMethod 引入多态以及 借助 Java final 关键字避免意外的子类的重写导致更改原有的不变;通过模版方法的不变因子,有效的表达了一个通用的结构模型,同时子类可以很容易的根据既定框架定制特定算法;

为了重构实现模版方法,去取冗余,通常我们从找相似开始;进而改进相似到重复,提取重复因子;但我们要注意的是提取的重复因子在父类是要有意义的,不能为了复用而导致逻辑混乱;

通常模式并非独立存在的,比如举例对于 策略模式的重构,策略模式简化了 ifelse 但是又造成子类膨胀,很可能在各个子类中又开始又一些冗余存在,针对策略子类实现中的冗余逻辑我们进一步通过模版方法的形式进行重构简化冗余部分,再次还策略一个清晰的逻辑算法框架;

提取 Composite

如果类层次中,多个子类实现了同一个 Composite, 则对该 Composite 的子类进行提取,提取一个父类,进而子类通过继承该父类实现,精简类间层次结构;

由于各个子类都实现了同一个 Composite, 导致各个子类中很可能都存在复杂的业务逻辑,进而导致整个系统的复杂度升高,而进行提取之后可以让子类逻辑变得精简,而同时整个系统由于有父类的存在而联系更加紧密,对于子类的理解程度也会更高;

提取组合的父类事实上是提炼超类的本质是相同的,只有在涉及到 Composite 时才使用这种方式,否则采用通常的提炼超类进行处理;

单一对象与多个对象处理逻辑的替换

利用 Composite 处理单一对象与多个对象的处理工作:

  • action(One)

  • action(ListOne)

几乎相同的代码,处理不同的对象,单个对象以及多个对象,造成冗余的逻辑以及不一致的客户代码(不一致的方法签名导致),这就是散落在四处的重复代码;

而利用组合则统一单一对象方法以及多对象方法,统一客户端调用逻辑,不再关心方法签名,只需要调用统一方法名代表;

如何统一单个对象方法以及多个对象方法?

利用组合对象封装多个对象并借助多态(实现单个对象接口),从而变多个对象为单个对象,再借助提炼函数,将多个对象的函数变为对于单个对象函数的装饰(添加一些特定的外部功能),进而完成重构;

利用 Observer

改硬编码的消息通知为 Observer ,变紧耦合为松耦合,通过观察者接口以及主题实现,主题通常有添加以及删除 Observer 的函数实现,而对于删除则需要根据在主题的生命周期中是否真的必要完成删除操作进而选择是否实现;而改删除时不删除则可能造成 内存泄漏问题

利用 Observer 改造硬编码通知问题并不困难,很多时候我们需要具体考量是否上来就需要应用 Observer 模式,防止滥用的问题,如果硬编码已经满足了需求就无需改造为观察者模式;

Adapter 统一接口

通过 Adapter 可以在依赖运行时环境状态,进而使用正确的 Adapter 来配置程序的运行;

Adapter 帮助决定如何与他人代码交互;甚至可以通过 V1 V2这样的形式在运行时决定究竟使用何时的代码;

Adapter 与 Facade(外观模式)通常 外观模式针对子系统对外交互的包装,用于系统性适配;开发者可以通过编写新的 Facade 来完成整个系统的重写,进而客户端通过包装的 Facade 调用,此外还能通过编写不同版本的 Facade 完成新旧技术的迁移,避免频繁的修改核心代码或者是 LegacyCode;

Interpreter

切面编程: 典型 Android 应用 AnnotationProcessor 以及 apt 生成源码;

  • 利用隐式语言生成业务代码实现,精简逻辑;
  • 衡量考虑: 语言过于简单则增加设计复杂度,语言过于复杂则又对于 interceptor 的构建复杂度提升过大;
上一篇
下一篇
Loading Disqus comments...
Table of Contents