重构-改善既有代码的设计:重新组织数据的16种方法(六)
1.Self Encapsulate Field 自封装字段
间接访问类的属性:你直接访问一个字段,但与字段之间的耦合关系逐渐变得笨拙。为这个字段建立取值/设值函数,并且只以这些函数来访问字段。
间接访问变量的好处是,子类可以通过覆写一个函数而改变获取数据的途径;它还支持更灵活的数据管理方式,例如延迟初始化。
如果你想访问超类中的一个字段,却又想子类中将对这个变量的访问改为一个计算后的值,这就是使用Self Encapsulate Field (自封装字段)的时候。“字段自我封装”只是第一步。完成自我封装后,你可以在子类中根据自己的需要随意覆写取值/设值函数。
2. Replace Data Value with Object 对象取代数据值
聚合改为组合:你有一个数据项,需要与其他数据和行为一起使用才有意义。将数据项变成对象。
开发初期,你往往决定以简单的数据项表示简单的情况。但是,随着开发的进行,你可能会发现,这些简单数据项不再那么简单了。如果这样的数据项只有一两个,你还可以把相关函数放进数据项所属的对象里;但是重复代码(Duplicated Code)坏味道和 依恋情结(Feature Envy)坏味道很快就会从代码中散发出来,当这些坏味道开始出现,就应该将数据值变成对象。
3.Change value to Reference 将值对象改为引用对象
组合改为重数性关联(另一个类的一个对象与一个或多个该类对象有关系 Customer类对象可以包含多个order类的对象。):从一个类中衍生出许多彼此相等的实例,希望将它们替换为一个对象。将这个值对象变成引用对象。(Customer是一个实值对象,就算多份订单属于一个客户每个Order对象还是拥有各自的Customer对象。重构的结果是所有的订单对象共享一个客户名称。也就是,一个客户对象只对应一个客户名称。)
可以将对象分成两类:reference object(引用对象)和value object(实值对象)。
有一个实值对象,在其中保存了少量的不可修改的数据。你需要给这个对象加入一些可修改的数据,并确保对任何一个对象的修改都能影响到所有引用此对象的地方。这是后就需要将value object(实值对象)变成一个reference object(引用对象)。
在许多系统中,都可以对对象做一个有用的分类:引用对象和值对象。要在引用对象和值对象之间做选择有时并不容易。有时候,你会从一个简单的值对象开始,在其中保存少量不可修改的数据。而后,你可能会希望给这个对象加入一些可修改数据,并确保对任何一个对象的修改都能影响到所引用此对象的地方。这时候你就需要将这个对象变成引用对象。
4. Change Reference to Value 将引用对象改为值对象
你有一个引用对象,很小且不可变,而且不易管理。 将它变成一个值对象。
要在引用对象和值对象之间做选择,有时并不容易。做出选择后,你常会需要一条回头路,
如果引用对象开始变得难以使用,也许就应该将它改为值对象。引用对象必须被某种方式控制,你总是必须向其控制者请求适当的引用对象。它们可能造成内存区域之间错综复杂的关联。在分布式和并发系统中,不可变的值对象特别有用,因为你无需考虑它们的同步问题。
值对象有一个非常重要的特征:它们应该是不可变的。无论何时,只有你调用同一对象的同一个查询函数,都应该得到同样结果。如果保证了这一点,就可以放心地以多个对象表示同一个事物。如果值对象是可变的,你就必须确保某个对象的修改会自动更新其他“代表相同事物”的对象。与其如此还不如把它变成引用对象。
澄清下“不可变”(immutable)的意思:如果你以money类表示“钱’的概念,其中有”币种“和”金额“2条信息。那么Money对象通常是一个不可变的值对象。这并非意味你的薪资不能改变,而是意味:如果要改变你的薪资,就需要使用另一个Money对象来取代现有的Money对象,而不是在现有的Money对象上修改。你和Money对象之间的关系可以改变,但Money对象自身不能改变。
5.Replace Array with Object 以对象取代数组
数组时一种常见的用以组织数据的结构。不过,它们应该只用于“以某种顺序容纳一组相似对象”。有时候你会发现,一个数组容纳了多种不同对象,这会给用户带来麻烦,因为他们很难记住像“数组的第一个元素是人名”这样的约定。对象就不同了,你可以运用字段名和函数名来传达这样的信息,因此你无需死记它,也无需依赖注释。而且如果使用对象,你还可以将信息封装起来。并使用 Move Method (搬移函数)为它加上相关行为。
6. Duplicate Observed data 复制被监视数据
你有一些领域数据置身于GUI控件中,而领域函数需要访问这些数据。将该数据复制到一个领域对象中。建立一个Observer模式,用以同步领域对象和GUI对象内的重复数据。
一个分层良好的系统,应该将处理用户界面和处理业务逻辑的代码分开。之所以这样做,原因有以下几点:1)你可能需要使用不同的用户界面来表现相同的业务逻辑,如果同时承担2种责任,用户界面会变得过分复杂;2)与GUI隔离后,领域对象的维护和演化都会更容易,你甚至可以让不同的开发者负责不同部分的开发。
尽管可以轻松地将“行为”划分到不同部位,“数据”却往往不能如此。同一项数据可能既需要内嵌于GUI控件,也需要保存于领域模型里。自从MVC模式出现后,用户界面框架都使用多层系统来提供某种机制,使你不但可以提供这类数据,并保持它们同步。
如果你遇到的代码是以2层方式开发,业务逻辑被内嵌于用户界面之中,你就有必要将行为分离出来。其中的主要工作就是函数的分解和搬移。但数据就不同了;你不能仅仅只是移动数据,必须将它复制到新的对象中,并提供相应的同步机制。
7.Change Unidirection Association to Bidirectional 将单向关联改为双向关联
两个类都需要使用对方特性,但其间只有一条单向连接。添加一个反向指针,并使修改函数能够同时更新2条连接。
开发初期,你可能会在2个类之间建立1条单向连接,使其中一个类可以使用另一个类。随着时间推移,你可能发现被引用类需要得到其引用者以便进行某些处理。也就是说它需要一个反向指针。但指针是一种单向连接,你不可能反向操作它。通常你可以绕道而行,虽然会耗费一些计算时间,成本还算合理,然后你可以在被引用类中建立一个函数专门负责此行为。但是,有时候想绕过这个问题并不容易,此时就需要建立双向引用关系,或称为反向指针。如果使用不当,反向指针和很容易造成混乱;但只要你习惯了这种手法,它们其实并不太复杂。
“反向指针”手法有点棘手,所以在你能够自如运用之前,应该有相应的测试。通常不用花心思去测试访问函数,因为普通访问函数的风险没有高到需要测试的地步,但本重构要求测试访问函数,所以它是极少数需要添加测试的重构手法之一。
本重构运用反向指针实现双向关联,其他技术需要其他重构手法。
8.Change Bidirectional Association to Unidirection将双向关联改为单向关联
双向关联很有用,但你必须为它付出代价,那就是维护双向连接、确保对象被正确创建和删除而增加的复杂度。而且,由于很多程序员并不习惯使用双向关联,它往往成为错误之源。
大量的双向连接也很容易造成“僵尸对象”:某个对象本来应该死亡了,却仍然保留在系统中,因为对它的引用没有完全清除。
此外,双向关联也迫使2个类之间有了依赖:对其中任一个类的任何修改,都可能引发另一个类的变化。如果这2个类位于不同的程序集,这种依赖就是程序集之间的相依。过多的跨程序集依赖会造就紧耦合的系统,使得任何一点小小改动就可能造成许多无法预知的后果。
只有在真正需要双向关联的时候,才该使用它。如果发现双向关联不再有存在价值,就应该去掉不必要的一条关联。
9.Replace Magic Number with Symbolic Constant字面常量取代魔法数
在计算科学中,魔法数是历史悠久的不良现象之一。所谓魔法数是指拥有特殊意义,却又不能明确表现出这种意义的数字。如果你需要在不同的地点引用同一个逻辑数,魔法数会让你烦恼不已,因为一旦这些数发生变化,你就必须在程序中找到所有魔法数,并将它们全部修改一遍。就算你不需要修改,要准确指出每个魔法数的用途,也会让你颇费脑筋。
许多语言都允许声明常量。常量不会造成任何性能开销,却可以大大提高代码的可读性。
进行本项重构之前,你应该先寻找其他替代方案。你应该观察魔法数任何被使用,而后你往往会发现一种更好的使用方式。如果这个魔法数是个类型码,请考虑使用 Replace Type Code with Class (以类取代类型码);如果这个魔法数代表一个数组的长度,请在遍历数组时,改用数组.length。
10.Encapsulate Field 封装字段
面向对象的首要原则之一就是封装,或者称为“数据隐藏”。按此原则,你绝不应该将数据声明为public,否则其他对象就有可能访问甚至修改这项数据,而拥有该数据的对象却毫无察觉。于是,数据和行为就被分开了。
数据声明为public被看做是一种不好的做法,因为这样会降低程序的模块化程度。数据和使用该数据的行为如果集中在一起,一旦情况发生变化,代码的修改就会比较简单,因为需要修改的代码都集中于同一块地方,而不是星罗棋布地散落在整个程序中。
Encapsulated Field (封装字段)是封装过程的第一步,通过这项重构手法,你可以将数据隐藏起来,并通过相应的访问函数。但它毕竟只是第一步。如果一个类除了访问函数外不能提供其他行为,它终究只是一个哑巴类。这样的;类并不能享受对象技术带来的好处。实施Encapsulated Field (封装字段)之后,尝试寻找用到新建访问函数的代码,看看是否可以通过简单的 Move Method(搬移函数)将它们移到新对象去。
11.Encapsulate Coolection 封装集合
我们常常会在一个类中使用集合来保存一组实例。这样的类通常也会提供针对该集合的取值/设值函数。
但是,集合的处理方式应该和其他种类的数据略有不同。取值函数不该返回集合自身,因为这会让用户得以修改集合内容而集合拥有者却一无所悉。也会对用户暴露过多对象内部数据结构信息。如果一个取值函数确实需要返回多个值,它应该避免用户直接操作对象内所保存的集合。并隐藏对象内与用户无关的数据结构。
另外,不应该为这整个集合提供设值函数,但应该提供用以为集合添加/移除元素的函数。这样,集合拥有者就可以控制集合元素的添加和移除。
如果你做到以上几点,集合就被很好地封装起来,这便可以降低集合拥有者和用户之间的耦合度。
12.Replace Record with Data Class 以数据类取代记录
13. Replace Type Code with Class 以类来取代类型码
在以C为基础的编程语言中,类型码或枚举值很常见。如果带着一个有意义的符号名,类型码的可读性还不错。问题在于,符号名终究只是个别名,编译器看见的、进行类型检验的,还是背后那个数值。任何接受类型码作为参数的函数,所期望的实际上是一个数值,无法强制使用符号名。这会大大降低代码的可读性,从而成为bug之源。
如果把那样的数值换成一个类,编译器就可以对这个类进行类型检验。只要为这个类提供工厂函数,你就可以始终保证只有合法的实例才会被常见出来,而且他们都会被传递给正确的宿主对象。
但是,在使用Replace Type Code with Class (以类取代类型码)之前,你应该先考虑类型码的其他替换方式。只有当类型码是纯粹数据时(也就是类型码不会在switch语句中引起行为变化时),你才能以类来取代它。更重要的是:任何switch语句都应该运用Replace Conditional with Polymorphism (以多态取代条件表达式)去掉。为了进行那样的重构,你首先必须运用 Replace Type Code with Subclass (以子类取代类型码)或Replace Type Code with State/Strategy (以状态策略取代类型码),把类型码处理掉。
即使一个类型码不会因其数值的不同而引起行为上的差异,宿主类中的某些行为还是可能更适合放置于类型码类中,因此你还应该留意是否有必要使用Move Method(搬移函数)将一两个函数搬过去。
14.Replace Type Code with Subclasses 以子类来取代类型码
如果你面对的类型码不会影响宿主类的行为,可以使用Replace Type Code with Class (以类取代类型码)来处理它们。但如果类型码会影响宿主类的行为,那么最后的办法就是借助多态来处理变化行为。
一般来说,这种情况的标志就是像switch这样的条件表达式。这种条件表达式可能有2种表现形式:switch语句或者if –then-else结构。不论哪种形式,它们都是检查类型码值,并根据不同的值执行不同的动作。这种情况下,你应该以 Replace Conditional with Polymorphism (以多态取代条件表达式)进行重构。但为了那个顺利进行那样的重构,首先应该将类型码替换为可拥有多态行为的继承体系。这样一个继承体系应该以类型码宿主类为基类,并针对每一种类型码建立一个子类。
但是以下2种情况你不能那么做1)类型码值在对象创建之后发生了变化;2)由于某种原因,类型码宿主类已经有了子类。如果你恰好面临这2种情况之一,就需要运用 Replace Type Code with StateStrategy (以状态策略取代类型码)。
Replace Type Code with Subclass (以子类取代类型码)的主要作用其实是搭建一个舞台,让Replace Conditional with Polymorphism (以多态取代条件表达式)得以一展身手。如果宿主类中并没有出现条件表达式,那么Replace Type Code with Class (以类取代类型码)更合适,风险也较低。
使用Replace Type Code with Subclass (以子类取代类型码)的另一个原因是,宿主类中出现了“只与具备特定类型码之对象相关”的特性。完成本项重构后,你可以使用 push down Method (函数下移)和push down field (字段下移)将这些特性推到合适的子类中去,以彰显它们只与特定情况相关这一事实。
Replace Type Code with Subclass (以子类取代类型码)的好处在于:它把“对不同行为的了解”从类用户那转移到了类自身。如果需要再加入新的行为变化,只需要添加一个子类就行了。如果没有多态机制,就必须找到所有条件表达式,并逐一修改它们。因此,如果为了还有可能加入新行为,这项重构将特别有价值。
15.Replace Type Code with State/Strategy 以状态/策略取代类型码
本项重构和Replace Type Code with Subclass (以子类取代类型码)类似,但如果“类型码在对象生命期中发生变化”或“其他原因使得宿主类不能被继承”,你也可以使用本重构。本重构使用State模式和Strategy模式。
State模式和Strategy模式非常相似,因此无论你选择其中哪一个,重构过程都是一样的。“选择哪个模式”并非问题的关键所在,你只需要选择更合适特定情境的模式就行了。如果你打算在完成本重构后再以Replace Conditional with Polymorphism (以多态取代条件表达式)简化一个算法,那么选择Strategy模式较合适;如果你打算搬移状态相关的数据,而且你把新建对象视为一种变迁状态,就应该选择State模式。
16.Replace Subclass with Fieldls 以字段取代子类
建立子类的目的,是为了增加新特性或变化其行为。有一种变化行为被称为“常量函数”,它们会返回一个硬编码的值。这东西有其用途:你可以让不同的子类中的同一个访问函数返回不同的值。你可以在超类中将访问函数声明为抽象函数,并在不同子类中让它返回不同的值。
尽管常量函数有其用途,但若与子类中只有常量函数,实在没有足够的存在价值。你可以在超类中设计一个与常量函数返回值相应的字段,从而完全除去这样的子类。如此一来就可以避免因继承而带来的额外复杂性。
重构-改善既有代码的设计:重新组织数据的16种方法(六)相关推荐
- 重构—改善既有代码的设计
概述 1.1 参考资料 <重构-改善既有代码的设计>读后总结 <重构改善既有代码的设计> 22种代码的坏味道,一句话概括 1.2 何谓重构 首先要说明的是:视上下文不同,重构的 ...
- PHP 杂谈《重构-改善既有代码的设计》之二 对象之间搬移特性
思维导图 索引: Ø Move Method(搬移函数) Ø Move Field (搬移值域) Ø Extract Class (提炼类) Ø Inline Class (将类内联化,就是把当前的类 ...
- 『重构--改善既有代码的设计』读书笔记----序
作为C++的程序员,我从大学就开始不间断的看书,看到如今上班,也始终坚持每天多多少少阅读技术文章,书看的很多,但很难有一本书,能让我去反复的翻阅.但唯独『重构--改善既有代码的设计』这本书让我重复看了 ...
- PHP 杂谈《重构-改善既有代码的设计》之三 重新组织数据
介绍 承接上文的PHP 杂谈<重构-改善既有代码的设计>之 重新组织你的函数继续重构方面的内容. 这章主要针对数据的重构. 1.争论的声音--直接访问Field还是通过函数(Accesso ...
- 重构 改善既有代码的设计:代码的坏
以下内容来自<<重构 改善既有代码的设计>> 一.什么是重构 所谓重构(Refactoring)是这样一个过程:在不改变代码外在行为的前提下,对代码做出修改以改进程序的内部结构 ...
- 《重构-改善既有代码的设计》-第1例:租赁影片(2)
前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家.点击跳转到教程. 上接 重构-改善既有代码的设计-第1例:租赁影片(1) 2 运用多态取代与价格相关的条件逻辑 2 ...
- 《重构-改善既有代码的设计》-第1例:租赁影片(1)
前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家.点击跳转到教程. 买了<重构 - 改善既有代码的设计 >一书,一直没有好好看,大致过了下也觉得只是有点点印 ...
- 实践提高《重构改善既有代码的设计第2版》PDF中文+PDF英文+对比分析
重构是编程的基础,是在不改变外部行为的前提下,有条不紊地改善代码.编程爱好者都知道,Martin Fowler 的<重构:改善既有代码的设计>已经成为全球有经验的程序员手中的利器,既可用来 ...
- 重构改善既有代码的设计(github源码)
refactoring improving the design of existing code(重构改善既有代码的设计) https://github.com/CoderDream/refacto ...
最新文章
- python入门有基础-Python基础知识入门,这三个原则是你要知道的,还有教程
- namespace命名空间的理解C++
- python 项目生成 requirements.txt 文件
- java包和访问权限_Java包和访问权限—1
- mysql 默认当前时间_复制信息记录表|全方位认识 mysql 系统库
- video4linux简介
- Spring AOP 本质
- 一种较方便的MATLAB GUI中popupmenu中选取值得获得方法
- GitLab 公开拒收中国员工,你怎么看?!
- 一个简单的python登录验证系统
- redis命令之哈希表类型lpush命令用法详情
- pc端支付宝扫码支付
- ei检索的cpss_国际检索系统收录我校期刊
- 香港常见问题扫盲贴(港币兑换/签注直飞/刷卡/酒店押金/关税/香港上网)
- 4.2.3偏移寻址(19)
- linux系统php连接dm达梦
- 【六】Python全栈之路--for循环
- 2014年11月3日至2014年12月29日
- Codeforces Round #787 (Div. 3)
- 通达信交易系统接口是不是免费的?
热门文章
- 【Android 异步操作】线程池 ( 线程池使用示例 | 自定义线程池使用流程 | 自定义任务拒绝处理策略 | 完整代码示例 )
- clipboard_monitor_in_win7
- 个人随笔之《我有一个需要妈妈帮我实现的愿望》
- 将秒(非时间戳)转化成 ** 小时 ** 分
- CDQ分治嵌套模板:多维偏序问题
- fancybox关闭弹出窗体parent.$.fancybox.close();
- 荷兰国旗 Flag of the Kingdom of the Netherlands
- 给UIButton添加背景图片
- 如何评估自己对外界认知是否正确?
- 【转】Linux上安装使用SSH(ubunturedhat)