【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。
重构之前的代码片段如下:
- package sunny.refactoring.one.before;
- import java.util.Collection;
- import java.util.ArrayList;
- //订单类
- class Order {
- private double orderTotal; //订单总金额
- private Collection<OrderItem> orderItems; //集合对象,存储一个订单中的所有订单项
- public Order() {
- this.orderItems = new ArrayList<OrderItem>();
- }
- //返回订单项集合
- public Collection<OrderItem> getOrderItems() {
- return this.orderItems;
- }
- //返回订单总金额
- public double getOrderTotal() {
- return this.orderTotal;
- }
- //增加订单项,同时增加订单总金额
- public void addOrderItem(OrderItem orderItem) {
- this.orderTotal += orderItem.getTotalPrice();
- orderItems.add(orderItem);
- }
- //删除订单项,同时减少订单总金额
- public void removeOrderItem(OrderItem orderItem) {
- this.orderTotal -= orderItem.getTotalPrice();
- orderItems.remove(orderItem);
- }
- }
- //订单项类,省略了很多属性
- class OrderItem {
- private double totalPrice; //订单项商品总价格
- public OrderItem() {
- }
- public OrderItem(double totalPrice) {
- this.totalPrice = totalPrice;
- }
- public void setTotalPrice(double totalPrice) {
- this.totalPrice = totalPrice;
- }
- public double getTotalPrice() {
- return this.totalPrice;
- }
- }
- class Client {
- public static void main(String args[]) {
- OrderItem orderItem1 = new OrderItem(116.00);
- OrderItem orderItem2 = new OrderItem(234.00);
- OrderItem orderItem3 = new OrderItem(58.00);
- Order order = new Order();
- order.addOrderItem(orderItem1);
- order.addOrderItem(orderItem2);
- order.addOrderItem(orderItem3);
- //获取订单类中的订单项集合
- Collection<OrderItem> orderItems = order.getOrderItems();
- System.out.print("订单中各订单项的价格分别为:");
- for (Object obj : orderItems) {
- System.out.print(((OrderItem)obj).getTotalPrice() + ",");
- }
- System.out.println("订单总金额为" + order.getOrderTotal());
- //通过订单项集合对象的add()方法增加新订单
- orderItems.add(new OrderItem(100.00));
- System.out.print("订单中各订单项的价格分别为:");
- for (Object obj : orderItems) {
- System.out.print(((OrderItem)obj).getTotalPrice() + ",");
- }
- System.out.println("增加新项后订单总金额为" + order.getOrderTotal());
- }
- }
输出结果如下:
订单中各订单项的价格分别为: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()等修改成员的方法。因此,只能遍历集合中的元素,而不能对集合进行修改,这不正是我们想看到的吗?
代码片段如下(考虑到篇幅,省略了一些相同的代码):
- package sunny.refactoring.one.after;
- ……
- class Order {
- ……
- //将getOrderItems()的返回类型改为Iterable
- public Iterable<OrderItem> getOrderItems() {
- return this.orderItems;
- }
- ……
- }
- class OrderItem {
- ……
- }
- class Client {
- public static void main(String args[]) {
- OrderItem orderItem1 = new OrderItem(116.00);
- OrderItem orderItem2 = new OrderItem(234.00);
- OrderItem orderItem3 = new OrderItem(58.00);
- Order order = new Order();
- order.addOrderItem(orderItem1);
- order.addOrderItem(orderItem2);
- order.addOrderItem(orderItem3);
- //获取Iterable<OrderItem>类型的订单项集合对象
- Iterable<OrderItem> orderItems = order.getOrderItems();
- Iterator<OrderItem> iterator = orderItems.iterator();
- System.out.print("订单中各订单项的价格分别为:");
- while(iterator.hasNext()) {
- System.out.print(((OrderItem)iterator.next()).getTotalPrice() + ",");
- }
- System.out.println("订单总金额为" + order.getOrderTotal());
- //无法访问Order中的集合,Iterable没有提供add()方法,只能通过Order的addOrderItem()方法增加新元素
- order.addOrderItem(new OrderItem(100.00));
- Iterator<OrderItem> iteratorNew = orderItems.iterator();
- System.out.print("订单中各订单项的价格分别为:");
- while(iteratorNew.hasNext()) {
- System.out.print(((OrderItem)iteratorNew.next()).getTotalPrice() + ",");
- }
- System.out.println("增加新项后订单总金额为" + order.getOrderTotal());
- }
- }
输出结果如下:
订单中各订单项的价格分别为:116.0,234.0,58.0,订单总金额为408.0 订单中各订单项的价格分别为:116.0,234.0,58.0,100.0,增加新项后订单总金额为508.0 |
重构方法二:将getOrderItemsIterator()方法的返回类型改为Iterator<OrderItem>,不直接返回集合对象,而是返回遍历集合对象的迭代器,客户端使用迭代器来遍历集合而不能直接操作集合中的元素。
代码片段如下:
- package sunny.refactoring.one.after;
- ……
- import java.util.Iterator;
- ……
- class Order {
- ……
- //返回遍历orderItems对象的迭代器
- public Iterator<OrderItem> getOrderItemsIterator() {
- return orderItems.iterator();
- }
- ……
- }
- class OrderItem {
- ……
- }
- class Client {
- public static void main(String args[]) {
- OrderItem orderItem1 = new OrderItem(116.00);
- OrderItem orderItem2 = new OrderItem(234.00);
- OrderItem orderItem3 = new OrderItem(58.00);
- Order order = new Order();
- order.addOrderItem(orderItem1);
- order.addOrderItem(orderItem2);
- order.addOrderItem(orderItem3);
- //获取遍历订单项集合对象的迭代器
- Iterator<OrderItem> iterator = order.getOrderItemsIterator();
- System.out.print("订单中各订单项的价格分别为:");
- while(iterator.hasNext()) {
- System.out.print(((OrderItem)iterator.next()).getTotalPrice() + ",");
- }
- System.out.println("订单总金额为" + order.getOrderTotal());
- //无法访问Order中的集合,只能通过Order的addOrderItem()方法增加新元素
- order.addOrderItem(new OrderItem(100.00));
- Iterator<OrderItem> iteratorNew = order.getOrderItemsIterator();
- System.out.print("订单中各订单项的价格分别为:");
- while(iteratorNew.hasNext()) {
- System.out.print(((OrderItem)iteratorNew.next()).getTotalPrice() + ",");
- }
- System.out.println("订单总金额为" + order.getOrderTotal());
- }
- }
输出结果如下:
订单中各订单项的价格分别为: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式之封装集合的更多相关文章
- Java基础系列(31)- 可变参数
可变参数 JDK1.5开始,Java支持传递同类型的可变参数给一个方法 在方法声明中,在指定参数类型后加一个省略号(...) 一个方法中只能指定一个可变参数,它必须是方法的最后一个参数.任何普通的参数 ...
- 【SSH进阶之路】一步步重构容器实现Spring框架——彻底封装,实现简单灵活的Spring框架(十一)
文件夹 [SSH进阶之路]一步步重构容器实现Spring框架--从一个简单的容器開始(八) [SSH进阶之路]一步步重构容器实现Spring框架--解决容器对组件的"侵入 ...
- java高并发系列 - 第31天:获取线程执行结果,这6种方法你都知道?
这是java高并发系列第31篇. 环境:jdk1.8. java高并发系列已经学了不少东西了,本篇文章,我们用前面学的知识来实现一个需求: 在一个线程中需要获取其他线程的执行结果,能想到几种方式?各有 ...
- Java并发编程系列-(4) 显式锁与AQS
4 显示锁和AQS 4.1 Lock接口 核心方法 Java在java.util.concurrent.locks包中提供了一系列的显示锁类,其中最基础的就是Lock接口,该接口提供了几个常见的锁相关 ...
- Java基础系列1:Java基本类型与封装类型
Java基础系列1:Java基本类型与封装类型 当初学习计算机的时候,教科书中对程序的定义是:程序=数据结构+算法,Java基础系列第一篇就聊聊Java中的数据类型. 本篇聊Java数据类型主要包括两 ...
- 重构第1天:封装集合(Encapsulate Collection)
理解:封装集合就是把集合进行封装,只提供调用者所需要的功能行借口,保证集合的安全性. 详解:在大多的时候,我们没有必要把所有的操作暴露给调用者,只需要把调用者需要的相关操作暴露给他,这种情况中下我们就 ...
- Java 8系列之Stream的基本语法详解
本文转至:https://blog.csdn.net/io_field/article/details/54971761 Stream系列: Java 8系列之Stream的基本语法详解 Java 8 ...
- Java多线程系列
一.参考文献 1.:Java多线程系列目录 (一) 基础篇 01. Java多线程系列--“基础篇”01之 基本概念 02. Java多线程系列--“基础篇”02之 常用的实现多线程的两种方式 03. ...
- 夯实Java基础系列10:深入理解Java中的异常体系
目录 为什么要使用异常 异常基本定义 异常体系 初识异常 异常和错误 异常的处理方式 "不负责任"的throws 纠结的finally throw : JRE也使用的关键字 异常调 ...
随机推荐
- H5小内容(五)
Geolocation(地理定位) 基本内容 地理定位 - 地球的经度和纬度的相交点 实现地理定位的方式 GPS - 美国的,依靠卫星定位 北斗定位 - 纯 ...
- PHP设计模式之适配器模式
将一个类的接口转换成客户希望的另一个接口,适配器模式使得原本的由于接口不兼容而不能一起工作的那些类可以一起工作.应用场景:老代码接口不适应新的接口需求,或者代码很多很乱不便于继续修改,或者使用第三方类 ...
- Poco版信号槽
#include "Poco/BasicEvent.h"#include "Poco/Delegate.h"#include <iostream> ...
- lucene开发序之luke神器
lucene是一款很优秀的全文检索的开源库,目前最新的版本是lucene4.4,关于lucene的历史背景以及发展状况,在这里笔者就不多介绍了,如果你真心想学习lucene,想必在这之前你已经对此作过 ...
- C++顺序容器类总结
主要是vector,deque,list,forward_list,array,string 插入方法: 元素访问: 元素删除: 容器赋值: forward_list有很多特殊的方法 毕竟平时forw ...
- 贴板子系列_1-exgcd
exgcd ll exgcd(ll a,ll b,ll &x,ll &y) { ) { x=;y=;return a; } ll r=exgcd(b,a%b,x,y); ll t=x; ...
- 单片微机原理P0:80C51结构原理
本来我真的不想让51的东西出现在我的博客上的,因为51这种东西真的太low了,学了最多就所谓的垃圾科创利用一下,但是想一下这门课我也要考试,还是写一点东西顺便放博客上吧. 这一系列主要参考<单片 ...
- java 资料收集
java中线程队列BlockingQueue的用法 为什么jdk中把String类设计成final? 深入浅出单实例Singleton设计模式
- Android开源项目发现---ListView篇(持续更新)
资料转载地址:https://github.com/Trinea/android-open-project 1. android-pulltorefresh 一个强大的拉动刷新开源项目,支持各种控件下 ...
- ZOJ3582:Back to the Past(概率DP)
Recently poet Mr. po encountered a serious problem, rumor said some of his early poems are written b ...