【Java重构系列】重构31式之封装集合

2009年,Sean Chambers在其博客中发表了31 Days of Refactoring: Useful refactoring techniques you have to know系列文章,每天发布一篇,介绍一种重构手段,连续发文31篇,故得名“重构三十一天:你应该掌握的重构手段”。此外,Sean Chambers还将这31篇文章【即31种重构手段】整理成一本电子书, 以下是博客原文链接和电子书下载地址:

博客原文:http://lostechies.com/seanchambers/2009/07/31/31-days-of-refactoring/

电子书下载地址:http://lostechies.com/wp-content/uploads/2011/03/31DaysRefactoring.pdf

      本系列博客将基于Sean Chambers的工作,但是更换了编程语言(C# --> Java),更重要的是增加了很多新的内容,融入了大量Sunny对这些重构手段的理解,实例更加完整,分析也更为深入,此外,还引申出一些新的讨论话题,希望能够帮助大家写出更高质量的程序代码!

这三十一种重构手段罗列如下【注:原文是重构第N天,即Refactoring Day N,Sunny个人觉得有些重构非常简单,一天一种太少,不过瘾,于是重命名(Rename)为重构第N式,计划对这31种重构手段用Java语言重新介绍一遍,介绍次序与原文次序并不完全一致,补充了很多新的内容,】:

Refactoring 1: Encapsulate Collection【重构第一式:封装集合】

Refactoring 2: Move Method【重构第二式:搬移方法】

Refactoring 3: Pull Up Method【重构第三式:上移方法】

Refactoring 4: Pull Up Field【重构第四式:上移字段】

Refactoring 5: Push Down Method【重构第五式:下移方法】

Refactoring 6: Push Down Field【重构第六式:下移字段】

Refactoring 7: Rename(method,class,parameter)【重构第七式:重命名(方法,类,参数)】

Refactoring 8: Replace Inheritance with Delegation【重构第八式:用委托取代继承】

Refactoring 9: Extract Interface【重构第九式:提取接口】

Refactoring 10: Extract Method【重构第十式:提取方法】

Refactoring 11: Switch to Strategy【重构第十一式:重构条件语句为策略模式】

Refactoring 12: Break Dependencies【重构第十二式:消除依赖】

Refactoring 13: Extract Method Object【重构第十三式:提取方法对象】

Refactoring 14: Break Responsibilities【重构第十四式:分离职责】

Refactoring 15: Remove Duplication【重构第十五式:去除重复代码】

Refactoring 16: Encapsulate Conditional【重构第十六式:封装条件表达式】

Refactoring 17: Extract Superclass【重构第十七式:提取父类】

Refactoring 18: Replace exception with conditional【重构第十八式:用条件语句取代异常】

Refactoring 19: Extract Factory Class【重构第十九式:提取工厂类】

Refactoring 20: Extract Subclass【重构第二十式:提取子类】

Refactoring 21: Collapse Hierarchy【重构第二十一式:合并继承层次结构】

Refactoring 22: Break Method【重构第二十二式:分解方法】

Refactoring 23: Introduce Parameter Object【重构第二十三式:引入参数对象】

Refactoring 24: Remove Arrowhead Antipattern【重构第二十四式:去除复杂的嵌套条件判断】

Refactoring 25: Introduce Design By Contract checks【重构第二十五式:引入契约式设计验证】

Refactoring 26: Remove Double Negative【重构第二十六式:消除双重否定】

Refactoring 27: Remove God Classes【重构第二十七式:去除上帝类】

Refactoring 28: Rename boolean method【重构第二十八式:重命名布尔方法】

Refactoring 29: Remove Middle Man【重构第二十九式:去除中间人】

Refactoring 30: Return ASAP【重构第三十式:尽快返回】

Refactoring 31: Replace conditional with Polymorphism【重构第三十一式:用多态取代条件语句】

在英文原文中提供了C#版的重构实例,对重构手段的描述较为精简,Sunny将这些实例都改为了Java版本,并结合个人理解对实例代码和重构描述进行了适当的补充和完善。在本系列文章写作过程中,参考了麒麟.NET的翻译版本《31天重构速成 :你必须知道的重构技巧》以及圣殿骑士(Knights Warrior)《31天重构学习笔记》,在此表示感谢!

----------------------------------------------------------------------------------------------------------------------------------------

重构第一式:封装集合 (Refactoring 1: Encapsulate Collection)

我们知道,对属性和方法的封装可以通过设置它们的可见性来实现,但是对于集合,如何进行封装呢?

本重构提供了一种向类的使用者(客户端)隐藏类中集合的方法,既可以让客户类能够访问到集合中的元素,但是又不让客户类直接修改集合的内容,尤其是在原有类的addXXX()方法和removeXXX()方法中还包含一些其他代码逻辑时,如果将集合暴露给其他所以类,且允许这些类来直接修改集合,将导致在addXXX()方法和removeXXX()方法中新增业务逻辑失效。

下面举一个例子来加以说明:

【重构实例】

电子商务网站通常会有订单管理功能,用户可以查看每张订单(Order)的详情,也可以根据需要添加和删除订单中的订单项(OrderItem)。因此在Order类中定义了一个集合用于存储多个OrderItem。

重构之前的代码片段如下:

[java] view plaincopy

  1. package sunny.refactoring.one.before;
  2. import java.util.Collection;
  3. import java.util.ArrayList;
  4. //订单类
  5. class Order {
  6. private double orderTotal; //订单总金额
  7. private Collection<OrderItem> orderItems; //集合对象,存储一个订单中的所有订单项
  8. public Order() {
  9. this.orderItems = new ArrayList<OrderItem>();
  10. }
  11. //返回订单项集合
  12. public Collection<OrderItem> getOrderItems() {
  13. return this.orderItems;
  14. }
  15. //返回订单总金额
  16. public double getOrderTotal() {
  17. return this.orderTotal;
  18. }
  19. //增加订单项,同时增加订单总金额
  20. public void addOrderItem(OrderItem orderItem) {
  21. this.orderTotal += orderItem.getTotalPrice();
  22. orderItems.add(orderItem);
  23. }
  24. //删除订单项,同时减少订单总金额
  25. public void removeOrderItem(OrderItem orderItem) {
  26. this.orderTotal -= orderItem.getTotalPrice();
  27. orderItems.remove(orderItem);
  28. }
  29. }
  30. //订单项类,省略了很多属性
  31. class OrderItem {
  32. private double totalPrice; //订单项商品总价格
  33. public OrderItem() {
  34. }
  35. public OrderItem(double totalPrice) {
  36. this.totalPrice = totalPrice;
  37. }
  38. public void setTotalPrice(double totalPrice) {
  39. this.totalPrice = totalPrice;
  40. }
  41. public double getTotalPrice() {
  42. return this.totalPrice;
  43. }
  44. }
  45. class Client {
  46. public static void main(String args[]) {
  47. OrderItem orderItem1 = new OrderItem(116.00);
  48. OrderItem orderItem2 = new OrderItem(234.00);
  49. OrderItem orderItem3 = new OrderItem(58.00);
  50. Order order = new Order();
  51. order.addOrderItem(orderItem1);
  52. order.addOrderItem(orderItem2);
  53. order.addOrderItem(orderItem3);
  54. //获取订单类中的订单项集合
  55. Collection<OrderItem> orderItems = order.getOrderItems();
  56. System.out.print("订单中各订单项的价格分别为:");
  57. for (Object obj : orderItems) {
  58. System.out.print(((OrderItem)obj).getTotalPrice() + ",");
  59. }
  60. System.out.println("订单总金额为" + order.getOrderTotal());
  61. //通过订单项集合对象的add()方法增加新订单
  62. orderItems.add(new OrderItem(100.00));
  63. System.out.print("订单中各订单项的价格分别为:");
  64. for (Object obj : orderItems) {
  65. System.out.print(((OrderItem)obj).getTotalPrice() + ",");
  66. }
  67. System.out.println("增加新项后订单总金额为" + order.getOrderTotal());
  68. }
  69. }

输出结果如下:


订单中各订单项的价格分别为:116.0,234.0,58.0,订单总金额为408.0

订单中各订单项的价格分别为:116.0,234.0,58.0,100.0,增加新项后订单总金额为408.0

不难发现,第二句输出结果是有问题的,在增加了新项后订单的总金额居然没有发生改变,还是408.0,但是新项却又能够增加成功。原因很简单,因为返回了Collection类型的订单项集合对象,可以直接使用在Collection接口中声明的add()方法来增加元素,而绕过了在Order类的addOrderItem()方法中统计订单总金额的代码,导致订单项增加成功,但是总金额并没有变化。

在此,客户端只需要遍历访问一个集合对象中的元素,而此时却提供了一个Collection集合对象,它具有对集合的所有操作,这将给程序带来很多隐患,因为使用Order类的用户并不知道在Order类的addOrderItem()方法中还有一些额外的代码,而会习惯性地使用Collection提供的add()方法增加元素。面对这种情况,最好的做法当然是重构。

如何重构?

我们需要对存储订单项的集合对象orderItems进行封装,只允许客户端遍历该集合中的元素,而不允许客户端修改集合中的元素,所有的修改都只能通过Order类统一进行。

下面提供两种重构方案:

重构方案一:将Collection改成Iterable,因为在java.lang. Iterable接口中只提供了一个返回迭代器Iterator对象的iterator()方法,没有提供add()、remove()等修改成员的方法。因此,只能遍历集合中的元素,而不能对集合进行修改,这不正是我们想看到的吗?

代码片段如下(考虑到篇幅,省略了一些相同的代码):

[java] view plaincopy

  1. package sunny.refactoring.one.after;
  2. ……
  3. class Order {
  4. ……
  5. //将getOrderItems()的返回类型改为Iterable
  6. public Iterable<OrderItem> getOrderItems() {
  7. return this.orderItems;
  8. }
  9. ……
  10. }
  11. class OrderItem {
  12. ……
  13. }
  14. class Client {
  15. public static void main(String args[]) {
  16. OrderItem orderItem1 = new OrderItem(116.00);
  17. OrderItem orderItem2 = new OrderItem(234.00);
  18. OrderItem orderItem3 = new OrderItem(58.00);
  19. Order order = new Order();
  20. order.addOrderItem(orderItem1);
  21. order.addOrderItem(orderItem2);
  22. order.addOrderItem(orderItem3);
  23. //获取Iterable<OrderItem>类型的订单项集合对象
  24. Iterable<OrderItem> orderItems = order.getOrderItems();
  25. Iterator<OrderItem> iterator = orderItems.iterator();
  26. System.out.print("订单中各订单项的价格分别为:");
  27. while(iterator.hasNext()) {
  28. System.out.print(((OrderItem)iterator.next()).getTotalPrice() + ",");
  29. }
  30. System.out.println("订单总金额为" + order.getOrderTotal());
  31. //无法访问Order中的集合,Iterable没有提供add()方法,只能通过Order的addOrderItem()方法增加新元素
  32. order.addOrderItem(new OrderItem(100.00));
  33. Iterator<OrderItem> iteratorNew = orderItems.iterator();
  34. System.out.print("订单中各订单项的价格分别为:");
  35. while(iteratorNew.hasNext()) {
  36. System.out.print(((OrderItem)iteratorNew.next()).getTotalPrice() + ",");
  37. }
  38. System.out.println("增加新项后订单总金额为" + order.getOrderTotal());
  39. }
  40. }

输出结果如下:


订单中各订单项的价格分别为:116.0,234.0,58.0,订单总金额为408.0

订单中各订单项的价格分别为:116.0,234.0,58.0,100.0,增加新项后订单总金额为508.0

       

       重构方法二:将getOrderItemsIterator()方法的返回类型改为Iterator<OrderItem>,不直接返回集合对象,而是返回遍历集合对象的迭代器,客户端使用迭代器来遍历集合而不能直接操作集合中的元素。

代码片段如下:

[java] view plaincopy

  1. package sunny.refactoring.one.after;
  2. ……
  3. import java.util.Iterator;
  4. ……
  5. class Order {
  6. ……
  7. //返回遍历orderItems对象的迭代器
  8. public Iterator<OrderItem> getOrderItemsIterator() {
  9. return orderItems.iterator();
  10. }
  11. ……
  12. }
  13. class OrderItem {
  14. ……
  15. }
  16. class Client {
  17. public static void main(String args[]) {
  18. OrderItem orderItem1 = new OrderItem(116.00);
  19. OrderItem orderItem2 = new OrderItem(234.00);
  20. OrderItem orderItem3 = new OrderItem(58.00);
  21. Order order = new Order();
  22. order.addOrderItem(orderItem1);
  23. order.addOrderItem(orderItem2);
  24. order.addOrderItem(orderItem3);
  25. //获取遍历订单项集合对象的迭代器
  26. Iterator<OrderItem> iterator = order.getOrderItemsIterator();
  27. System.out.print("订单中各订单项的价格分别为:");
  28. while(iterator.hasNext()) {
  29. System.out.print(((OrderItem)iterator.next()).getTotalPrice() + ",");
  30. }
  31. System.out.println("订单总金额为" + order.getOrderTotal());
  32. //无法访问Order中的集合,只能通过Order的addOrderItem()方法增加新元素
  33. order.addOrderItem(new OrderItem(100.00));
  34. Iterator<OrderItem> iteratorNew = order.getOrderItemsIterator();
  35. System.out.print("订单中各订单项的价格分别为:");
  36. while(iteratorNew.hasNext()) {
  37. System.out.print(((OrderItem)iteratorNew.next()).getTotalPrice() + ",");
  38. }
  39. System.out.println("订单总金额为" + order.getOrderTotal());
  40. }
  41. }

输出结果如下:


订单中各订单项的价格分别为:116.0,234.0,58.0,订单总金额为408.0

订单中各订单项的价格分别为:116.0,234.0,58.0,100.0,订单总金额为508.0

上述两种重构手段都可以防止客户端直接调用集合类的那些修改集合对象的方法(如add()和remove()等等),无论是返回Iterable类型的对象还是返回Iterator类型的对象,客户端都只能遍历集合,而不能改变集合,从而达到了封装集合的目的。

重构心得

说实话,Sunny觉得该重构用得并不是特别广泛(与其他使用更为频繁的重构手段相比),但是这种封装的思想很重要,让客户端“能够看到该看到的,不该看的一定看不到”。我们在设计和实现类时,一定要多思考每一个属性和方法以及类本身的封装性,这样可以减少一些将来使用上的不便。实质上,迭代器模式引入的目的之一就是为了更好地实现聚合对象的封装性,聚合对象负责存储数据,而遍历数据的职责交给迭代器来完成,增加和修改遍历方法无须修改原有聚合对象,用户也不能通过迭代器来修改聚合对象中的元素,而仅仅只是使用这些元素。封装本身就是一种很重要的编程技巧。

【Java重构系列】重构31式之封装集合

时间: 2024-10-22 06:09:21

【Java重构系列】重构31式之封装集合的相关文章

【Java重构系列】重构31式之搬移方法

重构第二式:搬移方法 (Refactoring 2: Move Method) 毋容置疑,搬移方法(Move Method)应该是最常用的重构手段之一,正因为太常用而且较为简单,以至于很多人并不认为它是一种很有价值的重构,但事实并非如此,在最初的代码诞生之后,有些方法可能会被放在一些不合适的地方,例如,一个方法被其他类使用比在它所在的类中的使用还要频繁或者一个方法本身就不应该放在某个类中时,我们应该考虑将它移到更合适的地方.搬移方法,顾名思义就是将方法搬移至合适的位置,如将方法搬移到更频繁地使用

小酌重构系列[24]&mdash;&mdash;封装集合

概述 当方法返回类型或属性类型为集合时,有些开发者会千篇一律地使用IList<T>集合.然而IList<T>具有集合的所有操作,这意味着调用者不仅可以读取集合信息,还能够修改集合.业务需求本来只是为调用者提供一个可读的集合,例如数据的查询和展示,但当方法返回IList<T>时,无疑隐式地开放了集合可写的权限.此时,我们无法阻止调用者篡改集合元素. 注意:将属性设定为IList<T>类型时,即使声明为只读的,我们仍然无法避免集合元素的篡改.例如public I

【SSH进阶之路】一步步重构容器实现Spring框架——彻底封装,实现简单灵活的Spring框架(十一)

目录 [SSH进阶之路]一步步重构容器实现Spring框架--从一个简单的容器开始(八) [SSH进阶之路]一步步重构容器实现Spring框架--解决容器对组件的"侵入式"管理的两种方案--主动查找和控制反转(九) [SSH进阶之路]一步步重构容器实现Spring框架--配置文件+反射实现IoC容器(十) [SSH进阶之路]一步步重构容器实现Spring框架--彻底封装,实现简单灵活的Spring框架(十一) 博文[SSH进阶之路]一步步重构容器实现Spring框架--从一个简单的容器

小酌重构系列目录汇总

为了方便大家阅读这个系列的文章,我弄了个目录汇总. 方法.字段重构 移动方法 (2016-04-24) 提取方法.提取方法对象 (2016-04-26) 方法.字段的提升和降低 (2016-05-01) 分解方法 (2016-05-02) 为布尔方法命名 (2016-05-03) 引入对象参数 (2016-05-04) 类.接口重构 使用委派代替继承 (2016-05-07) 提取接口 (2016-05-08) 解除依赖 (2016-05-09) 分离职责 (2016-05-11) 提取基类.提

小酌重构系列[19]——分解大括号

概述 if else, for, while等是程序中最常用的语句,这些语句有一个共同点——它们的逻辑都封装在一对“{}”包围的代码块中.在实现复杂的业务逻辑时,会较多地用到这些语句,可能会形成多层的代码嵌套.代码的嵌套层数越大,代码的缩进层次就越深,这将会降低代码的可读性.如下图所示,如果我们想理解绿色if代码块的逻辑,需要先了解前3个代码块是如何工作的. N层嵌套的代码不仅可读性差,也难以维护.当需要变更某一层的代码时,因前后层次的逻辑制约,很容易改出有问题的代码.本文要讲的“分解大括号”策

小酌重构系列[8]&mdash;&mdash;提取接口

前言 世间唯一"不变"的是"变化"本身,这句话同样适用于软件设计和开发.在软件系统中,模块(类.方法)应该依赖于抽象,而不应该依赖于实现. 当需求发生"变化"时,如果模块(类.方法)依赖于具体实现,具体实现也需要修改:如果模块(类.方法)依赖于接口,则无需修改现有实现,而是基于接口扩展新的实现. 面向实现?面向接口? 接口可以被复用,但接口的实现却不一定能被复用. 面向实现编程,意味着软件的模块(类.方法)之间的耦合性非常高,每次遭遇"

移动端重构系列6——图标

移动端重构系列-mobile 本系列文章,如果没有特别说明,兼容安卓4.0.4+ 这里我们把图标分为三种:背景图片,直接绘制,@font-face.如无特殊情况,图标的标签采用i标签 背景图片 首先我们会选择sprite形式,把所有的图标都放在一个大图中,然后考虑到retina屏,所以我们的图标应该设计为实际大小的2倍,然后设置background-size为实际大小.以下面的msg icon为例: 图中的每个icon大小为24px,实际应用时,我们是以12px来使用的: %icon-msg{

移动端重构系列1——新建空白页面

移动端重构系列-mobile 本系列文章,如果没有特别说明,兼容安卓4.0.4+,测试demo html5文档申明 <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Document</title> </head> <body> </body> </html> meta标签 <meta nam

小酌重构系列[14]——使用多态代替条件判断

概述 有时候你可能会在条件判断中,根据不同的对象类型(通常是基类的一系列子类,或接口的一系列实现),提供相应的逻辑和算法.当出现大量类型检查和判断时,if else(或switch)语句的体积会比较臃肿,这无疑降低了代码的可读性.另外,if else(或switch)本身就是一个“变化点”,当需要扩展新的对象类型时,我们不得不追加if else(或switch)语句块,以及相应的逻辑,这无疑降低了程序的可扩展性,也违反了面向对象的OCP原则. 基于这种场景,我们可以考虑使用“多态”来代替冗长的条