(三)封装与结构优化:让代码更优雅

代码重构专题文章

代码重构精要:提升代码品质的艺术(汇总篇)

(一)代码匠心:重构之道,化腐朽为神奇

(二)重构的艺术:精进代码的第一组基本功

(三)封装与结构优化:让代码更优雅

(四)优雅重构:洞悉“搬移特性”的艺术与实践

(五)数据重构的艺术:优化你的代码结构与可读性

(六)重构的艺术:简化复杂条件逻辑的秘诀

(七)API 重构的艺术:打造优雅、可维护的 API

(八)掌握继承的艺术:重构之路,化繁为简

七、封装与结构优化:让代码更优雅

0. 前言

在软件开发的世界里,代码不仅仅是实现功能的工具,更是一门艺术。随着项目的迭代和需求的演变,代码的复杂度会逐渐攀升,维护成本也水涨船高。这时,重构就成了我们手中最重要的工具之一。它不是为了增加新功能,而是为了改善既有代码的设计,让代码更易读、易懂、易维护。

本篇博客将深入探讨一系列封装与结构优化的重构手法,它们将帮助你将臃肿、耦合的代码变得清晰、内聚,从而提升整体的软件质量。封装是面向对象编程的基石,它不仅隐藏了实现细节,更提供了数据保护和清晰的接口。在重构的语境下,封装更是贯穿始终的核心思想,它帮助我们理清代码的职责,构建更加健壮的系统。

1. 封装记录(Encapsulate Record)/ 以数据类取代记录(Replace Record with Data Class)

核心思想: 对象的用户不应该需要关心数据的底层存储细节,也不必追究数据计算的过程。通过将原始的记录结构转换为数据类,我们可以将数据和操作数据的方法封装在一起,提供一个更高级别的抽象。

为什么要这么做?

  • 隐藏实现细节: 客户端只需与数据类交互,无需了解其内部如何存储数据。
  • 集中业务逻辑: 相关的业务逻辑(如校验、计算)可以集中在数据类中,而不是散落在各处。
  • 便于字段改名: 当需要修改字段名称时,只需要修改数据类内部,而不会影响到外部使用者。
  • 更强的类型安全: 数据类提供了明确的类型,避免了因使用原始记录(如字典、元组)而导致的类型错误。

示例: 假设我们有一个表示订单的字典,重构后可以将其转换为一个 Order 数据类。

Java 代码阐述:

重构前 (使用原始记录 - 假设为 Map 或简单的数据结构):

import java.util.HashMap;
import java.util.Map;

public class OrderProcessor_Before {
    public double calculateTotal(Map<String, Object> orderRecord) {
        String item = (String) orderRecord.get("item");
        int quantity = (int) orderRecord.get("quantity");
        double unitPrice = (double) orderRecord.get("unitPrice");

        System.out.println("Processing order for item: " + item);
        return quantity * unitPrice;
    }

    public static void main(String[] args) {
        Map<String, Object> order1 = new HashMap<>();
        order1.put("item", "Laptop");
        order1.put("quantity", 2);
        order1.put("unitPrice", 1200.0);

        OrderProcessor_Before processor = new OrderProcessor_Before();
        double total = processor.calculateTotal(order1);
        System.out.println("Order total: " + total);

        // 如果字段名改变,所有使用的地方都需要修改
        // order1.put("productName", "Mouse"); // 外部使用者需要知道内部字段名
    }
}

重构后 (以数据类取代记录):

public class Order {
    private String item;
    private int quantity;
    private double unitPrice;

    public Order(String item, int quantity, double unitPrice) {
        this.item = item;
        this.quantity = quantity;
        this.unitPrice = unitPrice;
    }

    // 提供 getter 方法,按需可以提供 setter 方法
    public String getItem() {
        return item;
    }

    public int getQuantity() {
        return quantity;
    }

    public double getUnitPrice() {
        return unitPrice;
    }

    // 可以在数据类中封装相关业务逻辑
    public double calculateTotal() {
        return this.quantity * this.unitPrice;
    }

    // 可以有其他业务逻辑,例如打印订单详情
    public void printDetails() {
        System.out.println("Item: " + item + ", Quantity: " + quantity + ", Unit Price: " + unitPrice);
    }
}

public class OrderProcessor_After {
    public double processOrder(Order order) {
        System.out.println("Processing order for item: " + order.getItem());
        order.printDetails(); // 调用Order类中的业务逻辑
        return order.calculateTotal(); // 调用Order类中的业务逻辑
    }

    public static void main(String[] args) {
        Order order1 = new Order("Laptop", 2, 1200.0);

        OrderProcessor_After processor = new OrderProcessor_After();
        double total = processor.processOrder(order1);
        System.out.println("Order total: " + total);

        // 如果 Order 内部的字段名改变 (例如 item 改为 productName),
        // 外部使用者只需要通过 getter 方法访问,无需关心内部字段名变化。
        // order1.setProductName("Mouse"); // 即使有setter,也通过方法名访问
    }
}

阐述: 重构后,Order 类封装了订单的所有数据和计算总价的逻辑。OrderProcessor_After 不再直接操作原始数据结构,而是与 Order 对象交互。这使得代码更具可读性、类型安全,并且易于修改和扩展。

2. 封装集合(Encapsulate Collection)

核心思想: 程序中的所有可变数据都应该被封装起来。这使得我们能够清晰地追踪数据被修改的地点和方式,从而在需要更改数据结构时变得更加方便。关键在于,不要让集合的取值函数直接返回原始集合对象,以避免客户端意外地修改集合内容。

为什么要这么做?

  • 防止外部修改: 如果直接返回原始集合,外部代码可能会在不经意间修改集合,导致难以追踪的副作用。
  • 控制访问: 我们可以为集合提供受控的访问方式,例如只允许添加或删除特定元素,而不允许清空整个集合。
  • 改变内部实现不影响外部: 如果未来需要更换集合的底层实现(例如从 ArrayList 切换到 LinkedList),外部代码无需改动。

如何操作?
最常见的做法是,为集合提供一个取值函数,但令其返回一个集合的副本。这样,即使客户端修改了副本,也不会影响到原始集合。此外,还可以提供专门的添加、删除方法来操作集合。

我的经验: 总的来讲,我觉得对集合保持适度的审慎是非常有益的。我宁愿多复制一份数据(或提供受控的访问接口),也不愿去调试因意外修改集合招致的错误。请记住,修改操作并不总是显而易见的,有时一个看似无害的迭代也可能导致集合被修改。

Java 代码阐述:

重构前 (直接返回内部集合):

import java.util.ArrayList;
import java.util.List;

public class CourseCatalog_Before {
    private List<String> courses = new ArrayList<>();

    public CourseCatalog_Before() {
        courses.add("Math");
        courses.add("Physics");
    }

    public List<String> getCourses() { // 直接返回内部集合
        return courses;
    }

    public static void main(String[] args) {
        CourseCatalog_Before catalog = new CourseCatalog_Before();
        List<String> studentCourses = catalog.getCourses();
        System.out.println("Original courses: " + studentCourses); // [Math, Physics]

        // 客户端直接修改了内部集合!
        studentCourses.add("Chemistry");
        System.out.println("Modified student courses: " + studentCourses); // [Math, Physics, Chemistry]
        System.out.println("Catalog's courses after modification: " + catalog.getCourses()); // [Math, Physics, Chemistry]
        // 这可能导致意料之外的问题,因为 catalog 的内部状态被外部代码改变了
    }
}

重构后 (封装集合,返回副本或提供修改方法):

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class CourseCatalog_After {
    private List<String> courses = new ArrayList<>();

    public CourseCatalog_After() {
        courses.add("Math");
        courses.add("Physics");
    }

    // 方式一:返回集合的不可修改视图
    public List<String> getCoursesImmutable() {
        return Collections.unmodifiableList(courses);
    }

    // 方式二:返回集合的副本 (更安全,但有性能开销)
    public List<String> getCoursesCopy() {
        return new ArrayList<>(courses);
    }

    // 提供专门的添加方法
    public void addCourse(String courseName) {
        this.courses.add(courseName);
    }

    // 提供专门的删除方法
    public void removeCourse(String courseName) {
        this.courses.remove(courseName);
    }

    public static void main(String[] args) {
        CourseCatalog_After catalog = new CourseCatalog_After();
        System.out.println("Original courses: " + catalog.getCoursesCopy()); // [Math, Physics]

        // 尝试通过副本修改,不会影响内部集合
        List<String> studentCoursesCopy = catalog.getCoursesCopy();
        studentCoursesCopy.add("Chemistry");
        System.out.println("Modified student courses copy: " + studentCoursesCopy); // [Math, Physics, Chemistry]
        System.out.println("Catalog's courses after copy modification: " + catalog.getCoursesCopy()); // [Math, Physics]

        // 尝试通过不可修改视图修改,会抛出异常
        List<String> immutableCourses = catalog.getCoursesImmutable();
        try {
            immutableCourses.add("Biology"); // UnsupportedOperationException
        } catch (UnsupportedOperationException e) {
            System.out.println("Cannot modify immutable list: " + e.getMessage());
        }

        // 应该通过提供的API修改集合
        catalog.addCourse("Biology");
        System.out.println("Catalog's courses after adding Biology: " + catalog.getCoursesCopy()); // [Math, Physics, Biology]
    }
}

阐述: 重构后,CourseCatalog_After 提供了 getCoursesImmutable()getCoursesCopy() 方法来访问课程列表。getCoursesImmutable() 返回一个不可修改的视图,任何尝试修改都会抛出异常。getCoursesCopy() 返回一个新列表,即使被修改也不会影响 CourseCatalog_After 内部的状态。同时,提供了 addCourseremoveCourse 方法来受控地修改内部集合。这大大增强了数据完整性和安全性。

3. 以对象取代基本类型(Replace Primitive with Object)/ 以对象取代数据值(Replace Data Value with Object)/ 以类取代类型码(Replace Type Code with Class)

核心思想: 一旦发现对某个数据的操作不仅仅局限于简单的打印或比较时,就应该考虑为它创建一个新的类来封装它。

为什么要这么做?

  • 提升表达力: 一个自定义对象比一个简单的基本类型(如字符串、整数)更能清晰地表达其含义和业务意图。
  • 集中行为: 围绕这个数据的业务逻辑(如校验、格式化、计算)可以集中到新类中,避免散落在各处形成“贫血模型”。
  • 避免魔术字符串/数字: 类型码(如用整数表示状态)常导致代码难以理解和维护。用类取代类型码,能提供更强的类型安全和更好的可读性。
  • 为未来扩展留出空间: 即使一开始这个类只是简单地包装了基本类型,但一旦类有了,日后添加的业务逻辑就有了一个明确的归属地。

示例: 将一个表示金额的 int 替换为 Money 对象,可以包含货币类型和汇率转换等逻辑。将表示订单状态的 int 替换为 OrderStatus 类,每个状态都是一个具体的对象。

Java 代码阐述:

重构前 (使用基本类型和类型码):

public class Product_Before {
    private String name;
    private double price; // 使用基本类型 double 表示价格
    private int discountType; // 使用 int 作为类型码:0=无折扣, 1=VIP折扣, 2=节日折扣

    public static final int NO_DISCOUNT = 0;
    public static final int VIP_DISCOUNT = 1;
    public static final int HOLIDAY_DISCOUNT = 2;

    public Product_Before(String name, double price, int discountType) {
        this.name = name;
        this.price = price;
        this.discountType = discountType;
    }

    public double getDiscountedPrice() {
        if (discountType == VIP_DISCOUNT) {
            return price * 0.9; // 9折
        } else if (discountType == HOLIDAY_DISCOUNT) {
            return price * 0.8; // 8折
        } else {
            return price; // 无折扣
        }
    }

    public static void main(String[] args) {
        Product_Before product1 = new Product_Before("Book", 50.0, NO_DISCOUNT);
        System.out.println("Book price: " + product1.getDiscountedPrice()); // 50.0

        Product_Before product2 = new Product_Before("Laptop", 1000.0, VIP_DISCOUNT);
        System.out.println("Laptop price (VIP): " + product2.getDiscountedPrice()); // 900.0

        // 问题:
        // 1. discountType 是一个 int,无法表达其含义。
        // 2. 外部代码可能传入无效的 int 值。
        // 3. 折扣计算逻辑散布在各处。
    }
}

重构后 (以对象取代基本类型 / 以类取代类型码):

// 1. 以对象取代基本类型:Money 类
class Money {
    private double amount;
    private String currency; // 可以进一步扩展为 Currency 对象

    public Money(double amount, String currency) {
        // 可以在这里添加校验逻辑,例如金额不能为负
        if (amount < 0) {
            throw new IllegalArgumentException("Amount cannot be negative");
        }
        this.amount = amount;
        this.currency = currency;
    }

    public double getAmount() {
        return amount;
    }

    public String getCurrency() {
        return currency;
    }

    public Money applyDiscount(double factor) {
        return new Money(amount * factor, currency);
    }

    @Override
    public String toString() {
        return String.format("%.2f %s", amount, currency);
    }
}

// 2. 以类取代类型码:DiscountType 类 (使用枚举更佳)
enum DiscountType { // 使用枚举是取代类型码的最佳实践
    NONE {
        @Override
        public double getDiscountFactor() {
            return 1.0;
        }
    },
    VIP {
        @Override
        public double getDiscountFactor() {
            return 0.9; // 9折
        }
    },
    HOLIDAY {
        @Override
        public double getDiscountFactor() {
            return 0.8; // 8折
        }
    };

    public abstract double getDiscountFactor(); // 定义抽象方法,让每个枚举实例实现自己的逻辑
}

public class Product_After {
    private String name;
    private Money price; // 使用 Money 对象表示价格
    private DiscountType discountType; // 使用 DiscountType 枚举表示折扣类型

    public Product_After(String name, Money price, DiscountType discountType) {
        this.name = name;
        this.price = price;
        this.discountType = discountType;
    }

    public Money getDiscountedPrice() {
        return price.applyDiscount(discountType.getDiscountFactor());
    }

    public static void main(String[] args) {
        Product_After product1 = new Product_After("Book", new Money(50.0, "USD"), DiscountType.NONE);
        System.out.println("Book price: " + product1.getDiscountedPrice()); // 50.00 USD

        Product_After product2 = new Product_After("Laptop", new Money(1000.0, "USD"), DiscountType.VIP);
        System.out.println("Laptop price (VIP): " + product2.getDiscountedPrice()); // 900.00 USD

        // 优势:
        // 1. Money 对象明确表达了金额的概念,可以封装校验和格式化。
        // 2. DiscountType 枚举提供了强类型安全,避免了无效的类型码,并且将折扣逻辑封装在枚举自身。
        // 3. 代码更具可读性,意图清晰。
    }
}

阐述: 重构后,double priceMoney 对象取代,int discountTypeDiscountType 枚举取代。Money 对象封装了金额和货币信息,并可以添加相关行为(如 applyDiscount)。DiscountType 枚举将不同的折扣类型表示为具名常量,并将各自的折扣因子逻辑封装在枚举实例内部,大大提高了代码的可读性、类型安全性和可维护性。

4. 以查询取代临时变量(Replace Temp with Query)

核心思想: 临时变量的一个作用是保存某段代码的返回值,以便在函数的后面部分使用它。临时变量允许我们引用之前的值,既能解释它的含义,还能避免对代码进行重复计算。但尽管使用变量很方便,很多时候还是值得更进一步,将它们抽取成函数。

为什么要这么做?

  • 提高可读性: 将计算逻辑封装成一个具名函数,能更好地解释这个“临时变量”所代表的含义。
  • 消除重复代码: 如果同一个计算结果在多处被使用,抽取成函数可以避免重复计算。
  • 便于重构: 拥有更多的小函数比一个包含大量临时变量的大函数更容易理解和重构。
  • 测试更方便: 独立的查询函数更容易进行单元测试。

注意事项: 只有当这个临时变量的计算成本不高,并且没有副作用时,才应该考虑将其替换为查询函数。如果计算成本很高,或者存在副作用,那么保留临时变量可能更合适。

Java 代码阐述:

重构前 (使用临时变量):

public class OrderCalculator_Before {
    private double itemPrice;
    private int quantity;

    public OrderCalculator_Before(double itemPrice, int quantity) {
        this.itemPrice = itemPrice;
        this.quantity = quantity;
    }

    public double calculateFinalPrice() {
        // 临时变量 basePrice
        double basePrice = itemPrice * quantity;

        // 临时变量 discount
        double discount;
        if (basePrice > 1000) {
            discount = basePrice * 0.1; // 10% discount for large orders
        } else {
            discount = 0;
        }

        // 临时变量 shippingCost
        double shippingCost = 20.0; // Fixed shipping cost

        // 最终价格的计算使用了多个临时变量
        return basePrice - discount + shippingCost;
    }

    public static void main(String[] args) {
        OrderCalculator_Before order1 = new OrderCalculator_Before(150.0, 5); // basePrice = 750
        System.out.println("Order 1 final price: " + order1.calculateFinalPrice()); // 750 - 0 + 20 = 770

        OrderCalculator_Before order2 = new OrderCalculator_Before(200.0, 6); // basePrice = 1200
        System.out.println("Order 2 final price: " + order2.calculateFinalPrice()); // 1200 - 120 + 20 = 1100
    }
}

重构后 (以查询取代临时变量):

public class OrderCalculator_After {
    private double itemPrice;
    private int quantity;

    public OrderCalculator_After(double itemPrice, int quantity) {
        this.itemPrice = itemPrice;
        this.quantity = quantity;
    }

    // 将 basePrice 抽取为查询方法
    private double getBasePrice() {
        return itemPrice * quantity;
    }

    // 将 discount 抽取为查询方法
    private double getDiscount() {
        if (getBasePrice() > 1000) { // 这里再次调用 getBasePrice(),如果计算成本高需要权衡
            return getBasePrice() * 0.1;
        } else {
            return 0;
        }
    }

    // 将 shippingCost 抽取为查询方法 (虽然是常量,但封装起来更清晰)
    private double getShippingCost() {
        return 20.0;
    }

    public double calculateFinalPrice() {
        // 直接调用查询方法,消除了临时变量
        return getBasePrice() - getDiscount() + getShippingCost();
    }

    public static void main(String[] args) {
        OrderCalculator_After order1 = new OrderCalculator_After(150.0, 5);
        System.out.println("Order 1 final price: " + order1.calculateFinalPrice());

        OrderCalculator_After order2 = new OrderCalculator_After(200.0, 6);
        System.out.println("Order 2 final price: " + order2.calculateFinalPrice());
    }
}

阐述: 重构后,basePricediscountshippingCost 这三个临时变量被替换成了私有的查询方法 getBasePrice()getDiscount()getShippingCost()。这使得 calculateFinalPrice() 方法变得更短、更易读,每个部分都由一个有意义的方法名来解释。如果这些查询方法的计算成本很高,或者存在副作用,可以考虑在 calculateFinalPrice() 方法开头将它们的结果存储到私有字段中,但通常情况下,这种重构是值得的。

5. 提炼类(Extract Class)

核心思想: 如果一个类维护了过多的函数和数据,导致它变得庞大且难以理解,这就表明这个类承担了过多的责任。此时,你需要识别哪些部分可以被独立出来,并将它们分离到一个独立的类中。

为什么要这么做?

  • 单一职责原则(SRP): 每个类应该只有一个改变的理由。提炼类有助于将不同职责分离,使每个类都更加内聚。
  • 提高可读性: 较小的类更容易理解和维护。
  • 降低耦合: 分离后的类可以独立演进,减少了类之间的耦合度。
  • 提高复用性: 提炼出的职责有可能在其他地方被复用。

如何操作?

  1. 识别类中的一组相关数据和行为。
  2. 创建一个新的类,将这些数据和行为搬移过去。
  3. 在新旧类之间建立必要的关联(例如,通过引用)。
  4. 调整客户端代码,使其使用新的类。

Java 代码阐述:

重构前 (一个类承担过多职责):
假设有一个 Person 类,它不仅包含个人信息,还包含了电话号码的格式化和存储逻辑。

public class Person_Before {
    private String name;
    private String officeAreaCode;
    private String officeNumber;
    private String homeAreaCode;
    private String homeNumber;

    public Person_Before(String name, String officeAreaCode, String officeNumber, String homeAreaCode, String homeNumber) {
        this.name = name;
        this.officeAreaCode = officeAreaCode;
        this.officeNumber = officeNumber;
        this.homeAreaCode = homeAreaCode;
        this.homeNumber = homeNumber;
    }

    public String getName() {
        return name;
    }

    public String getOfficePhoneNumber() {
        return "(" + officeAreaCode + ") " + officeNumber;
    }

    public String getHomePhoneNumber() {
        return "(" + homeAreaCode + ") " + homeNumber;
    }

    // 假设还有其他关于地址、邮件等信息和方法...
    // 这个类变得臃肿,因为它同时管理个人基本信息和电话号码的细节。

    public static void main(String[] args) {
        Person_Before person = new Person_Before("Alice", "010", "12345678", "020", "87654321");
        System.out.println("Name: " + person.getName());
        System.out.println("Office Phone: " + person.getOfficePhoneNumber());
        System.out.println("Home Phone: " + person.getHomePhoneNumber());
    }
}

重构后 (提炼 PhoneNumber 类):

// 新提炼的 PhoneNumber 类
class PhoneNumber {
    private String areaCode;
    private String number;

    public PhoneNumber(String areaCode, String number) {
        this.areaCode = areaCode;
        this.number = number;
    }

    public String getAreaCode() {
        return areaCode;
    }

    public String getNumber() {
        return number;
    }

    // 将电话号码的格式化逻辑封装在这里
    public String getFullNumber() {
        return "(" + areaCode + ") " + number;
    }
}

public class Person_After {
    private String name;
    private PhoneNumber officePhoneNumber; // 使用 PhoneNumber 对象
    private PhoneNumber homePhoneNumber;   // 使用 PhoneNumber 对象

    public Person_After(String name, String officeAreaCode, String officeNumber, String homeAreaCode, String homeNumber) {
        this.name = name;
        this.officePhoneNumber = new PhoneNumber(officeAreaCode, officeNumber);
        this.homePhoneNumber = new PhoneNumber(homeAreaCode, homeNumber);
    }

    public String getName() {
        return name;
    }

    // 现在 Person 类只委托给 PhoneNumber 对象来获取完整号码
    public String getOfficePhoneNumber() {
        return officePhoneNumber.getFullNumber();
    }

    public String getHomePhoneNumber() {
        return homePhoneNumber.getFullNumber();
    }

    public static void main(String[] args) {
        Person_After person = new Person_After("Alice", "010", "12345678", "020", "87654321");
        System.out.println("Name: " + person.getName());
        System.out.println("Office Phone: " + person.getOfficePhoneNumber());
        System.out.println("Home Phone: " + person.getHomePhoneNumber());

        // 如果电话号码的格式化规则改变,只需修改 PhoneNumber 类,而不需要动 Person 类。
        // PhoneNumber 类也可以在其他需要电话号码的地方复用。
    }
}

阐述: 重构前 Person_Before 类包含了电话号码的区域码、号码以及格式化逻辑。重构后,我们提炼出了一个独立的 PhoneNumber 类,它专门负责管理电话号码的数据和格式化行为。Person_After 类现在只持有 PhoneNumber 对象的引用,并委托它来处理电话号码相关的职责。这使得 Person_After 类更专注于个人基本信息,而 PhoneNumber 类则专注于电话号码的职责,从而实现了单一职责原则,提高了内聚性。

6. 内联类(Inline Class)

核心思想: 内联类正好与提炼类(Extract Class)相反。如果一个类不再承担足够责任,不再有单独存在的理由(这通常是因为此前的重构动作移走了这个类的责任),我就会挑选这一“萎缩类”的最频繁用户(也是一个类),以本手法将“萎缩类”塞进另一个类中。应用这个手法的另一个场景是,我手头有两个类,想重新安排它们肩负的职责,并让它们产生关联。这时我发现先用本手法将它们内联成一个类再用提炼类(Extract Class)去分离其职责会更加简单。这是重新组织代码时常用的做法:有时把相关元素一口气搬移到位更简单,但有时先用内联手法合并各自的上下文,再使用提炼手法再次分离它们会更合适。

为什么要这么做?

  • 简化结构: 消除不必要的类,减少类的数量,从而简化系统结构。
  • 去除不必要的间接性: 如果一个类只是另一个类的简单包装,合并后可以减少一层抽象。

如何操作?

  1. 选择该“萎缩类”的最频繁使用者(也是一个类)。
  2. 将“萎缩类”的所有字段和方法搬移到这个使用者类中。
  3. 删除原来的“萎缩类”。
  4. 更新所有引用了该“萎缩类”的代码,使其直接使用使用者类。

Java 代码阐述:

重构前 (存在一个“萎缩类”):
假设我们有一个 Address 类,但它非常简单,只包含城市和街道,并且只被 Person 类使用,没有复杂的业务逻辑。

// 萎缩类 Address
class Address_Before {
    private String street;
    private String city;

    public Address_Before(String street, String city) {
        this.street = street;
        this.city = city;
    }

    public String getStreet() {
        return street;
    }

    public String getCity() {
        return city;
    }

    public String getFullAddress() {
        return street + ", " + city;
    }
}

public class Customer_Before {
    private String name;
    private Address_Before address; // Customer 持有 Address 对象的引用

    public Customer_Before(String name, String street, String city) {
        this.name = name;
        this.address = new Address_Before(street, city);
    }

    public String getName() {
        return name;
    }

    public String getCustomerAddress() {
        return address.getFullAddress();
    }

    public static void main(String[] args) {
        Customer_Before customer = new Customer_Before("Bob", "Main St", "New York");
        System.out.println("Customer Name: " + customer.getName());
        System.out.println("Customer Address: " + customer.getCustomerAddress());
    }
}

重构后 (内联 Address 类到 Customer 类):

// Address_Before 类被删除

public class Customer_After {
    private String name;
    // 原 Address_Before 的字段直接移入 Customer_After
    private String street;
    private String city;

    public Customer_After(String name, String street, String city) {
        this.name = name;
        this.street = street;
        this.city = city;
    }

    public String getName() {
        return name;
    }

    // 原 Address_Before 的方法直接移入 Customer_After
    public String getCustomerAddress() {
        return street + ", " + city;
    }

    public static void main(String[] args) {
        Customer_After customer = new Customer_After("Bob", "Main St", "New York");
        System.out.println("Customer Name: " + customer.getName());
        System.out.println("Customer Address: " + customer.getCustomerAddress());
    }
}

阐述: 重构前,Address_Before 类只包含简单的 streetcity 字段,以及一个简单的 getFullAddress 方法,且只被 Customer_Before 类使用。它没有独立的复杂行为或与其他类交互。通过内联,我们将 Address_Before 的字段和方法直接合并到 Customer_After 类中,消除了 Address_Before 这个不必要的类,简化了系统结构,减少了对象之间的间接性。

7. 隐藏委托关系(Hide Delegate)

核心思想: 如果某些客户端首先通过服务对象的字段获取到另一个对象(受托类),然后调用后者的函数,那么客户端就必须知晓这一层委托关系。万一受托类的接口发生修改,这种变化会波及所有通过服务对象使用它的客户端。为了降低这种耦合,可以在服务对象上放置一个简单的委托函数,将委托关系隐藏起来。

为什么要这么做?

  • 降低耦合: 客户端不再直接依赖于受托类。即使将来受托类发生变化(例如接口改变或被替换),变化也只会影响到服务对象,而不会直接波及所有客户端。
  • 集中控制: 服务对象可以控制对受托类的访问,甚至在访问前或访问后添加额外的逻辑。

示例: 假设我们有一个 Employee 类,它有一个 Department 字段。客户端代码需要获取员工所在部门的经理。

重构前:

// 受托类 Department
class Department {
    private String name;
    private String manager;

    public Department(String name, String manager) {
        this.name = name;
        this.manager = manager;
    }

    public String getName() {
        return name;
    }

    public String getManager() {
        return manager;
    }

    public void setManager(String manager) {
        this.manager = manager;
    }
}

// 服务对象 Employee
class Employee {
    private String name;
    private Department department;

    public Employee(String name, Department department) {
        this.name = name;
        this.department = department;
    }

    public String getName() {
        return name;
    }

    public Department getDepartment() { // 直接暴露了Department对象
        return department;
    }
}

// 客户端代码
public class ClientBefore {
    public static void main(String[] args) {
        Department sales = new Department("Sales", "John Doe");
        Employee alice = new Employee("Alice", sales);

        // 客户端直接通过 Employee 获取 Department,再获取经理
        String managerName = alice.getDepartment().getManager();
        System.out.println("Alice's manager is: " + managerName);

        // 如果Department的接口改变,客户端也需要修改
        // 例如,如果 getManager() 改名为 getDepartmentHead()
        // 那么这里的 alice.getDepartment().getManager() 就会报错
    }
}

在上面的代码中,ClientBefore 必须知道 Employee 有一个 Department 对象,并且 Department 对象有一个 getManager() 方法。这就意味着 ClientBeforeDepartment 之间存在着直接的耦合。

重构后:
我们可以在 Employee 类中添加一个委托方法来隐藏 Department 对象的存在。

// 受托类 Department (不变)
class Department {
    private String name;
    private String manager;

    public Department(String name, String manager) {
        this.name = name;
        this.manager = manager;
    }

    public String getName() {
        return name;
    }

    public String getManager() {
        return manager;
    }

    public void setManager(String manager) {
        this.manager = manager;
    }
}

// 服务对象 Employee (添加委托方法)
class Employee {
    private String name;
    private Department department;

    public Employee(String name, Department department) {
        this.name = name;
        this.department = department;
    }

    public String getName() {
        return name;
    }

    // 隐藏了对 Department 对象的直接访问
    public String getDepartmentManager() {
        return department.getManager();
    }

    // 可以考虑移除 getDepartment() 方法,如果客户端不再需要直接访问 Department
    // public Department getDepartment() {
    //     return department;
    // }
}

// 客户端代码
public class ClientAfterHideDelegate {
    public static void main(String[] args) {
        Department sales = new Department("Sales", "John Doe");
        Employee alice = new Employee("Alice", sales);

        // 客户端现在直接通过 Employee 获取经理,无需知道 Department
        String managerName = alice.getDepartmentManager();
        System.out.println("Alice's manager is: " + managerName);

        // 如果 Department 的 getManager() 方法改变,只需要修改 Employee.getDepartmentManager()
        // 客户端代码 ClientAfterHideDelegate 无需修改,降低了耦合
    }
}

现在,客户端通过调用 alice.getDepartmentManager() 来获取经理姓名,而无需知道 Employee 内部是如何存储部门信息的,或者部门对象有什么方法。如果 Department 类的 getManager() 方法的名称发生改变,我们只需要修改 Employee 类中的 getDepartmentManager() 方法,而所有客户端代码都无需改动,这大大降低了系统中的耦合度。

8. 移除中间人(Remove Middle Man)

核心思想: 在隐藏委托关系(Hide Delegate)的“动机”一节中,我们谈到了“封装受托对象”的好处。但是,这层封装也是有代价的。每当客户端需要使用受托类的新特性时,你就必须在服务对象中添加一个新的简单委托函数。随着受托类的特性(功能)越来越多,服务类中充斥着大量的转发函数,使它完全变成了一个“中间人”。在这种情况下,就应该让客户直接调用受托类。

为什么要这么做?

  • 去除不必要的间接性: 当中间人提供的价值非常有限,只是一层简单的转发时,直接访问受托对象可以简化代码路径。
  • 提高效率: 减少了一层方法调用。
  • 避免“中间人”模式的滥用: 当一个服务对象仅仅是将所有请求转发给另一个对象时,它实际上并没有提供任何有意义的抽象或逻辑,反而增加了系统的复杂度。

如何判断? 这个味道通常在人们狂热地遵循迪米特法则(Law of Demeter)时悄然出现。迪米特法则主张“只与你的直接朋友对话”,但过度遵循会导致过多的中间人。

示例: 延续上一个例子。假设 Employee 的客户端经常需要访问 Department 的多个方法,而不仅仅是 getManager()

重构前(过度隐藏委托关系):

// 受托类 Department (不变)
class Department {
    private String name;
    private String manager;
    private int employeeCount;

    public Department(String name, String manager, int employeeCount) {
        this.name = name;
        this.manager = manager;
        this.employeeCount = employeeCount;
    }

    public String getName() {
        return name;
    }

    public String getManager() {
        return manager;
    }

    public int getEmployeeCount() {
        return employeeCount;
    }
    // 假设 Department 还有很多其他方法
    public void addEmployee() { employeeCount++; }
    public void removeEmployee() { employeeCount--; }
}

// 服务对象 Employee (过度充当中间人)
class Employee {
    private String name;
    private Department department;

    public Employee(String name, Department department) {
        this.name = name;
        this.department = department;
    }

    public String getName() {
        return name;
    }

    // 委托方法1
    public String getDepartmentManager() {
        return department.getManager();
    }

    // 委托方法2
    public String getDepartmentName() {
        return department.getName();
    }

    // 委托方法3
    public int getDepartmentEmployeeCount() {
        return department.getEmployeeCount();
    }

    // 委托方法4 (为 Department 的 addEmployee() 转发)
    public void addEmployeeToDepartment() {
        department.addEmployee();
    }

    // 想象一下如果 Department 有几十个方法,Employee 就会有几十个这样的转发方法
    // 这使得 Employee 成为了一个臃肿的“中间人”
}

// 客户端代码
public class ClientBeforeRemoveMiddleMan {
    public static void main(String[] args) {
        Department hr = new Department("HR", "Jane Doe", 15);
        Employee bob = new Employee("Bob", hr);

        System.out.println("Bob's department name: " + bob.getDepartmentName());
        System.out.println("Bob's department manager: " + bob.getDepartmentManager());
        System.out.println("Bob's department employee count: " + bob.getDepartmentEmployeeCount());
        bob.addEmployeeToDepartment();
        System.out.println("After adding employee, count: " + bob.getDepartmentEmployeeCount());
        // 客户端需要 Department 的更多功能时,就需要 Employee 额外添加更多委托方法
    }
}

在这个例子中,Employee 类充当了一个过度的“中间人”。它仅仅是将 Department 的方法简单地转发出去,而没有添加任何自身的业务逻辑。这增加了代码量,也使得 Employee 类变得臃肿且难以维护。

重构后:
让客户端直接访问 Department 对象。

// 受托类 Department (不变)
class Department {
    private String name;
    private String manager;
    private int employeeCount;

    public Department(String name, String manager, int employeeCount) {
        this.name = name;
        this.manager = manager;
        this.employeeCount = employeeCount;
    }

    public String getName() {
        return name;
    }

    public String getManager() {
        return manager;
    }

    public int getEmployeeCount() {
        return employeeCount;
    }

    public void addEmployee() { employeeCount++; }
    public void removeEmployee() { employeeCount--; }
}

// 服务对象 Employee (不再是中间人)
class Employee {
    private String name;
    private Department department; // 直接暴露 Department

    public Employee(String name, Department department) {
        this.name = name;
        this.department = department;
    }

    public String getName() {
        return name;
    }

    // 重新暴露 Department 对象,让客户端直接访问
    public Department getDepartment() {
        return department;
    }
}

// 客户端代码
public class ClientAfterRemoveMiddleMan {
    public static void main(String[] args) {
        Department hr = new Department("HR", "Jane Doe", 15);
        Employee bob = new Employee("Bob", hr);

        // 客户端直接通过 Employee 获取 Department,然后调用 Department 的方法
        Department bobsDepartment = bob.getDepartment();
        System.out.println("Bob's department name: " + bobsDepartment.getName());
        System.out.println("Bob's department manager: " + bobsDepartment.getManager());
        System.out.println("Bob's department employee count: " + bobsDepartment.getEmployeeCount());
        bobsDepartment.addEmployee();
        System.out.println("After adding employee, count: " + bobsDepartment.getEmployeeCount());
        // 客户端可以直接访问 Department 的所有公共方法,无需 Employee 额外转发
    }
}

通过移除中间人,我们简化了 Employee 类的结构,使其职责更清晰。客户端现在直接与 Department 对象交互,避免了不必要的间接层。这种做法的权衡点在于,客户端现在对 Department 类的接口有了直接依赖。何时隐藏何时暴露,需要根据实际情况(如受托类的稳定性、客户端对受托类方法的访问频率和复杂性)来决定。

9. 替换算法(Substitute Algorithm)

核心思想: 如果发现做一件事情可以有更清晰、更简单、更高效的方式,就应该用这种更好的算法取代原有的复杂或低效的算法。

为什么要这么做?

  • 提高可读性: 简单清晰的算法比复杂纠结的算法更容易理解。
  • 提高性能: 替换为更优的算法可以显著提升程序的执行效率。
  • 降低维护成本: 越简单的算法,出错的可能性越小,维护起来也越容易。
  • 修复错误: 旧的算法可能存在bug,替换为新的、经过验证的算法可以修复这些问题。

如何操作?

  1. 找到要替换的算法。
  2. 用新的算法逻辑替换掉旧的算法逻辑。
  3. 运行测试以确保新的算法能够正确工作,并且没有引入新的问题。
  4. 在替换前,最好确保有足够的测试来验证算法的正确性。

示例: 假设我们有一个方法用于查找列表中的特定元素。

重构前(效率较低的查找算法):

import java.util.Arrays;
import java.util.List;

public class AlgorithmBefore {

    /**
     * 在列表中查找指定名称的员工,并返回其ID。
     * 使用线性查找,当列表很大时效率较低。
     */
    public String findEmployeeIdByName(List<Employee> employees, String nameToFind) {
        for (Employee employee : employees) {
            if (employee.getName().equals(nameToFind)) {
                return employee.getId();
            }
        }
        return null; // 未找到
    }

    static class Employee {
        private String id;
        private String name;

        public Employee(String id, String name) {
            this.id = id;
            this.name = name;
        }

        public String getId() {
            return id;
        }

        public String getName() {
            return name;
        }
    }

    public static void main(String[] args) {
        List<Employee> employeeList = Arrays.asList(
            new Employee("E001", "Alice"),
            new Employee("E002", "Bob"),
            new Employee("E003", "Charlie"),
            new Employee("E004", "Alice") // 假设有重复名字
        );

        AlgorithmBefore finder = new AlgorithmBefore();
        String aliceId = finder.findEmployeeIdByName(employeeList, "Alice");
        System.out.println("Alice's ID: " + aliceId); // 可能会返回第一个匹配项的ID
        String zoeId = finder.findEmployeeIdByName(employeeList, "Zoe");
        System.out.println("Zoe's ID: " + zoeId); // null
    }
}

这里的 findEmployeeIdByName 方法使用简单的线性查找。如果 employees 列表非常大,并且查找操作频繁,这种方法效率会比较低。同时,如果列表中存在多个同名员工,它只会返回第一个匹配项的ID,这可能不是我们期望的行为。

重构后(使用更高效或更清晰的算法):

假设我们希望:

  1. 提高查找效率,特别是当列表被排序时。
  2. 或者,如果名称不唯一,我们可能希望明确返回一个列表或者抛出异常。
  3. 如果列表不是特别大,但我们希望使用 Java 8 Stream API 让代码更具函数式风格和表达力。

替换方案一:使用 Stream API 提升表达力(适用于数据量不是极其庞大,且追求代码简洁性)

import java.util.Arrays;
import java.util.List;
import java.util.Optional;

public class AlgorithmAfterStream {

    /**
     * 在列表中查找指定名称的员工,并返回其ID。
     * 使用 Stream API,代码更简洁,表达力更强。
     */
    public Optional<String> findEmployeeIdByName(List<Employee> employees, String nameToFind) {
        return employees.stream()
                        .filter(employee -> employee.getName().equals(nameToFind))
                        .map(Employee::getId)
                        .findFirst(); // 返回第一个匹配的ID,使用Optional包装
    }

    static class Employee {
        private String id;
        private String name;

        public Employee(String id, String name) {
            this.id = id;
            this.name = name;
        }

        public String getId() {
            return id;
        }

        public String getName() {
            return name;
        }
    }

    public static void main(String[] args) {
        List<Employee> employeeList = Arrays.asList(
            new Employee("E001", "Alice"),
            new Employee("E002", "Bob"),
            new Employee("E003", "Charlie"),
            new Employee("E004", "Alice")
        );

        AlgorithmAfterStream finder = new AlgorithmAfterStream();
        Optional<String> aliceId = finder.findEmployeeIdByName(employeeList, "Alice");
        aliceId.ifPresent(id -> System.out.println("Alice's ID: " + id)); // E001
        
        Optional<String> zoeId = finder.findEmployeeIdByName(employeeList, "Zoe");
        System.out.println("Zoe's ID: " + zoeId.orElse("Not Found")); // Not Found
    }
}

替换方案二:使用 Map 优化查找性能(适用于需要频繁按名称查找,且名称可能不唯一但我们只关心第一个的情况,或者希望通过 Map 处理多值)

如果我们希望在后续多次查找时提高效率,可以先将列表转换为一个 Map 结构,尤其是如果名称是唯一的键。如果名称不唯一,Map<String, List<Employee>> 会是更好的选择。

import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;

public class AlgorithmAfterMap {

    private Map<String, Employee> employeeMap; // 假设名称是唯一的,或者我们只关心第一个

    public AlgorithmAfterMap(List<Employee> employees) {
        // 在构造函数中预处理数据,提高后续查找效率
        this.employeeMap = employees.stream()
                                  .collect(Collectors.toMap(
                                      Employee::getName, // 键:员工姓名
                                      e -> e,            // 值:员工对象
                                      (existing, replacement) -> existing // 解决键冲突,保留第一个
                                  ));
    }

    /**
     * 通过Map查找员工ID,效率更高。
     */
    public Optional<String> findEmployeeIdByName(String nameToFind) {
        return Optional.ofNullable(employeeMap.get(nameToFind))
                       .map(Employee::getId);
    }

    static class Employee {
        private String id;
        private String name;

        public Employee(String id, String name) {
            this.id = id;
            this.name = name;
        }

        public String getId() {
            return id;
        }

        public String getName() {
            return name;
        }
    }

    public static void main(String[] args) {
        List<Employee> employeeList = Arrays.asList(
            new Employee("E001", "Alice"),
            new Employee("E002", "Bob"),
            new Employee("E003", "Charlie"),
            new Employee("E004", "Alice") // 这里的Alice会被前一个覆盖,如果用toMap(...,...,(e1,e2)->e1)
        );

        AlgorithmAfterMap finder = new AlgorithmAfterMap(employeeList);
        Optional<String> aliceId = finder.findEmployeeIdByName("Alice");
        aliceId.ifPresent(id -> System.out.println("Alice's ID: " + id)); // E001

        Optional<String> zoeId = finder.findEmployeeIdByName("Zoe");
        System.out.println("Zoe's ID: " + zoeId.orElse("Not Found")); // Not Found
    }
}

在这两种重构方案中,我们都用更现代、更具表达力或更高效的算法替换了原始的线性查找算法。

  • Stream API 方案 提升了代码的可读性和简洁性,并利用 Optional 更好地处理了查找结果可能为空的情况。
  • Map 优化方案 通过预处理数据结构,将多次查找的复杂度从 O(N) 降低到 O(1),显著提高了性能,特别适合需要重复查找的场景。

替换算法的关键在于识别出当前算法的不足(可读性差、效率低、有bug等),然后寻找并应用一个更好的解决方案,并且在替换后通过测试确保新算法的正确性。


参考

《重构:改善既有代码的设计(第二版)》

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值