DRY(Don't Repeat Yourself,不要重复自己)是软件工程中最基础也最重要的原则之一。它看似简单——"不要重复代码"——但在实际应用中却包含着深刻的工程智慧和丰富的实践细节。本文将从理论根源、判定标准、实践模式、常见误区、与其他原则的权衡等多个维度,全面解析DRY原则,帮助开发者建立系统性的认知框架。
DRY原则由Andy Hunt和Dave Thomas在1999年出版的《程序员修炼之道》(The Pragmatic Programmer)中首次系统性地提出。其核心定义是:
"Every piece of knowledge must have a single, unambiguous, authoritative representation within a system."
(系统中的每一条知识都必须有单一的、无歧义的、权威的表示。)
这个定义远比"不要重复代码"深刻。关键在于理解"知识"(knowledge)的含义——DRY针对的不是代码文本的重复,而是业务逻辑、领域概念、系统规则的重复表达。
在软件系统中,"知识"可以表现为多种形式:
| 知识类型 | 具体表现 | 重复风险 |
|---|---|---|
| 业务规则 | 验证逻辑、计算公式、状态流转 | 散落在多个服务中 |
| 数据结构 | 实体定义、DTO、数据库Schema | 前后端不一致 |
| 配置信息 | 超时时间、重试策略、阈值参数 | 硬编码在多处 |
| 流程定义 | 审批流程、状态机、业务编排 | 代码与文档不一致 |
| 技术决策 | 架构约束、编码规范、接口契约 | 口头约定未文档化 |
代码重复带来的问题远不止"看起来不优雅"这么简单:
1. 维护成本指数级增长
当同一知识在N处重复时,任何变更都需要修改N处。假设每处修改的成功率是90%,那么整体成功率就是0.9^N。当N=10时,成功率已经降至35%。
2. 不一致性风险
人类天生不擅长机械地重复相同操作。多处实现必然产生细微差异,这些差异在初期可能无害,但随着系统演进,会成为难以追踪的bug源头。
3. 认知负担
开发者需要理解"这几处代码是否真的相同"还是"看似相同实则不同"。这种不确定性增加了代码审查的难度,也提高了新成员上手的门槛。
4. 测试膨胀
重复的代码需要重复的测试。测试代码本身也需要维护,形成恶性循环。
这是理解DRY最关键的区别:
语法重复(Syntactic Duplication)
语义重复(Semantic Duplication)
DRY原则关注的是语义重复,而非语法重复。
我们可以用"变化频率"和"变化同步性"两个维度来评估重复的风险:
变化同步性
必须同步 可能独立
┌─────────┬─────────┐
高变化频率 │ 危险区 │ 机会区 │
│ (必须DRY) │ (适度抽象)│
变化频率 ─────┼─────────┼─────────┤
低变化频率 │ 安全区 │ 过度区 │
│ (可容忍) │ (避免过度)│
└─────────┴─────────┘
危险区(高频率 + 必须同步):这是最危险的情况。例如支付金额计算逻辑,任何变更都必须全局生效,且经常因业务调整而变化。必须立即DRY。
安全区(低频率 + 必须同步):虽然必须同步,但很少变化。例如国家代码ISO标准映射,可以容忍一定程度的重复。
机会区(高频率 + 可能独立):变化频繁但可能独立演进。例如两个业务线的用户注册流程,当前相似但未来可能分化。需要谨慎抽象,保留扩展点。
过度区(低频率 + 可能独立):这是过度设计的温床。强行抽象会增加不必要的耦合。
在决定是否消除重复前,问自己以下问题:
只有当问题1、2的回答是"是",且问题4的成本足够高时,才应该优先消除重复。
最基础的DRY技法。当发现多段代码执行相同操作时,提取为共享函数。
示例:参数校验逻辑的提取
// ❌ 重复:多处相同的校验逻辑
fun createOrder(request: CreateOrderRequest) {
if (request.userId.isBlank()) throw ValidationException("userId不能为空")
if (request.amount <= 0) throw ValidationException("amount必须大于0")
if (request.currency !in SUPPORTED_CURRENCIES) throw ValidationException("不支持的货币类型")
// ... 业务逻辑
}
fun updateOrder(request: UpdateOrderRequest) {
if (request.userId.isBlank()) throw ValidationException("userId不能为空")
if (request.amount <= 0) throw ValidationException("amount必须大于0")
if (request.currency !in SUPPORTED_CURRENCIES) throw ValidationException("不支持的货币类型")
// ... 业务逻辑
}
// ✅ DRY:提取共享校验逻辑
object OrderValidation {
fun validate(request: OrderRequest) {
require(request.userId.isNotBlank()) { "userId不能为空" }
require(request.amount > 0) { "amount必须大于0" }
require(request.currency in SUPPORTED_CURRENCIES) { "不支持的货币类型" }
}
}
fun createOrder(request: CreateOrderRequest) {
OrderValidation.validate(request)
// ... 业务逻辑
}
当多个算法有相同的骨架但具体步骤不同时,使用模板方法模式。
// 抽象骨架
abstract class PaymentProcessor {
fun process(payment: Payment): PaymentResult {
validate(payment) // 通用步骤
val prepared = prepare(payment) // 通用步骤
val result = execute(prepared) // 抽象步骤 - 子类实现
return finalize(result) // 通用步骤
}
private fun validate(payment: Payment) { /* 通用校验 */ }
private fun prepare(payment: Payment): PreparedPayment { /* 通用准备 */ }
private fun finalize(result: RawResult): PaymentResult { /* 通用收尾 */ }
protected abstract fun execute(prepared: PreparedPayment): RawResult
}
// 具体实现
class AlipayProcessor : PaymentProcessor() {
override fun execute(prepared: PreparedPayment): RawResult {
// 支付宝特定逻辑
}
}
class WechatPayProcessor : PaymentProcessor() {
override fun execute(prepared: PreparedPayment): RawResult {
// 微信支付特定逻辑
}
}
当需要根据不同条件执行不同算法,且这些算法可能独立变化时。
interface DiscountStrategy {
fun calculateDiscount(order: Order): BigDecimal
}
class PercentageDiscount(private val percentage: BigDecimal) : DiscountStrategy {
override fun calculateDiscount(order: Order): BigDecimal {
return order.amount * percentage
}
}
class FixedAmountDiscount(private val amount: BigDecimal) : DiscountStrategy {
override fun calculateDiscount(order: Order): BigDecimal {
return amount.min(order.amount)
}
}
class OrderService(private val discountStrategy: DiscountStrategy) {
fun calculateFinalAmount(order: Order): BigDecimal {
val discount = discountStrategy.calculateDiscount(order)
return order.amount - discount
}
}
确保每个数据元素在系统中只有一个权威来源。
反模式:多处维护用户信息
// ❌ 重复:用户信息在多个服务中维护
class OrderService {
fun createOrder(userId: String) {
val user = userRepository.findById(userId) // 本地缓存的用户表
// ...
}
}
class PaymentService {
fun processPayment(userId: String) {
val user = userClient.getUser(userId) // 调用用户服务
// ...
}
}
正确做法:用户服务是用户信息的唯一权威来源
class UserService {
fun getUser(userId: String): User {
// 唯一的数据访问入口,包含缓存策略、权限控制等
}
}
// 其他服务通过UserService获取用户信息
class OrderService(private val userService: UserService) {
fun createOrder(userId: String) {
val user = userService.getUser(userId)
// ...
}
}
前后端数据结构的重复是常见痛点。解决方案:
# OpenAPI 定义(单一来源)
openapi: 3.0.0
components:
schemas:
Order:
type: object
properties:
id:
type: string
amount:
type: number
status:
$ref: '#/components/schemas/OrderStatus'
# 通过代码生成器生成:
# - TypeScript interfaces(前端)
# - Kotlin/Java classes(后端)
# - Python dataclasses(数据分析)
将跨服务的通用能力下沉为共享基础设施:
| 层级 | 内容 | 示例 |
|---|---|---|
| 基础设施层 | 技术无关的通用能力 | 日志、监控、配置中心 |
| 业务基础层 | 领域无关的业务能力 | 支付网关抽象、消息通知 |
| 领域共享层 | 特定领域的通用概念 | 订单状态机、风控规则引擎 |
在DDD中,DRY体现在多个层面:
限界上下文(Bounded Context)内的DRY:
跨限界上下文的DRY:
// 值对象:消除原始类型的重复校验
@JvmInline
value class Email(val value: String) {
init {
require(value.matches(EMAIL_PATTERN)) { "Invalid email format" }
}
}
// 在多处使用,校验逻辑只需定义一次
class User(val email: Email)
class Contact(val email: Email)
class NotificationTarget(val email: Email)
这是违反DRY最常见的错误——在还没有看清变化模式时就强行抽象。
症状:
案例:
// ❌ 过早抽象:为了2个相似但不同的需求创建复杂抽象
interface DataExporter {
fun export(data: List<Data>, options: ExportOptions): ExportResult
}
class CsvExporter : DataExporter { /* ... */ }
class ExcelExporter : DataExporter { /* ... */ }
// 实际上CSV和Excel的导出逻辑差异很大,
// 强行统一导致ExportOptions类膨胀,包含大量互斥参数
原则:
"三法则"(Rule of Three):当同一代码出现第三次时,才考虑抽象。
抽象得太高或太低都会导致问题。
抽象过高:丢失领域语义
// ❌ 过度泛化:丢失了业务语义
fun <T> process(items: List<T>,
validator: (T) -> Boolean,
transformer: (T) -> T,
saver: (T) -> Unit) {
items.filter(validator).map(transformer).forEach(saver)
}
// ✅ 保留领域语义
fun approvePendingOrders(orders: List<Order>) {
orders.filter { it.status == PENDING }
.map { it.copy(status = APPROVED, approvedAt = now()) }
.forEach { orderRepository.save(it) }
}
抽象过低:无法消除真正的重复
// ❌ 抽象不足:只提取了语法重复,语义重复仍然存在
fun validateNotBlank(value: String, field: String) {
if (value.isBlank()) throw ValidationException("$field不能为空")
}
fun validatePositive(value: BigDecimal, field: String) {
if (value <= 0) throw ValidationException("$field必须大于0")
}
// 调用处仍然重复:校验逻辑散落各处
fun createOrder(request: CreateOrderRequest) {
validateNotBlank(request.userId, "userId")
validatePositive(request.amount, "amount")
validateNotBlank(request.currency, "currency")
// ...
}
强行消除不相关的重复,会引入不必要的耦合。
案例:工具方法的滥用
// ❌ 将不相关的逻辑强行放入Utils类
object StringUtils {
// 用于订单号生成
fun generateRandomCode(length: Int): String { /* ... */ }
// 用于密码重置令牌
fun generateSecureToken(): String { /* ... */ }
// 用于日志追踪ID
fun generateTraceId(): String { /* ... */ }
}
// 问题:这三者虽然都是"生成字符串",但变化原因完全不同
// - 订单号格式可能随业务调整
// - 密码令牌需要安全升级
// - 追踪ID格式可能随监控系统变化
正确做法:按变化原因分离
object OrderCodeGenerator {
fun generate(): String { /* 订单号特定逻辑 */ }
}
object SecurityTokenGenerator {
fun generate(): String { /* 安全令牌特定逻辑 */ }
}
object TraceIdGenerator {
fun generate(): String { /* 追踪ID特定逻辑 */ }
}
在不同限界上下文之间强行共享代码,是微服务架构中的常见错误。
// ❌ 错误:订单服务和库存服务共享领域模型
// 位于共享库中
class OrderItem(
val productId: String,
val quantity: Int,
val unitPrice: BigDecimal
)
// 订单服务使用OrderItem计算订单金额
// 库存服务使用OrderItem扣减库存
// 问题:
// 1. 订单和库存对"订单项"的理解不同(价格vs可用性)
// 2. 一方的需求变化会影响另一方
// 3. 破坏了服务自治
正确做法:每个上下文有自己的模型,通过防腐层转换
// 订单服务内部模型
class OrderItem {
val productId: ProductId
val quantity: Quantity
val unitPrice: Money
fun calculateSubtotal(): Money = unitPrice * quantity
}
// 库存服务内部模型
class StockReservation {
val productId: ProductId
val quantity: Quantity
val reservationTime: Instant
}
// 防腐层转换
class InventoryAntiCorruptionLayer {
fun toStockReservation(orderItem: OrderItem): StockReservation {
return StockReservation(
productId = ProductId(orderItem.productId.value),
quantity = Quantity(orderItem.quantity.value),
reservationTime = now()
)
}
}
当消除重复会显著增加复杂性时,宁可保留简单的重复。
决策框架:
if (抽象复杂度 > 重复维护成本 × 重复次数) {
保留重复(遵循KISS)
} else {
消除重复(遵循DRY)
}
案例:简单的CRUD操作
// 两个几乎相同的查询,但强行抽象后难以理解
// ❌ 过度抽象
abstract class EntityQuery<T, ID, F> {
abstract fun buildSpec(filter: F): Specification<T>
abstract fun buildSort(sort: Sort): Sort
fun execute(filter: F, page: Pageable): Page<T> { /* ... */ }
}
// ✅ 保留简单重复
class OrderRepository {
fun findByStatusAndDateRange(status: OrderStatus, start: LocalDate, end: LocalDate): List<Order>
fun findByCustomerAndAmountRange(customerId: String, min: BigDecimal, max: BigDecimal): List<Order>
}
不要为了"未来可能的复用"而创建抽象。只有当重复真实存在且带来实际问题时,才消除它。
反模式:
// ❌ YAGNI违反:为"可能"的复用创建复杂框架
abstract class WorkflowEngine<T> {
abstract fun validate(input: T): ValidationResult
abstract fun execute(input: T): ExecutionResult
abstract fun compensate(result: ExecutionResult): CompensationResult
fun run(input: T): Result {
// 通用工作流编排逻辑
// 支持重试、补偿、状态持久化等
}
}
// 实际上目前只有一个工作流,且短期内不会有第二个
有时消除重复会引入性能开销(如额外的函数调用、数据转换)。在性能关键路径上,可以有控制地违反DRY。
案例:内联展开
// 通用实现(DRY但多一次函数调用)
fun calculateDiscount(order: Order): BigDecimal {
return discountStrategy.calculate(order)
}
// 性能关键路径的内联版本(违反DRY但减少开销)
fun calculateDiscountFast(order: Order): BigDecimal {
// 直接将最常用的策略内联展开
if (order.customerType == VIP) {
return order.amount * 0.15.toBigDecimal()
}
return BigDecimal.ZERO
}
重要:这种优化应该有明确的性能测试支撑,并添加详细注释说明。
有时完全消除重复会降低代码的可读性。
案例:测试代码
// ❌ 过度DRY的测试 - 难以理解具体测试什么
class OrderServiceTest {
private fun testOrderCreation(
customerType: CustomerType,
expectedDiscount: BigDecimal,
expectedStatus: OrderStatus
) {
// 通用测试逻辑
}
@Test fun `create order for regular customer`() = testOrderCreation(REGULAR, 0BD, PENDING)
@Test fun `create order for vip customer`() = testOrderCreation(VIP, 15BD, PENDING)
@Test fun `create order for wholesale customer`() = testOrderCreation(WHOLESALE, 20BD, PENDING)
}
// ✅ 适度重复但清晰的测试
class OrderServiceTest {
@Test fun `regular customer gets no discount`() {
val order = orderService.createOrder(regularCustomer(), sampleItems())
assertThat(order.discount).isEqualTo(0BD)
assertThat(order.status).isEqualTo(PENDING)
}
@Test fun `vip customer gets 15 percent discount`() {
val order = orderService.createOrder(vipCustomer(), sampleItems())
assertThat(order.discount).isEqualTo(15BD)
assertThat(order.status).isEqualTo(PENDING)
}
}
组件复用:
// ❌ 重复:每个页面都复制粘贴相同的表单验证
function UserForm() {
const [email, setEmail] = useState('');
const [error, setError] = useState('');
const validate = () => {
if (!email.includes('@')) setError('Invalid email');
// ...
};
// ...
}
function ProfileForm() {
const [email, setEmail] = useState('');
const [error, setError] = useState('');
const validate = () => {
if (!email.includes('@')) setError('Invalid email');
// ...
};
// ...
}
// ✅ 提取自定义Hook
function useEmailValidation() {
const [email, setEmail] = useState('');
const [error, setError] = useState('');
const validate = useCallback(() => {
if (!email.includes('@')) {
setError('Invalid email');
return false;
}
setError('');
return true;
}, [email]);
return { email, setEmail, error, validate };
}
// 多处复用
function UserForm() {
const { email, setEmail, error, validate } = useEmailValidation();
// ...
}
样式复用:
// 使用CSS-in-JS或Tailwind的@apply提取公共样式
// 而非复制粘贴CSS规则
const ButtonBase = styled.button`
padding: 0.5rem 1rem;
border-radius: 0.25rem;
font-weight: 600;
`;
const PrimaryButton = styled(ButtonBase)`
background: blue;
color: white;
`;
const SecondaryButton = styled(ButtonBase)`
background: gray;
color: black;
`;
ETL管道:
# ❌ 重复:每个数据源都复制相同的清洗逻辑
def process_orders_data(raw_data):
df = pd.DataFrame(raw_data)
df = df.dropna(subset=['order_id', 'amount'])
df['amount'] = df['amount'].astype(float)
df['created_at'] = pd.to_datetime(df['created_at'])
return df
def process_refunds_data(raw_data):
df = pd.DataFrame(raw_data)
df = df.dropna(subset=['refund_id', 'amount'])
df['amount'] = df['amount'].astype(float)
df['created_at'] = pd.to_datetime(df['created_at'])
return df
# ✅ 提取通用清洗步骤
class DataCleaner:
def __init__(self, required_fields: List[str], numeric_fields: List[str],
date_fields: List[str]):
self.required_fields = required_fields
self.numeric_fields = numeric_fields
self.date_fields = date_fields
def clean(self, raw_data: List[Dict]) -> pd.DataFrame:
df = pd.DataFrame(raw_data)
df = df.dropna(subset=self.required_fields)
for field in self.numeric_fields:
df[field] = pd.to_numeric(df[field], errors='coerce')
for field in self.date_fields:
df[field] = pd.to_datetime(df[field], errors='coerce')
return df
# 配置化使用
order_cleaner = DataCleaner(
required_fields=['order_id', 'amount'],
numeric_fields=['amount', 'discount'],
date_fields=['created_at', 'paid_at']
)
Terraform模块:
# ❌ 重复:为每个环境复制相同的资源配置
# production/main.tf
resource "aws_instance" "api" {
ami = "ami-12345678"
instance_type = "t3.large"
# ... 20+ 行配置
}
# staging/main.tf
resource "aws_instance" "api" {
ami = "ami-12345678"
instance_type = "t3.medium"
# ... 相同的20+ 行配置
}
# ✅ 提取模块
# modules/api-server/main.tf
variable "instance_type" {}
variable "environment" {}
resource "aws_instance" "api" {
ami = var.ami
instance_type = var.instance_type
tags = {
Environment = var.environment
}
# ... 共享配置
}
# production/main.tf
module "api_server" {
source = "../modules/api-server"
instance_type = "t3.large"
environment = "production"
}
# staging/main.tf
module "api_server" {
source = "../modules/api-server"
instance_type = "t3.medium"
environment = "staging"
}
审查清单:
新增代码是否与现有代码重复?
修改是否需要在多处进行?
抽象是否恰当?
测试是否重复?
识别DRY债务:
// 标记需要重构的重复代码
// TODO-DRY: 与PaymentService.validateAmount重复,待提取到共享库
fun validateAmount(amount: BigDecimal) {
require(amount > 0) { "Amount must be positive" }
}
优先级评估:
| 优先级 | 条件 | 处理策略 |
|---|---|---|
| P0 | 核心业务逻辑重复,频繁变化 | 立即重构 |
| P1 | 工具方法重复,影响多个团队 | 排入下个迭代 |
| P2 | 测试代码重复 | 技术债日处理 |
| P3 | 配置/文档重复 | 随下次变更处理 |
编码规范:
## DRY原则团队规范
1. 发现重复时,先判断是语法重复还是语义重复
2. 语义重复必须消除,语法重复可酌情保留
3. 提取抽象前,确保已有3处或以上的重复
4. 抽象必须位于正确的层次(领域层 vs 基础设施层)
5. 跨服务的代码复用必须通过共享库,禁止复制粘贴
静态分析工具:
# SonarQube配置示例
# 检测代码重复
sonar.cpd.exclusions:
- "**/test/**" # 测试代码允许适度重复
- "**/generated/**" # 生成的代码不检查
# 设置重复阈值
sonar.cpd.minimumTokens: 100 # 最小token数
sonar.cpd.minimumLines: 10 # 最小行数
DRY原则触及了软件工程的一个根本问题:知识如何在代码中表达。
代码不仅是给机器执行的指令,更是给人类阅读的知识载体。DRY追求的是"每一个知识点都有唯一的权威表达",这实际上是在追求:
DRY原则的实践本质上是一门抽象的艺术。好的抽象:
坏的抽象则:
DRY原则不是绝对的教条。在实践中,我们需要不断权衡:
这种权衡没有标准答案,需要依赖经验、上下文判断和团队共识。
日常编码:
代码审查:
技术债管理:
团队建设:
本文档遵循DRY原则编写,所有概念、案例、模式均经过精心设计,力求成为DRY原则实践的权威参考。