审校 | 重楼
对于软件开发者而言,编写可重用的代码是一项基本而重要的技能。每位工程师都应掌握如何尽可能地提高代码的复用性。当前,一些开发人员可能会认为微服务的本质是小而高效,因此他们无需编写高质量代码。然而,即便是微服务,在变得庞大时,阅读和理解代码的时间成本也会迅速增加至编写时的十倍。
代码一开始编写得不佳,将会大幅增加修复 bug 或添加新功能的工作量。在一些极端情况下,我见证过团队因代码质量问题而放弃原有代码,重新编写。这不仅浪费了宝贵时间,还可能导致开发人员承担责任并失去工作。
本文将介绍经过实践验证的提高Java 中代码可重用性的八条指导原则。
Java 编程中编写可重用代码的八大指导原则
- 制定编码规范
- 记录 API 文档
- 遵守代码命名规范
- 提高代码内聚性
- 代码解耦
- 遵循 SOLID 原则
- 合理运用设计模式
- 避免重复造轮子
制定编码规范
编写可重用代码的首要步骤是与团队一同确立代码规范。如果对编码规范不能达成共识,代码很快就会变得混乱不堪。如果团队成员之间意见不统一,关于代码实现的无效讨论也会频繁发生。同时,你需要确立一个基础的代码设计框架,以解决软件需要解决的问题。
在制定了标准和代码设计框架之后,接下来应当明确代码指导原则。
常见的指导原则包括:
- 代码命名规范
- 类和方法的行数限制
- 异常处理方式
- 包结构设计
- 编程语言及其版本
- 所使用的框架、工具和库
- 代码测试标准
- 代码结构层次(如控制器、服务、存储库、领域等)
一旦团队就这些规范达成共识,每个成员都应对代码审查负责,以确保编写出高质量、可重用的代码。如果没有共识,写出高质量且可重用的代码几乎不可能。
记录 API 文档
当创建服务并以 API 形式公开时,应该详细记录 API 信息,以便新加入的开发人员能够轻松理解和使用。
API 在微服务架构中扮演着重要角色。因此,对你的项目不太熟悉的其他团队成员必须能够通过阅读 API 文档来理解其功能。如果 API 文档记录不当,代码的重复编写风险会增加。新开发人员可能会无意中创建一个和现有功能重复的方法。
因此,精确记录 API 至关重要。但在代码中过度使用文档可能并无益处。应该只记录 API 中的关键信息,如业务操作的解释、参数、返回值对象等。
遵守代码命名规范
简洁且具有描述性的代码命名总是优于晦涩难懂的缩写。浏览不熟悉的代码库时,我发现缩写往往难以让人立即理解其含义。
因此,相较于使用像Ctr这样的缩写,直接命名为Customer更为明晰和有意义。Ctr可能代表了合同(contract)、控制(control)、客户(customer)等多种含义,使人难以确定其准确意图。
此外,要遵循你所使用编程语言的命名规范。以 Java 为例,它有 JavaBeans 命名规范,这对每个 Java 开发者来说都是基本常识。以下是 Java 中类、方法、变量和包的命名方式:
- 类名采用 PascalCase(帕斯卡命名法):如CustomerContract
- 方法和变量采用 camelCase(驼峰命名法):如customerContract
- 包名全部小写:如service
提高代码内聚性
内聚的代码应该专注于_做好一件事_。虽然这是一个简单的概念,但即便是经验丰富的开发人员也常常忽视它。这样,他们就会创建出所谓的_超级复杂类_,即一个承担了过多职责的类,有时也被称为_全能类_。
要实现高内聚的代码,关键是学会拆分代码,确保每个类和方法专注于单一职责。比如,如果你创建了一个名为saveCustomer的方法,它应当只负责一个动作:保存客户信息。它不应该同时负责更新和删除客户信息。
同理,如果有一个名为CustomerService的类,它应该仅包含与客户相关的功能。如果CustomerService类中有执行产品相关操作的方法,应移至ProductService类中。
与其在CustomerService类中添加执行产品操作的方法,不如在该类中引用ProductService,并调用我们所需的任何方法。
为了更清晰地介绍这个概念,我们来分析一个低内聚的类示例:
public class CustomerPurchaseService {
public void saveCustomerPurchase(CustomerPurchase customerPurchase) {
// 执行与客户相关的操作
registerProduct(customerPurchase.getProduct());
// 更新客户信息
// 删除客户信息
}
private void registerProduct(Product product) {
// 在客户领域中执行对产品的逻辑操作…
}
}
这个类存在以下问题:
- saveCustomerPurchase方法不止注册产品,还涉及到更新和删除客户的操作。这个方法承担了过多的职责。
- registerProduct方法在当前位置难以被其他开发者发现。因此,如果其他开发者需要类似的功能,可能会不必要地重写这个方法。
- registerProduct方法处于不恰当的领域。CustomerPurchaseService不应负责注册产品。
- saveCustomerPurchase方法调用了一个私有方法,而不是委托给专门处理产品操作的外部类。
识别出这些问题后,我们可以重新编写这段代码,使其变得更加内聚。我们将registerProduct方法移动到更合适的位置,即ProductService类中。这样做使代码更易于搜索和重用,同时避免将此方法局限在CustomerPurchaseService中:
public class CustomerPurchaseService {
private ProductService productService;
public CustomerPurchaseService(ProductService productService) {
this.productService = productService;
}
public void saveCustomerPurchase(CustomerPurchase customerPurchase) {
// 仅执行与客户购买相关的操作
productService.registerProduct(customerPurchase.getProduct());
}
}
public class ProductService {
public void registerProduct(Product product) {
// 在产品领域中执行相关逻辑…
}
}
在这个改进后的版本中,saveCustomerPurchase仅执行其主要职责:保存客户购买信息。同时,registerProduct方法的职责被正确地委托给了ProductService类,从而使两个类都更加专注和内聚。现在,这些类及其方法都专注于执行预期的特定任务。
代码解耦
_高度耦合的代码_指的是那些具有过多依赖关系的代码,这种情况会导致代码难以维护。类中定义的依赖(其他类)越多,其耦合程度就越高。
微服务架构的目标之一就是将服务解耦,如果一个微服务与其他服务都有连接,那么它就会高度耦合。
想要更好地实现代码复用,就需要尽可能使系统和代码各自独立。虽然服务和代码之间的通信不可避免会产生一定程度的耦合,但关键在于让这些服务尽可能保持独立性。
下面是一个高度耦合类的例子:
public class CustomerOrderService {
private ProductService productService;
private OrderService orderService;
private CustomerPaymentRepository customerPaymentRepository;
private CustomerDiscountRepository customerDiscountRepository;
private CustomerContractRepository customerContractRepository;
private CustomerOrderRepository customerOrderRepository;
private CustomerGiftCardRepository customerGiftCardRepository;
// 其他方法…
}
注意CustomerService类与许多其他服务类的耦合程度很高。这么多的依赖意味着该类将包含大量代码,这不利于代码的测试和维护。
更有效的方法是拆分这个类,创造多个依赖更少的服务。我们可以通过将CustomerService类分解为独立的服务来降低其耦合度:
public class CustomerOrderService {
private OrderService orderService;
private CustomerPaymentService customerPaymentService;
private CustomerDiscountService customerDiscountService;
// 省略其他方法…
}
public class CustomerPaymentService {
private ProductService productService;
private CustomerPaymentRepository customerPaymentRepository;
private CustomerContractRepository customerContractRepository;
// 省略其他方法…
}
public class CustomerDiscountService {
private CustomerDiscountRepository customerDiscountRepository;
private CustomerGiftCardRepository customerGiftCardRepository;
// 省略其他方法…
}
经过这样的重构,CustomerService及其他类变得更易于进行单元测试,同时也更便于维护。类的职责越单一且清晰,就越容易实现新功能。如果出现 bug,也更容易进行修复。
遵循 SOLID 原则
SOLID 代表面向对象编程(OOP)中五个关键的设计原则,它们的目标是使软件系统更加可维护、灵活,并易于理解。
下面是这些原则的简要说明:
- 单一职责原则(SRP):一个类应该仅承担一个职责或目标,并且完全封装这个职责。这个原则促进_高内聚_,帮助保持类的专注和易于管理。
- 开放封闭原则(OCP):软件实体(如类、模块、函数等)应该对扩展开放,对修改封闭。应设计代码以便在不更改现有代码的情况下增加新功能,从而减少更改的影响并促进代码重用。
- 里氏替换原则(LSP):超类的任何实例都应该能被其子类的实例替换,而不影响程序的正确性。换言之,基类的任何实例都应该可以用其派生类的实例替换,保证程序行为的一致性。
- 接口隔离原则(ISP):客户端不应被迫依赖它们不需要的接口。建议将大型接口拆分为更小且更具体的接口,使得客户端仅需依赖它们真正需要的接口。这有助于松耦合和避免不必要的依赖。
- 依赖反转原则(DIP):高层模块不应依赖低层模块,两者都应依赖抽象。鼓励使用抽象(接口或抽象类)来解耦高层模块和低层实现细节,促进基于抽象而非具体实现的依赖。
遵循这些 SOLID 原则有助于开发者编写更模块化、可维护且易于扩展的代码。这些原则有助于实现更易于理解、测试和修改的代码,从而形成更健壮、适应性更强的软件系统。
合理运用设计模式
设计模式是经验丰富的开发者在处理多种编码场景后总结出的最佳实践。恰当地使用设计模式可以显著提升代码的复用性。
掌握设计模式还能增强你阅读和理解代码的能力——这包括 JDK 中的代码。当你能识别出其背后的设计模式时,代码会变得更加清晰。
尽管设计模式有其用处,但并非每种模式适用于所有情况,因此使用时需谨慎。仅仅因为我们了解某个模式,并不意味着就应该随意应用。在不恰当的场景中使用设计模式可能会使代码变得更复杂、更难以维护。然而,在合适的场合应用设计模式,可以使代码更加灵活和易于扩展。
以下是面向对象编程中常见的设计模式简要概述:
创建型模式
- 单例(Singleton):确保一个类仅有一个实例,并提供一个全局访问点。
- 工厂方法(Factory Method):定义一个用于创建对象的接口,但由子类决定实例化哪个类。
- 抽象工厂(Abstract Factory):提供一个接口,用于创建相关或依赖对象的族群。
- 建造者(Builder):分离复杂对象的构建和表示。
- 原型(Prototype):通过复制现有的实例来创建新实例。
结构型模式
- 适配器(Adapter):将一个类的接口转换成客户端所期望的另一种接口。
- 装饰器(Decorator):动态地为对象添加新的功能。
- 代理(Proxy):为另一个对象提供一个代理或占位符,以控制对这个对象的访问。
- 组合(Composite):将对象组合成树形结构,以表示部分整体的层次结构。
- 桥接(Bridge):将抽象部分与其实现部分分离,使它们可以独立变化。
行为型模式
- 观察者:在对象间建立一种一对多的依赖关系,使得当一个对象改变状态时,所有依赖它的对象都会自动收到通知并更新。
- 策略:封装了一系列算法,并在运行时允许选择其中的一种。
- 模板方法:在基类中定义一个算法的框架,并允许子类提供具体的实现。
- 命令:将请求或简单操作封装成对象,这使得你可以使用不同的请求、队列或日志请求,并支持可撤销的操作。
- 状态:当对象的内部状态改变时,允许对象改变其行为。
- 迭代器:提供一种方法来顺序访问聚合对象的元素,而无需暴露其底层表示。
- 责任链:允许请求沿着处理者链传递,直到一个处理者处理它。
- 中介者:定义一个对象,该对象封装了一组对象如何交互的方式,从而促进它们之间的松散耦合。
- 访问者:将算法从操作的对象中分离出来,将算法封装到一个称为访问者的对象中。
并不需要记住每一种设计模式,重要的是意识到这些模式的存在,并理解它们各自的使用场景。这样,你就能够根据具体的编程情境选择最合适的设计模式。
避免重复造轮子
许多公司在没有充分理由的情况下仍选择使用内部框架,但这对非大型科技公司来说通常是不现实的。对于中小型企业来说,与开源社区或大型科技公司竞争,开发出更优解决方案的可能性较低。
相比于重复发明轮子和制造不必要的工作,更理智的选择是直接利用已有的工具和技术。这不仅节省时间,还有助于开发人员的职业发展,因为他们无需学习只在公司内部使用的框架。
例如,Hibernate 是一个经过严格测试并被广泛使用的持久性框架。我遇到过的一家公司选择使用自己的内部框架进行持久化处理,尽管它并不具备 Hibernate 的全部功能和稳定性。维护和扩展这种内部框架给公司带来了额外负担,而没有带来相应的好处。
因此,建议尽可能使用市场上广泛可用且流行的技术和工具。开发一个能与成熟开源软件匹敌的框架几乎不可能,因为后者是众多才华横溢的开发者多年合作的成果。此外,许多大型公司也支持开源项目,确保它们能按预期运行。
结论
理解和应用代码复用性的关键原则对于构建高效且可维护的软件系统至关重要。通过掌握抽象、封装、关注点分离、标准化和文档化等关键概念,开发人员能创建可节省时间和减少重复工作的组件,同时提升代码质量。
设计模式对代码复用至关重要,提供了针对常见设计问题的经过验证过的解决方案。内聚性和低耦合确保组件独立且依赖性最小,提高了它们的复用性。遵循 SOLID 原则有助于创建模块化、可扩展的代码,易于集成到不同项目中。
编写可复用代码能够为开发人员带来诸多好处,包括提高生产力、加强协作和加快开发周期。可复用代码使项目迭代更快、维护更容易,能够有效利用现有解决方案。总之,掌握代码复用性的核心原则能够帮助开发人员构建可扩展、适应性强且面向未来的软件系统。
译者介绍
刘汪洋,51CTO社区编辑,昵称:明明如月,一个拥有 5 年开发经验的某大厂高级 Java 工程师,拥有多个主流技术博客平台博客专家称号。
原文How to write reusable Java code,作者:Rafael del Nero