引言
大家好!今天想和大家聊聊一个在软件开发中老生常谈但又常谈常新的话题——代码复用。相信很多朋友在面试中都被问过“你如何避免重复代码”,在日常工作中也为了抽出公共逻辑而绞尽脑汁。代码复用确实是提高开发效率、降低维护成本的重要手段,但我们也常常陷入“过度复用”的泥潭。
园友@码农札记 说得好:“复用是银弹,但银弹也会误伤队友。”
为什么要复用代码?
在深入探讨之前,先简单回顾一下代码复用的核心价值:
提高生产力:站在巨人的肩膀上,不必重新发明轮子
降低维护成本:修复一处 bug,所有地方都受益
提升代码质量:经过充分测试的复用模块往往更可靠
保持行为一致:相同的业务逻辑不会在不同地方“长歪”
常见的代码复用方式
- 函数/方法复用
最基础也是最直接的复用形式。
csharp
// 不好的做法:重复的验证逻辑
public void CreateUser(string email, string phone)
{
if (string.IsNullOrEmpty(email) || !email.Contains("@"))
throw new ArgumentException("邮箱格式错误");
// 业务逻辑...
}
public void UpdateUser(string email, string phone)
{
if (string.IsNullOrEmpty(email) || !email.Contains("@"))
throw new ArgumentException("邮箱格式错误");
// 业务逻辑...
}
// 好的做法:抽取验证函数
private void ValidateEmail(string email)
{
if (string.IsNullOrEmpty(email) || !email.Contains("@"))
throw new ArgumentException("邮箱格式错误");
}
public void CreateUser(string email, string phone)
{
ValidateEmail(email);
// 业务逻辑...
}
- 继承(Inheritance)
OOP 的三大特性之一,适合 is-a 关系。
csharp
public abstract class DataExporter
{
public abstract byte[] Export();
public void LogExport()
{
Console.WriteLine($"导出 {this.GetType().Name} 数据");
}
}
public class PdfExporter : DataExporter
{
public override byte[] Export()
{
// PDF 导出逻辑
}
}
⚠️ 注意:继承是强耦合的,滥用继承会导致“脆弱的基类”问题。优先考虑组合而非继承。
- 组合(Composition)
更灵活、更推荐的复用方式。
csharp
public interface ILogger
{
void Log(string message);
}
public class FileLogger : ILogger { / 实现 / }
public class DbLogger : ILogger { / 实现 / }
public class OrderService
{
private readonly ILogger _logger;
// 通过构造函数注入,灵活组合不同的日志实现
public OrderService(ILogger logger)
{
_logger = logger;
}
}
- 泛型(Generics)
类型安全的复用,避免为不同类型重复写相同逻辑。
csharp
// 无需为每个实体类型写一个 Repository
public interface IRepository where T : class
{
T GetById(int id);
IEnumerable GetAll();
void Add(T entity);
}
- 模板方法模式
定义算法骨架,让子类实现具体步骤。
csharp
public abstract class DataProcessor
{
// 模板方法
public void Process()
{
LoadData();
ProcessData();
SaveResult();
}
protected abstract void LoadData();
protected abstract void ProcessData();
protected virtual void SaveResult() { }
}
复用的陷阱:什么时候不该复用?
这是本文的重点!不是所有重复的代码都应该被复用。
陷阱一:过早复用
“Don’t write code for tomorrow, write code for today.”
如果你不确定未来的需求,不要把两段“看起来相似”的代码强行合并。未来它们可能会朝着不同方向演化,强行复用会让修改变得痛苦。
陷阱二:上下文耦合的复用
csharp
// 看似公共的验证方法
public void ValidateAndProcess(string input, bool isAdmin, bool isNewUser)
{
// 根据各种标志位执行不同的逻辑
// 天哪,这个方法内部已经充满了 if-else
}
当公共方法需要越来越多“模式切换”参数时,说明它已经不是真正的公共逻辑了。
陷阱三:跨越边界的复用
不同模块、不同服务、不同 bounded context 之间的复用要格外小心。看似相同的“用户”概念,在订单上下文中和权限上下文中可能包含不同的业务规则。
复用的原则与最佳实践
DRY 的正确理解
DRY(Don‘t Repeat Yourself)不是“不写重复代码”,而是“每一片知识在系统中只有单一、明确、权威的表达”。
两段代码即使字面相同,如果代表不同的业务含义,就不应该强行复用。
复用三问
在抽取公共逻辑之前,问自己:
变化方向是否相同? 如果未来修改的原因不一样,就别放一起
是否有真正的业务共性? 还是仅仅是目前巧合地相似?
复用的边界是否清晰? 公共模块的职责是否单一?
实践建议
场景 推荐方式 避免方式
通用工具函数 静态方法类 继承
业务逻辑复用 组合+依赖注入 深层继承链
跨项目复用 NuGet/Package 复制粘贴
UI 组件 组件化(组合) 多重继承
案例分享:一个过度复用的“受害者”
去年我们团队接手了一个遗留系统,发现一个名为 CommonHelper 的类有 3000 多行代码,包含了邮件发送、日志记录、数据校验、加密解密、Excel 导出……几乎所有业务都依赖它。结果每次修改这个类,都会引发意想不到的问题。
重构方案:按职责拆分成 EmailService、LoggingService、ValidationService 等独立服务,通过接口进行组合。经过两周的拆分,系统的可测试性、可维护性都大幅提升。
教训: 复用≠大杂烩。一个类被太多地方依赖,恰恰说明它可能承担了过多职责。
总结
代码复用是一把双刃剑,用好了事半功倍,用滥了反受其害。我的建议是:
Rule of Three:同一段代码出现三次再考虑抽象
组合优于继承:保持低耦合
明确边界:不同上下文不要强行复用
保持简单:如果复用带来的复杂性超过了它的好处,那就别复用
最后,借用园友的一句话结束本文:
“代码复用的目标不是写更少的代码,而是让每行代码都更容易理解和维护。”