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

2023-07-29

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式之封装集合的相关教程结束。