代码重构相关内容
使用工厂方法取代构造方法
构造方法的问题
我们使用构造方法来初始化对象时候,我们得到的只能是当前对象。而使用工厂方法替换构造方法,我们可以返回其子类型或者代理类型。这让我们可以通过不同的实现类来进行逻辑实现的变化。
更重要的一点是,构造方法的名称被严格限制,我们无法根据不同的构造方法来分析初始化的用途,但是使用工厂方法,我们可以通过参数名称、或者初始化方法的名称了解到对应方法解释初始化对象的用途。
如何创建构造工厂方法
- 新建一个工厂方法,让其调用现有的构造方法。如果需要可以为工厂方法取一个和业务相关的名称。
- 修改调用构造方法的代码,改为调用工厂方法。
- 进行测试
- 如果可以,尽量限制对象初始化方法的可见范围,初始化对象的工作全部交给工厂。
为对象设置工厂方法
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;
}
}
以命令对象取代方法
命令对象
有些业务中我们,需要将数据经过多个组合方法完成数据的处理。如果我们将数据看成一个整体,而要经过的方法作为数据内部的一些行为。我们可以创建一个命令对象
,我们将原始数据作为命令对象的初始化参数进行对象初始化,通过提供一个执行
。在内部包含对所有处理方法的调用。
使用命令对象可以将具体的操作与执行者分离,使得两者相对独立。并且对外我们提供的单独的一个接口,所有的执行序列都在内部完成。如果后续我们希望提供其他的逻辑实现时,我们只需要提供新的命令对象。对于调用者也只是需要修改一个新的命令对象。
并且需要修改的数据作为命令对象共享的字段,在不同处理方法中,不再需要设置繁琐的参数,所有的字段数据在调用方法后被实时更新。
使用命令对象,我们将数据的入口限制在初始化的地方,将数据的修改范围控制在命令对象内部。我们可以在可控的范围内对数据的访问和修改进行监控。
因为命令对象中进行执行的数据都保存在对象上下文中,这样通过命令对象
我们可以实现延迟启动或者回滚到某一个状态时的数据功能。
是否可以使用命令对象
所以当我们遇见下面场景可以考虑是否使用命令对象
- 需要做一些撤销、合并等业务,命令对象中可以记录之前状态值。
- 当需要将方法的执行过程进行延迟或者异步执行时,可以将方法转化为命令对象。
- 需要根据数据动态的改变方法执行过程,也可以使用方法转化为命令对象,并在命令对象中实现不同的执行方式。
构建命令对象的过程
- 首先创建一个空的类。给类起一个能表达要替换方法作用的名称。
- 将被替换方法复制到新的类中。
- 分析方法执行时需要的参数,其作为新建类的属性值。
- 创建对应的初始化方法。并提供一个执行方法来启动方法流程。
构建命令对象的例子
重构前
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);
}
}
}
以方法取代命令对象
拆解对象
但是很多时候我仅仅是想执行数据逻辑中的一部分。或者本身整个命令的执行序列并不复杂。但是这个时候却需要构建整个命令对象,这个时候构建命令对象会让整个过程的可读性大大降低。这个时候我们可以将命令对象还原成方法,直接调用其逻辑即可。
拆解命令对象
- 首先需要确定要重构的命令方法。
- 将命令方法中所有的逻辑提取,作为一个单独的方法。将命令对象初始化的参数作为单独方法的参数。
- 在调用命令方法的地方修改为调用单独方法。
- 测试。
拆解命令对象的例子
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);
引入参数对象
解决过多的参数
如果盲目的补充方法的参数,会导致方法中参数过多,在调用这个方法的时候需要识别每个参数的含义。另外一种情况在存在多个类型相同的参数时,多个参数传递会出现错位的情况,有时候一个疏忽会导致参数设置到了错误的位置上。
将存在的多个参数整合成一个对象进行传递,他带来的不仅仅是方法可读性的提高。更重要的是为这个方法的使用规定了数据结构。另外当我们将这个方法参数对象化后,当我们在处理其他方法的时候如果发现类似的数据结构时,我们就可以捕捉到这些参数对象是否存在共同的部分。围绕着这些共同的数据行为,就能判断出这些方法是否在围绕着同一段逻辑进行业务,这些方法是否可以基于共同的内容是否可以进行更深层次的优化,比如抽取父类、抽取公共代码等。
如果担心参数对象在初始化中存在可过多参数无法被合理设置,我们可以使用一些工厂方式来初始化这个对象,为每种业务设置单独的初始化方式,这样我们可以只关注和我们业务有关联的参数。
如果将参数转换为对象
- 首先考虑那些参数可以整合为对象,并不是方法中所有参数都需要整合成一个对象。最好是将有关联性的参数整合在一起。这样在未来能提供参数对象的复用程度。
- 创建参数对象,创建一个新的参数对象,并将相关参数添加到其中。
- 修改方法声明,修改方法的声明,将相关参数替换为新的参数对象。
- 修改方法实现,在方法内部汇总使用参数对象来数据数据
- 更新所有调用该方法的地方,以便使用新的参数对象。你需要修改方法调用中传递的参数,以便传递新的参数对象。
- 如果需要,可以为参数对象创建工厂类或者工厂方法,提供一些基于业务的初始化方式
- 进行测试
重构参数对象
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
类,而不需要修改方法声明或调用。
将处理相同数据的方法整合到一个类中
什么时候将方法组合成类
如果发现一些方法都在处理同一部分的数据。或者多个方法都在类似的业务中处理数据,这个时候对于这些方法就存在共同的内容。我们可以给这些方法提供一个共同的运行环境,简单的说就是提供一个类来承接这些方法。所以当一批方法都存下面的情况时可以考虑整合成类。
- 处理相同业务的一组方法,比如都是处理用户信息的方法
- 处理类似功能的一组方法,都是处理字符串或者时间的方法
- 访问相同数据的一组方法,都是访问数据库或者文件系统的方法
- 一组需要业务相同上下文中的方法。
如何将方法组合成类
- 首先确定哪些方法是关联的,这些方法通常是在相同的上下文中使用的方法
- 创建一个新的类,将方法移动到新类中
- 修改方法调用,所有调用相关方法的地方使用新类的相应方法。你需要修改方法调用中的参数,以便传递新类的实例。
- 测试代码
整合成类的例子
如果是处理相同业务的方法,我们开发中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();
以子类取代类型码或者不同的逻辑分支
面对多分支的逻辑要如何处理
有时候整个业务流程中都存在多个类型判断,我们在流程的任何一步都要通过类型判断执行不同的逻辑。这样会导致判断的重复以及处理逻辑在每一步都糅杂在一起,伴随着判断类型的增多代码也越来越难以阅读。并且这些条件逻辑分支可能随着需求的变化而频繁地修改时。
我们可以尝试将这些逻辑分支转换为不同的子类,每个子类负责处理一种特定的情况。这样的好处是如果发生变更我们在单独的子类进行处理,既不会影响其他分支也不会被其他分支所干扰。通过最初的判断进入不同的处理类中。这些类拥有一个共同的父类或者接口,但是在业务实现中又实用自己的逻辑。
使用多态取代逻辑分支并不是所有条件判断都需要如此实现。如果这些条件判断存在的地方很少,或者内部的业务分支并没有多么复杂的时候,盲目的引入多态,只会增加代码阅读负担。但是如果逻辑分支的代码已经很复杂了或者即将变得复杂,那么最好使用多态将这部分内容拆分开来。
如何进行多态处理
- 首先我们单独创建一个接口或抽象超类,根据判断分支创建对应的实现类。
- 将各个逻辑分支中使用的公共方法放在超类中。
- 在子实现类中,建立函数,其内容包含之前相关子类条件表达式分支内所有逻辑。
- 重复这个步骤,直到所有分支都被处理。
- 测试。
- 最后,可以将原有的条件逻辑分支代码删除。替换为调用不同实现类中的方法。也通过超类中实现工厂方法在内部完成根据判断调用不同实现类。
一个用多态处理后的例子
重构前,是一个有多个条件分支的业务处理
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();
}
}
然后每个特定类型的行为都由对应的子类实现,并且我们使用多态性来选择正确的子类实例。