聊一聊代码重构——程序方法和类上的代码实践
使用工厂方法取代构造方法
构造方法的问题
我们使用构造方法来初始化对象时候,我们得到的只能是当前对象。而使用工厂方法替换构造方法,我们可以返回其子类型或者代理类型。这让我们可以通过不同的实现类来进行逻辑实现的变化。
更重要的一点是,构造方法的名称被严格限制,我们无法根据不同的构造方法来分析初始化的用途,但是使用工厂方法,我们可以通过参数名称、或者初始化方法的名称了解到对应方法解释初始化对象的用途。
如何创建构造工厂方法
- 新建一个工厂方法,让其调用现有的构造方法。如果需要可以为工厂方法取一个和业务相关的名称。
- 修改调用构造方法的代码,改为调用工厂方法。
- 进行测试
- 如果可以,尽量限制对象初始化方法的可见范围,初始化对象的工作全部交给工厂。
为对象设置工厂方法
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;}@Overridepublic 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();}
}
然后每个特定类型的行为都由对应的子类实现,并且我们使用多态性来选择正确的子类实例。
相关文章:
聊一聊代码重构——程序方法和类上的代码实践
使用工厂方法取代构造方法 构造方法的问题 我们使用构造方法来初始化对象时候,我们得到的只能是当前对象。而使用工厂方法替换构造方法,我们可以返回其子类型或者代理类型。这让我们可以通过不同的实现类来进行逻辑实现的变化。 更重要的一点是&#…...

嵌入式学习笔记——寄存器开发STM32 GPIO口
寄存器开发STM32GPIO口前言认识GPIOGPIO是什么GPIO有什么用GPIO怎么用STM32上GPIO的命名以及数量GPIO口的框图(重点)输入框图解析三种输入模式GPIO输入时内部器件及其作用1.保护二极管2.上下拉电阻(可配置)3.施密特触发器4.输入数…...

[ 攻防演练演示篇 ] 利用通达OA 文件上传漏洞上传webshell获取主机权限
🍬 博主介绍 👨🎓 博主介绍:大家好,我是 _PowerShell ,很高兴认识大家~ ✨主攻领域:【渗透领域】【数据通信】 【通讯安全】 【web安全】【面试分析】 🎉点赞➕评论➕收藏 养成习…...
程序设计与 C 语言期末复习
程序设计与 C 语言 1.计算机语言与编译 机器语言:一串仅由 0 和 1 序列表示的语言。计算机只能识别和接受 0 和 1 组成的指令。 符号语言(汇编语言):用一些英文字母和数字表示一个指令。 符号语言(汇编语言…...

05-思维导图Xmind快速入门
文章目录5.1 认识思维导图5.2 Xmind的主要结构及主题元素5.2.1 Xmind的多种结构5.2.2 主题分类5.2.3 Xmind的主题元素章节总结5.1 认识思维导图 什么是思维导图? 思维导图是一种将思维进行可视化的实用工具。 具体实现方法是用一个关键词去引发相关想法࿰…...

使用去中心化存储构建网站
今天的大多数网站都遵循后端服务器到前端代码的架构。但在 Web3 应用程序中,前端代码不具有与受智能合约保护的后端代码相同的去中心化性和弹性。那么如何使网站像智能合约一样具有弹性呢? 该体系结构似乎很简单: 创建一个没有服务器的静态…...

L - Let‘s Swap(哈希 + 规律)
2023河南省赛组队训练赛(四) - Virtual Judge (vjudge.net) 约瑟夫最近开发了一款名为Pandote的编辑软件,现在他正在测试,以确保它能正常工作,否则,他可能会被解雇!Joseph通过实现对Pandote上字符串的复制和…...
c语言自动内存回收(RAII实现)
简述 什么是RAII RAII(Resource Acquisition Is Initialization)是c之父Bjarne Stroustrup提出的概念。资源一般分三个步骤:获取、使用和销毁,而在自由使用内存的c语言中,资源的销毁常常是程序员容易遗漏的事情&…...

Node.js的简单学习一-----未完待续
文章目录前言学习目标一、初识Node.js1.1 回顾与思考1.1.1 需要掌握那些技术1.1.2 浏览器中的JavaScript的组成部分1.2 Node.js简介1 什么是Node.js2 Node.js中的JavaScript运行环境3 Node.js 可以做什么1.3 Node.js环境的安装1.4 在Node.js环境中执行JavaScript 代码终端中的快…...

linux入门---粘滞位
为什么会有粘滞位 一台服务器有很多人使用,每个人在机器上都会有一个家目录,在家目录里可以实现自己想要的操作,但是有时候我们需要一个公共路径来完成一些操作,比如说资料分享产生临时文件的增删查改等等,这就好比我…...
关于正则表达式的讲解
以下内容源于《linux命令行与shell脚本编程大全【第三版】》一书的整理。 在shell脚本中成功运用sed编辑器和gawk程序的关键,在于熟练地使用正则表达式。 一、正则表达式的简介 1、正则表达式的定义 正则表达式(regular expression)是一个…...

贝塞尔曲线与B样条曲线
文章目录0.参考1.问题起源与插值法的曲线拟合1.1.问题起源1.2.拉格朗日插值1.3.“基”的概念1.4.插值存在的Runge现象2.贝塞尔曲线2.1.控制点的思想2.2.由控制点生成贝塞尔曲线2.3.多个控制点时的贝塞尔曲线公式2.4.贝塞尔曲线的递推公式2.5.贝塞尔曲线的性质3.B样条曲线3.1.B样…...
C语言-基础了解-24-C头文件
C头文件 一、C 头文件 头文件是扩展名为 .h 的文件,包含了 C 函数声明和宏定义,被多个源文件中引用共享。有两种类型的头文件:程序员编写的头文件和编译器自带的头文件。 在程序中要使用头文件,需要使用 C 预处理指令 #include…...

The 19th Zhejiang Provincial Collegiate Programming Contest vp
和队友冲了这场,极限6题,重罚时铁首怎么说,前面的A题我贡献了太多的罚时,然后我的G题最短路调了一万年,因为太久没写了,甚至把队列打成了优先队列,没把head数组清空完全,都是我的锅呜…...
用于<分类>的卷积神经网络、样本不平衡问题的解决
输入图像——卷积层——池化层——全连接层——输出 卷积层:核心,用来提取特征。 池化层:对特征降维。实际的主要作用是下采样,减少参数量来提高计算速度。 卷积神经网络的训练:前向传播(分类识别…...

网上订餐管理系统的设计与实现
技术:Java、JSP等摘要:随着信息技术的广泛使用,电子商务对于提高管理和服务水平发挥着关键的作用。越来越多的商家开始着手于电子商务建设。电子商务的发展为人们的生活提供了极大的便利,也成为现实社会到网络社会的真实体现。当今…...

Httpclient测试
在IDEA中有一个非常方便的http接口测试工具httpclient,下边介绍它的使用方法,后边我们会用它进行接口测试。如果IDEA版本较低没有自带httpclient,需要安装httpclient插件1.插件2.controller进入controller类,找到http接口对应的方…...

EXCEL里的各种奇怪计算问题:数字后面自动多了 0.0001, 数字后面位数变成000,以及一些取整,数学函数
1 公式计算后的数,用只粘贴数值后,后面自动多了 0.0001,导致不再是整数的问题 问题入戏 见第1个8400,计算时就出现了问题,按正常,这里8400应该是整数,而不应该带小数,但是确实就计…...
PHP CRUL请求GET、POST
// GET请求 public function curlGet($url,$header){ $ch curl_init(); curl_setopt($ch, CURLOPT_HTTPHEADER, $header); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE); curl_s…...

Oracle技术分享 exp导数据时报错ORA-01578 ORA-01110
问题描述:exp导数据时报错ORA-01578 ORA-01110,如下所示: 数据库:oracle 19.12 多租户 1、异常重现 [oracledbserver ~]$ exp ora1/ora1orclpdbfileemp.dmp tablesemp logexp.log Export: Release 19.0.0.0.0 - Production onS…...

业务系统对接大模型的基础方案:架构设计与关键步骤
业务系统对接大模型:架构设计与关键步骤 在当今数字化转型的浪潮中,大语言模型(LLM)已成为企业提升业务效率和创新能力的关键技术之一。将大模型集成到业务系统中,不仅可以优化用户体验,还能为业务决策提供…...
[2025CVPR]DeepVideo-R1:基于难度感知回归GRPO的视频强化微调框架详解
突破视频大语言模型推理瓶颈,在多个视频基准上实现SOTA性能 一、核心问题与创新亮点 1.1 GRPO在视频任务中的两大挑战 安全措施依赖问题 GRPO使用min和clip函数限制策略更新幅度,导致: 梯度抑制:当新旧策略差异过大时梯度消失收敛困难:策略无法充分优化# 传统GRPO的梯…...

从WWDC看苹果产品发展的规律
WWDC 是苹果公司一年一度面向全球开发者的盛会,其主题演讲展现了苹果在产品设计、技术路线、用户体验和生态系统构建上的核心理念与演进脉络。我们借助 ChatGPT Deep Research 工具,对过去十年 WWDC 主题演讲内容进行了系统化分析,形成了这份…...

【入坑系列】TiDB 强制索引在不同库下不生效问题
文章目录 背景SQL 优化情况线上SQL运行情况分析怀疑1:执行计划绑定问题?尝试:SHOW WARNINGS 查看警告探索 TiDB 的 USE_INDEX 写法Hint 不生效问题排查解决参考背景 项目中使用 TiDB 数据库,并对 SQL 进行优化了,添加了强制索引。 UAT 环境已经生效,但 PROD 环境强制索…...
多场景 OkHttpClient 管理器 - Android 网络通信解决方案
下面是一个完整的 Android 实现,展示如何创建和管理多个 OkHttpClient 实例,分别用于长连接、普通 HTTP 请求和文件下载场景。 <?xml version"1.0" encoding"utf-8"?> <LinearLayout xmlns:android"http://schemas…...

Linux相关概念和易错知识点(42)(TCP的连接管理、可靠性、面临复杂网络的处理)
目录 1.TCP的连接管理机制(1)三次握手①握手过程②对握手过程的理解 (2)四次挥手(3)握手和挥手的触发(4)状态切换①挥手过程中状态的切换②握手过程中状态的切换 2.TCP的可靠性&…...

LeetCode - 394. 字符串解码
题目 394. 字符串解码 - 力扣(LeetCode) 思路 使用两个栈:一个存储重复次数,一个存储字符串 遍历输入字符串: 数字处理:遇到数字时,累积计算重复次数左括号处理:保存当前状态&a…...

前端开发面试题总结-JavaScript篇(一)
文章目录 JavaScript高频问答一、作用域与闭包1.什么是闭包(Closure)?闭包有什么应用场景和潜在问题?2.解释 JavaScript 的作用域链(Scope Chain) 二、原型与继承3.原型链是什么?如何实现继承&a…...
关于 WASM:1. WASM 基础原理
一、WASM 简介 1.1 WebAssembly 是什么? WebAssembly(WASM) 是一种能在现代浏览器中高效运行的二进制指令格式,它不是传统的编程语言,而是一种 低级字节码格式,可由高级语言(如 C、C、Rust&am…...

html css js网页制作成品——HTML+CSS榴莲商城网页设计(4页)附源码
目录 一、👨🎓网站题目 二、✍️网站描述 三、📚网站介绍 四、🌐网站效果 五、🪓 代码实现 🧱HTML 六、🥇 如何让学习不再盲目 七、🎁更多干货 一、👨…...