嘿,兄弟姐妹们,我是老码小张。
不知道你有没有遇到过这样的场景:接手一个"祖传"项目,或者哪怕是自己几个月前写的代码,想加个小功能,或者改个 Bug,结果发现牵一发而动全身?改了一行代码,测试报出来十个八个新问题,越改越乱,最后心态崩了,默默地 git reset --hard
,心里想着:"这代码谁写的,也太烂了吧!"(然后发现是自己写的...尴尬不?)
或者,每次提交代码 Code Review 的时候,总是被大佬们指出各种"坏味道":这里逻辑太复杂、那里职责不清晰、到处都是重复代码... 心里是不是有点小委屈,觉得"功能能跑不就行了吗?"
其实啊,写出能跑的代码只是第一步,写出高质量、易维护、易扩展的代码,才是咱们程序员进阶的必经之路。这不仅仅是为了让别人看着舒服,更是为了未来的自己少加班、少掉头发!
今天,我就用大白话跟你唠唠 10 个能显著提升代码质量的编程好习惯。掌握了它们,不说立马成为大神,至少能让你写的代码看起来更"专业",下次 Code Review 底气都足一些!
1. 各司其职,职责单一 (SRP - Single Responsibility Principle)
这可是 SOLID 原则里的老大。啥意思呢?简单说,就是一个类(或者一个函数、一个模块)最好只干一件具体的事儿,并且干好它。就像一把瑞士军刀,功能挺多,但真要拧个大螺丝,还是专门的螺丝刀好用。
如果违反了会怎样? 一个类干的事儿太多,就像一个万能员工,啥都管。结果就是:
- 难修改:改动一个功能,可能影响到其他不相关的功能。
- 难测试:职责混在一起,单元测试写起来贼费劲。
- 难理解:代码臃肿,逻辑复杂,新人接手直接懵圈。
怎么做? 拆分!把不同的职责分离到不同的类或函数里。
python
# 反例:一个类干太多事
class UserManager:
def get_user_info(self, user_id):
# ... 连接数据库,获取用户信息 ...
print("获取用户信息")
def export_user_to_excel(self, user_id):
# ... 获取用户信息 ...
# ... 生成 Excel 逻辑 ...
print("导出用户到 Excel")
def send_welcome_email(self, user_id):
# ... 获取用户信息 ...
# ... 构造邮件内容 ...
# ... 发送邮件 ...
print("发送欢迎邮件")
# 正例:拆分职责
class UserRepository:
def get_user_info(self, user_id):
# ... 只负责数据获取 ...
print("从数据库获取用户信息")
return {"id": user_id, "name": "老张"}
class UserExcelExporter:
def export(self, user_info):
# ... 只负责生成 Excel ...
print(f"将用户信息 {user_info} 导出到 Excel")
class EmailService:
def send_email(self, user_info, subject, body):
# ... 只负责发送邮件 ...
print(f"向 {user_info['name']} 发送邮件: {subject}")
# 使用时组合
repo = UserRepository()
exporter = UserExcelExporter()
mailer = EmailService()
user = repo.get_user_info(123)
exporter.export(user)
mailer.send_email(user, "欢迎", "欢迎加入!")
你看,拆分之后,每个类是不是清爽多了?想改导出逻辑,就去 UserExcelExporter
;想换邮件服务商,就改 EmailService
,互不干扰。
2. 拥抱变化,扩展开放,修改封闭 (OCP - Open/Closed Principle)
这也是 SOLID 里的老二。意思是说,你的软件实体(类、模块、函数等)应该对于扩展是开放的,但是对于修改是封闭的。人话就是:加新功能的时候,尽量不要去改动已经测试过、稳定运行的老代码,而是通过增加新代码的方式来扩展。
怎么做到? 通常通过抽象(接口、抽象类)和多态来实现。定义好稳定的抽象层,新增的功能作为具体实现类来扩展。
想象一下支付场景,一开始只支持支付宝,后来要加微信支付、银联支付。如果直接在原来的支付函数里用 if-else
判断,那代码会越来越臃肿,每次加新的支付方式都要改老代码。
更好的方式是定义一个支付接口(PaymentProcessor
),包含 pay()
方法。支付宝、微信支付分别实现这个接口。需要加新的支付方式?再加个新的实现类就行了,完全不用动原来的代码。
这样,对于支付方式的扩展(新增 E)是开放的,但对于支付接口 B 和已有的实现 C、D 是封闭的,不需要修改它们。
3. 里氏替换,子类得"争气" (LSP - Liskov Substitution Principle)
SOLID 老三。简单说,所有引用基类(父类)的地方,必须能够透明地使用其子类的对象,而不引发错误或改变程序的原有行为。 也就是说,子类应该能够完全替代它的父类,并且表现出和父类一致的行为(或者说是符合父类定义的契约)。
经典的反例就是"正方形是长方形"的问题。如果 Rectangle
类有 setWidth
和 setHeight
方法,而 Square
继承自 Rectangle
,为了维持正方形的特性(边长相等),Square
的 setWidth
可能需要同时修改 height
。这就破坏了父类 Rectangle
的行为约定(设置宽度不应该影响高度),导致替换后行为不一致。
违反 LSP 会导致继承体系混乱,子类行为不可预测,给使用者带来困扰。设计继承关系时,要确保子类真正符合 "is-a" 的关系,并且遵循父类的行为约定。
4. 接口隔离,别强迫我实现用不上的 (ISP - Interface Segregation Principle)
SOLID 老四。意思是客户端不应该被强迫依赖它不使用的方法。 如果一个接口功能太多、太"胖",不同的客户端可能只需要其中的一部分功能,但却不得不依赖整个臃肿的接口。
怎么做? 把"胖"接口拆分成更小、更具体的接口。这样,客户端只需要依赖它真正关心的那些小接口。
比如,一个 Worker
接口有 work()
和 eat()
方法。对于 HumanWorker
来说没问题,但对于 RobotWorker
呢?机器人不吃饭啊!强迫 RobotWorker
实现一个空的 eat()
方法就不太好。
更好的做法是拆成 Workable
接口(含 work()
)和 Eatable
接口(含 eat()
)。HumanWorker
实现两个接口,RobotWorker
只实现 Workable
接口。
这样,RobotWorker
就不用依赖它不需要的 eat()
方法了。
5. 依赖倒置,面向接口编程 (DIP - Dependency Inversion Principle)
SOLID 老五。这个原则有两点:
- 高层模块不应该依赖于低层模块。两者都应该依赖于抽象。
- 抽象不应该依赖于具体实现。具体实现应该依赖于抽象。
有点绕?核心思想就是:要依赖抽象(接口或抽象类),而不是依赖具体的实现类。
想象一下,你的业务逻辑(高层模块)直接依赖一个具体的 MySQL 数据库操作类(低层模块)。如果将来想换成 PostgreSQL 数据库,那不是得把所有用到 MySQL 操作的地方都改一遍?
应用 DIP,我们应该定义一个数据库操作的接口(比如 IDatabase
),业务逻辑依赖这个接口。然后,我们提供 MySQLDatabase
和 PostgreSQLDatabase
作为这个接口的具体实现。通过依赖注入(DI)等方式,在运行时决定具体使用哪个实现。
这样,高层(业务逻辑)和低层(具体数据库实现)都依赖于抽象(IDatabase
),更换底层实现对高层几乎没有影响,代码更灵活、可测试性也更好。
(插句嘴,SOLID 这五个原则是面向对象设计的基石,理解透彻对写出高质量代码非常有帮助!)
6. 保持简单,傻瓜都能懂 (KISS - Keep It Simple, Stupid)
这个原则简直是真理!尽量让你的代码简单、直接、易于理解。 避免不必要的复杂性,不要为了炫技而写一些花里胡哨、难以读懂的代码。
简单的代码更容易:
- 阅读和理解
- 修改和维护
- 测试和调试
下次当你写下一段自认为"聪明"但复杂的代码时,停下来想一想:有没有更简单直接的方法?几个月后的自己或者接手的同事能看懂吗?
7. 拒绝重复,复制代码是万恶之源 (DRY - Don't Repeat Yourself)
系统中每一处知识都应该有单一、无歧义、权威的表示。 说白了,就是不要写重复的代码!
如果你发现自己在不同的地方复制粘贴同样或非常相似的代码块,那就要警惕了。一旦这部分逻辑需要修改,你就得找到所有复制粘贴的地方去改,很容易遗漏,导致 Bug。
怎么做?
- 把重复的逻辑提取成函数或方法。
- 把重复用到的常量定义成常量或枚举。
- 考虑使用类、继承、组合等方式来复用代码。
- 配置信息抽取到配置文件中。
python
# 反例:重复计算折扣
def calculate_order_price_vip(price, quantity):
total = price * quantity
discount = total * 0.1 # VIP 折扣
final_price = total - discount
print(f"VIP 订单最终价格: {final_price}")
return final_price
def calculate_order_price_svip(price, quantity):
total = price * quantity
discount = total * 0.2 # SVIP 折扣
final_price = total - discount
print(f"SVIP 订单最终价格: {final_price}")
return final_price
# 正例:提取计算逻辑,通过参数控制折扣
def calculate_order_price(price, quantity, discount_rate):
total = price * quantity
discount = total * discount_rate
final_price = total - discount
print(f"折扣率 {discount_rate*100}%, 最终价格: {final_price}")
return final_price
calculate_order_price(100, 2, 0.1) # VIP
calculate_order_price(100, 2, 0.2) # SVIP
DRY 不仅仅是代码层面的,也适用于文档、测试、构建脚本等方方面面。
8. 你不需要它!别过度设计 (YAGNI - You Ain't Gonna Need It)
这是极限编程(XP)的一个原则。意思是:不要现在就去实现那些你猜你未来可能会用到的功能。只实现当前真正需要的功能。
程序员往往有"未雨绸缪"的倾向,喜欢设计一些"灵活"、"可扩展"的架构,添加一些现在用不到但"将来可能有用"的功能。但很多时候,"将来"永远不会来,或者需求变化了,当初的设计反而成了负担。
过度设计会:
- 增加不必要的复杂性。
- 浪费开发时间。
- 引入潜在的 Bug。
遵循 YAGNI,可以让你聚焦于当前最重要的需求,更快地交付价值。当然,这不意味着完全不考虑扩展性,而是在简单设计 和过度设计之间找到平衡。
9. 组合优于继承,别滥用 "is-a" (Composition Over Inheritance)
这是面向对象设计中一个非常重要的建议。继承("is-a" 关系)是一种强耦合关系,子类和父类紧密绑定。父类的改变很容易影响到所有子类,而且 Java/C# 这类语言还不支持多重继承。
组合("has-a" 关系)则是将所需的功能作为成员变量(组件)包含进来,更加灵活。一个类可以通过组合不同的组件来实现不同的功能,运行时也可以动态地改变组合关系。
什么时候用继承? 当子类确实是父类的一个特殊类型(满足 LSP),并且想复用父类的实现时。
什么时候用组合? 当你需要复用功能,但类之间不是严格的 "is-a" 关系,或者你需要更灵活的组装方式时。
优劣势对比
特性 | 继承 (Inheritance) | 组合 (Composition) |
---|---|---|
关系 | is-a (强耦合) | has-a (松耦合) |
代码复用 | 复用父类实现 (白盒复用) | 复用组件功能 (黑盒复用) |
灵活性 | 较低,编译时确定 | 较高,可在运行时改变组合 |
对修改的封装 | 较差,父类修改可能破坏子类 | 较好,组件接口稳定,内部修改不影响使用者 |
多重继承 | 部分语言不支持,易导致"菱形问题" | 容易实现类似多重继承的效果 |
大部分情况下,优先考虑组合,它能带来更松耦合、更灵活的设计。
10. 关注点分离,让代码清爽起来 (SoC - Separation of Concerns)
这是一个更宏观的设计原则。意思是将一个复杂的系统或程序,按照不同的关注点(功能、职责)分解成不同的部分,每个部分处理一个独立的关注点。
这和 SRP 有点像,但 SoC 通常用在更高的层次上,比如:
- 分层架构:表现层(UI)、业务逻辑层(Service)、数据访问层(DAO/Repository)。每一层关注不同的事情。
- MVC/MVP/MVVM:模型(Model)、视图(View)、控制器/表示器/视图模型(Controller/Presenter/ViewModel)分离。
- 微服务架构:将大型单体应用拆分成多个独立的服务,每个服务关注特定的业务领域。
关注点分离能让系统的不同部分解耦,提高模块化程度,使得系统更容易理解、开发、测试和维护。
光说不练假把式,怎么落地这些原则?
知道了这些原则,更重要的是在日常开发中去实践它们:
- Code Review 时刻提醒:无论是 Review 别人的代码还是自己的代码被 Review,都可以用这些原则作为参照。
- 小步重构:遇到"坏味道"代码时,不要害怕修改。利用这些原则,小步、安全地进行重构。写测试是安全重构的保障!
- 从新代码做起:写新功能、新项目时,有意识地运用这些原则来指导设计。
- 刻意练习:尝试用不同的原则去解决同一个问题,对比差异。
- 讨论与交流:和同事多讨论代码设计,互相学习。
记住,这些原则不是银弹,也不是死板的教条。理解其背后的思想,根据具体场景灵活运用,才能真正写出优雅、健壮的代码。
希望今天分享的这几个编程好习惯能对你有所启发。代码质量的提升是一个持续修炼的过程,共勉!
我是老码小张,一个搬砖多年,喜欢琢磨技术原理,热爱在实践中不断学习和成长的普通技术人。如果你觉得这篇文章对你有帮助,或者有什么想交流的,欢迎在评论区留言,一起进步!