聊一聊代码重构——程序方法和类上的代码实践

代码重构相关内容

聊一聊代码重构——我们为什么要代码重构

聊一聊代码重构——代码中究竟存在哪些坏代码

聊一聊代码重构——关于变量的代码实践

聊一聊代码重构——关于循环逻辑的代码实践

聊一聊代码重构——关于条件表达式的代码实践

聊一聊代码重构——程序方法上的代码实践

聊一聊代码重构——程序方法和类上的代码实践

聊一聊代码重构——存在继承关系类上的代码实践

聊一聊代码重构——封装集合和替换算法的代码实践


使用工厂方法取代构造方法

构造方法的问题

我们使用构造方法来初始化对象时候,我们得到的只能是当前对象。而使用工厂方法替换构造方法,我们可以返回其子类型或者代理类型。这让我们可以通过不同的实现类来进行逻辑实现的变化。

更重要的一点是,构造方法的名称被严格限制,我们无法根据不同的构造方法来分析初始化的用途,但是使用工厂方法,我们可以通过参数名称、或者初始化方法的名称了解到对应方法解释初始化对象的用途。

如何创建构造工厂方法

  1. 新建一个工厂方法,让其调用现有的构造方法。如果需要可以为工厂方法取一个和业务相关的名称。
  2. 修改调用构造方法的代码,改为调用工厂方法。
  3. 进行测试
  4. 如果可以,尽量限制对象初始化方法的可见范围,初始化对象的工作全部交给工厂。

为对象设置工厂方法

class Point {
  private double x;
  private double y;

  private Point(double x, double y) {
    this.x = x;
    this.y = y;
  }

  static Point newCartesianPoint(double x, double y) {
    return new Point(x, y);
  }

  static Point newPolarPoint(double rho, double theta) {
    return new Point(rho * Math.cos(theta), rho * Math.sin(theta));
  }

  public double getX() {
    return x;
  }

  public double getY() {
    return y;
  }
}

以命令对象取代方法

命令对象

有些业务中我们,需要将数据经过多个组合方法完成数据的处理。如果我们将数据看成一个整体,而要经过的方法作为数据内部的一些行为。我们可以创建一个命令对象,我们将原始数据作为命令对象的初始化参数进行对象初始化,通过提供一个执行。在内部包含对所有处理方法的调用。

使用命令对象可以将具体的操作与执行者分离,使得两者相对独立。并且对外我们提供的单独的一个接口,所有的执行序列都在内部完成。如果后续我们希望提供其他的逻辑实现时,我们只需要提供新的命令对象。对于调用者也只是需要修改一个新的命令对象。

并且需要修改的数据作为命令对象共享的字段,在不同处理方法中,不再需要设置繁琐的参数,所有的字段数据在调用方法后被实时更新。

使用命令对象,我们将数据的入口限制在初始化的地方,将数据的修改范围控制在命令对象内部。我们可以在可控的范围内对数据的访问和修改进行监控。

因为命令对象中进行执行的数据都保存在对象上下文中,这样通过命令对象我们可以实现延迟启动或者回滚到某一个状态时的数据功能。

是否可以使用命令对象

所以当我们遇见下面场景可以考虑是否使用命令对象

  1. 需要做一些撤销、合并等业务,命令对象中可以记录之前状态值。
  2. 当需要将方法的执行过程进行延迟或者异步执行时,可以将方法转化为命令对象。
  3. 需要根据数据动态的改变方法执行过程,也可以使用方法转化为命令对象,并在命令对象中实现不同的执行方式。

构建命令对象的过程

  1. 首先创建一个空的类。给类起一个能表达要替换方法作用的名称。
  2. 将被替换方法复制到新的类中。
  3. 分析方法执行时需要的参数,其作为新建类的属性值。
  4. 创建对应的初始化方法。并提供一个执行方法来启动方法流程。

构建命令对象的例子

重构前

class BankAccount {
  private int balance;

  public void deposit(int amount) {
    balance += amount;
  }

  public void withdraw(int amount) {
    balance -= amount;
  }
}

重构后

interface Command {
  void call();
}

class BankAccountCommand implements Command {
  private BankAccount account;
  private int amount;
  private boolean success;

  public BankAccountCommand(BankAccount account, int amount, boolean success) {
    this.account = account;
    this.amount = amount;
    this.success = success;
  }

  public void call() {
    if (success) {
      account.deposit(amount);
    } else {
      account.withdraw(amount);
    }
  }
}

以方法取代命令对象

拆解对象

但是很多时候我仅仅是想执行数据逻辑中的一部分。或者本身整个命令的执行序列并不复杂。但是这个时候却需要构建整个命令对象,这个时候构建命令对象会让整个过程的可读性大大降低。这个时候我们可以将命令对象还原成方法,直接调用其逻辑即可。

拆解命令对象

  1. 首先需要确定要重构的命令方法。
  2. 将命令方法中所有的逻辑提取,作为一个单独的方法。将命令对象初始化的参数作为单独方法的参数。
  3. 在调用命令方法的地方修改为调用单独方法。
  4. 测试。

拆解命令对象的例子

public interface Command {
    void execute();
}

public class SaveCommand implements Command {
    private Data data;

    public SaveCommand(Data data) {
        this.data = data;
    }

    @Override
    public void execute() {
        // 在这里实现保存数据的逻辑
        // ...
    }
}

上面的例子中,设置了相关属性后,内部的逻辑并不是很多,此时可以直接移除命令对象改成下面调用方式。

Data data = new Data();
saveData(data);

引入参数对象

解决过多的参数

如果盲目的补充方法的参数,会导致方法中参数过多,在调用这个方法的时候需要识别每个参数的含义。另外一种情况在存在多个类型相同的参数时,多个参数传递会出现错位的情况,有时候一个疏忽会导致参数设置到了错误的位置上。

将存在的多个参数整合成一个对象进行传递,他带来的不仅仅是方法可读性的提高。更重要的是为这个方法的使用规定了数据结构。另外当我们将这个方法参数对象化后,当我们在处理其他方法的时候如果发现类似的数据结构时,我们就可以捕捉到这些参数对象是否存在共同的部分。围绕着这些共同的数据行为,就能判断出这些方法是否在围绕着同一段逻辑进行业务,这些方法是否可以基于共同的内容是否可以进行更深层次的优化,比如抽取父类、抽取公共代码等。

如果担心参数对象在初始化中存在可过多参数无法被合理设置,我们可以使用一些工厂方式来初始化这个对象,为每种业务设置单独的初始化方式,这样我们可以只关注和我们业务有关联的参数。

如果将参数转换为对象

  1. 首先考虑那些参数可以整合为对象,并不是方法中所有参数都需要整合成一个对象。最好是将有关联性的参数整合在一起。这样在未来能提供参数对象的复用程度。
  2. 创建参数对象,创建一个新的参数对象,并将相关参数添加到其中。
  3. 修改方法声明,修改方法的声明,将相关参数替换为新的参数对象。
  4. 修改方法实现,在方法内部汇总使用参数对象来数据数据
  5. 更新所有调用该方法的地方,以便使用新的参数对象。你需要修改方法调用中传递的参数,以便传递新的参数对象。
  6. 如果需要,可以为参数对象创建工厂类或者工厂方法,提供一些基于业务的初始化方式
  7. 进行测试

重构参数对象

public void printOrderDetails(String orderId, String customerName, String productCode, int quantity) {
    // code to print order details
}
public void printOrderDetails(OrderDetails orderDetails) {
    // code to print order details using orderDetails object
}

public class OrderDetails {
    private String orderId;
    private String customerName;
    private String productCode;
    private int quantity;

    public OrderDetails(String orderId, String customerName, String productCode, int quantity) {
        this.orderId = orderId;
        this.customerName = customerName;
        this.productCode = productCode;
        this.quantity = quantity;
    }
    
    public static OrderDetails createSpecialOrderDetail(String orderId) {
        return new OrderDetails(orderId,"",orderId,10);
    }

    // getter and setter methods
}

原始方法printOrderDetails()接受四个参数,重构后使用类OrderDetails来替代参数传递,同时创建了一个工厂方法createSpecialOrderDetail通过内部实现一些逻辑来减少某些业务中参数的传递。这使得代码更加简洁、易于阅读和维护。同时,如果我们需要添加或删除一些参数,我们只需要修改OrderDetails类,而不需要修改方法声明或调用。

将处理相同数据的方法整合到一个类中

什么时候将方法组合成类

如果发现一些方法都在处理同一部分的数据。或者多个方法都在类似的业务中处理数据,这个时候对于这些方法就存在共同的内容。我们可以给这些方法提供一个共同的运行环境,简单的说就是提供一个类来承接这些方法。所以当一批方法都存下面的情况时可以考虑整合成类。

  1. 处理相同业务的一组方法,比如都是处理用户信息的方法
  2. 处理类似功能的一组方法,都是处理字符串或者时间的方法
  3. 访问相同数据的一组方法,都是访问数据库或者文件系统的方法
  4. 一组需要业务相同上下文中的方法。

如何将方法组合成类

  1. 首先确定哪些方法是关联的,这些方法通常是在相同的上下文中使用的方法
  2. 创建一个新的类,将方法移动到新类中
  3. 修改方法调用,所有调用相关方法的地方使用新类的相应方法。你需要修改方法调用中的参数,以便传递新类的实例。
  4. 测试代码

整合成类的例子

如果是处理相同业务的方法,我们开发中Service层就是这种职责。

public class OrderService {
    public void createOrder(Order order) {
        // code to create order
    }

    public void updateOrder(Order order) {
        // code to update order
    }

    public void cancelOrder(Order order) {
        // code to cancel order
    }
}

另外一种情况是,当一组方法使用相同的上下文的时候,我们可以为这些方法和上下文设置单独的类,将执行序列中的方法,作为类自身属性。

public double calculateTotalSales(List<Product> products) {
    double totalSales = 0.0;
    for (Product product : products) {
        double productSales = calculateProductSales(product);
        totalSales += productSales;
    }
    return totalSales;
}

public double calculateProductSales(Product product) {
    double productSales = 0.0;
    for (Order order : product.getOrders()) {
        double orderSales = calculateOrderSales(order);
        productSales += orderSales;
    }
    return productSales;
}

public double calculateOrderSales(Order order) {
    double orderSales = order.getQuantity() * order.getProduct().getPrice();
    if (order.getDiscount() != null) {
        orderSales *= (1.0 - order.getDiscount());
    }
    return orderSales;
}

上面是一组用来计算价格的方法,这些方法都是处理计算这一动作的方法,可以创建一个专门的SalesCalculator 的类来保存起内容

public class SalesCalculator {
    private List<Product> products;

    public SalesCalculator(List<Product> products) {
        this.products = products;
    }

    public double calculateTotalSales() {
        double totalSales = 0.0;
        for (Product product : products) {
            double productSales = calculateProductSales(product);
            totalSales += productSales;
        }
        return totalSales;
    }

    private double calculateProductSales(Product product) {
        double productSales = 0.0;
        for (Order order : product.getOrders()) {
            double orderSales = calculateOrderSales(order);
            productSales += orderSales;
        }
        return productSales;
    }

    private double calculateOrderSales(Order order) {
        double orderSales = order.getQuantity() * order.getProduct().getPrice();
        if (order.getDiscount() != null) {
            orderSales *= (1.0 - order.getDiscount());
        }
        return orderSales;
    }
}

我们可以通过以下方式使用 SalesCalculator 类来计算所有产品的总销售额:

List<Product> products = ...; // 初始化产品列表
SalesCalculator calculator = new SalesCalculator(products);
double totalSales = calculator.calculateTotalSales();

以子类取代类型码或者不同的逻辑分支

面对多分支的逻辑要如何处理

有时候整个业务流程中都存在多个类型判断,我们在流程的任何一步都要通过类型判断执行不同的逻辑。这样会导致判断的重复以及处理逻辑在每一步都糅杂在一起,伴随着判断类型的增多代码也越来越难以阅读。并且这些条件逻辑分支可能随着需求的变化而频繁地修改时。

我们可以尝试将这些逻辑分支转换为不同的子类,每个子类负责处理一种特定的情况。这样的好处是如果发生变更我们在单独的子类进行处理,既不会影响其他分支也不会被其他分支所干扰。通过最初的判断进入不同的处理类中。这些类拥有一个共同的父类或者接口,但是在业务实现中又实用自己的逻辑。

使用多态取代逻辑分支并不是所有条件判断都需要如此实现。如果这些条件判断存在的地方很少,或者内部的业务分支并没有多么复杂的时候,盲目的引入多态,只会增加代码阅读负担。但是如果逻辑分支的代码已经很复杂了或者即将变得复杂,那么最好使用多态将这部分内容拆分开来。

如何进行多态处理

  1. 首先我们单独创建一个接口或抽象超类,根据判断分支创建对应的实现类。
  2. 将各个逻辑分支中使用的公共方法放在超类中。
  3. 在子实现类中,建立函数,其内容包含之前相关子类条件表达式分支内所有逻辑。
  4. 重复这个步骤,直到所有分支都被处理。
  5. 测试。
  6. 最后,可以将原有的条件逻辑分支代码删除。替换为调用不同实现类中的方法。也通过超类中实现工厂方法在内部完成根据判断调用不同实现类。

一个用多态处理后的例子

重构前,是一个有多个条件分支的业务处理

public void doSomething(Animal animal) {
    if (animal instanceof Cat) {
        // do something for cat
    } else if (animal instanceof Dog) {
        // do something for dog
    } else if (animal instanceof Bird) {
        // do something for bird
    } else {
        // handle other types of animals
    }
}

重构后,创建对应子类,子类继承Animal,然后再每个逻辑处理内将使用子类进行实现

public abstract class Animal {
    public abstract void doSomething();

    public static Animal create(Animal animal) {
        if (animal instanceof Cat) {
            return new Cat();
        } else if (animal instanceof Dog) {
            return new Dog();
        } else if (animal instanceof Bird) {
            return new Bird();
        } else {
            throw new IllegalArgumentException("Invalid animal type");
        }
    }
    
    public void doSomething(Animal animal) {
        Animal animalImpl = Animal.create(animal);
        animalImpl.doSomething();
	}
}

然后每个特定类型的行为都由对应的子类实现,并且我们使用多态性来选择正确的子类实例。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

大·风

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值