mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
1437 字
4 分钟
代码设计的原则
2020-12-27

一、为什么需要良好的代码设计#

所有的代码编写都是有目的性的创作行为,都是设想计划后实践的结果。没有经过良好设计的代码就如同纸糊的房子,一吹即倒。

有的「代码狂人」即使在自己不太熟悉的领域或场景,接过需求就撸起袖子开干,仅凭脑海中那一点模糊的蓝图便信心满满地说「我能搞定」,认为船到桥头自然直、车到山前必有路。到最后,可能功能测试通过了,代码确实能解决问题——就像纸房子确实能住人一样——但遇到问题时,便陷入无休止的回炉重造。

有经验的程序员比新手值钱,原因也在于此。因为有经验的程序员在某些熟悉的领域和场景已经踩过足够多的坑,相应地,他们写出的代码回炉重造的机会少很多,开发和维护的成本也就下来了。

那么,什么样的代码才是设计良好的代码?性能好?可读性强?鲁棒性高?似乎很难界定。其实,阅读开源代码的过程中就能悟出以下道理:

  • 人类容易理解的代码是好代码。 代码是写给人看的,只有易于理解的代码才有长远的发展潜力。
  • 易于维护和迭代升级的代码是好代码。 技术是不断迭代发展的,性能、安全性、兼容性等都能通过迭代不断升级。
  • 易于调试、可跟踪和可观测的代码是好代码。 只有能记录运行时产生问题的代码,才能更容易地迭代。

上述三个条件从编写时、维护时、运行时三个方面定义了一份好代码应承担的责任。满足这些条件,能让软件更健康地迭代与升级。

graph TB subgraph "好代码的三要素" A["好代码"] --> B["可读性<br>编写时"] A --> C["可维护性<br>维护时"] A --> D["可调试性<br>运行时"] B --> B1["命名清晰"] B --> B2["格式统一"] B --> B3["注释恰当"] C --> C1["低耦合"] C --> C2["高内聚"] C --> C3["易测试"] D --> D1["日志规范"] D --> D2["链路追踪"] D --> D3["优雅退出"] end

二、怎样保持良好的代码设计#

怎么进行代码设计,相信大家都看过不少书籍。有针对语言特性的最佳实践《Effective {Language}》系列,也有纯粹针对编码的《Clean Code》和《重构》,还有面向对象相关的《设计模式》等。

这里从可读性、可维护性和可调试性三个方面,说明如何编写好代码。

2.1 可读性#

命名#

命名是代码可读性的核心。一个好的命名应该:

  • 名副其实:看名字就知道变量/函数/类的用途
  • 避免缩写:除非是领域内公认缩写(如 ctxreqresp
  • 一致性:同样概念使用同样的命名,不要一会儿 GetUser 一会儿 FetchUser
// 不好:缩写和歧义
func calc(a, b int) int
// 好:清晰的命名
func calculateRectangleArea(width, height int) int
// 不好:中英混搭
func 获取用户信息(userId string) *User
// 好:统一语言
func getUserInfo(userId string) *User
graph LR subgraph "命名原则" N1["名副其实"] --> N2["一看就懂用途"] N3["避免缩写"] --> N4["除非公认缩写"] N5["保持一致"] --> N6["同一概念同一命名"] end

代码格式#

格式化工具(如 Go 的 gofmt、Python 的 black)能消除格式化争议,让团队风格统一。

# Go 格式化
go fmt ./...
# Python 格式化
black .

注释和 README#

注释解释「为什么」,代码本身说明「是什么」:

// 不好:注释显而易见的事实
// 增加 i
i++
// 好:解释业务背景
// 用户成功登录后,session 有效期延长 30 分钟
session.Extend(30 * time.Minute)
// TODO 标注未完成的工作
// TODO(zhangsan): 需要考虑并发安全问题

语法特性和语法糖#

语法糖可以简化代码,但过度使用会降低可读性:

// 过度使用:三元嵌套
result := condition1 ? condition2 ? value1 : value2 : condition3 ? value3 : value4
// 适度使用:清晰的条件分支
if condition1 {
result = value1
} else if condition2 {
result = value2
} else {
result = value3
}

2.2 可维护性#

重复代码封装为函数#

重复三次以上的代码块应该封装为函数:

// 重复代码
validateEmail(email)
sendWelcomeEmail(email)
logUserAction(email, "register")
validateEmail(phone)
sendWelcomeEmail(phone)
logUserAction(phone, "register")
// 好的做法:封装为函数
func registerUser(email string) {
validateContact(email)
sendWelcomeEmail(email)
logUserAction(email, "register")
}

核心设计原则#

graph TB subgraph "三大设计原则" KISS["KISS<br>保持简单"] --> K1["简单比聪明重要"] DRY["DRY<br>不要重复"] --> D1["抽象公共逻辑"] YAGNI["YAGNI<br>不做无用功"] --> Y1["只做当前需要的"] K1 --> G["可维护代码"] D1 --> G Y1 --> G end

KISS 原则(Keep It Simple, Stupid):简单比聪明更重要。

// 过度工程:为了「扩展性」引入不必要的抽象
type Repository interface {
GetUser(id string) (*User, error)
SaveUser(user *User) error
}
// 如果系统永远只需要 MySQL,不需要这个抽象层

YAGNI 原则(You Aren’t Gonna Need It):不要为你不需要的功能设计。

// 预测需要支持多种数据库
type UserRepositoryMysql struct {}
type UserRepositoryPostgres struct {}
// 实际上只有 MySQL,提前设计是浪费

高内聚低耦合#

graph TB subgraph "高内聚低耦合" subgraph "高内聚 模块内" A["功能A"] --> C["核心职责"] B["功能B"] --> C end subgraph "低耦合 模块间" D["模块X"] -.->|接口| E["模块Y"] E -.->|事件| F["模块Z"] end end
  • 高内聚:一个模块只做一件事
  • 低耦合:模块之间依赖尽量少
// 耦合示例:用户模块依赖了订单模块
type UserService struct {
orderService *OrderService
}
// 低耦合:只依赖自己的模块,通过事件解耦
type UserService struct {
eventBus *EventBus
}
userService.eventBus.Publish("user.registered", event)

测试#

测试是代码可维护的保障。TDD(测试驱动开发)是推荐的实践:

func TestCalculateArea(t *testing.T) {
result := calculateRectangleArea(3, 4)
if result != 12 {
t.Errorf("expected 12, got %d", result)
}
}

2.3 可调试性#

日志打印规范#

好的日志应该回答「发生了什么、谁触发的、什么时间、结果如何」:

// 不好的日志:信息不足
log.Println("error")
// 好的日志:结构化信息
log.WithFields(log.Fields{
"user_id": userID,
"action": "register",
"duration": duration.Milliseconds(),
}).Info("user registration completed")

模块之间的日志追踪#

使用 trace ID 串联跨模块请求:

[trace-id=abc123] user service received request
[trace-id=abc123] user service calling order service
[trace-id=abc123] order service completed in 50ms

关闭或崩溃时的动作#

优雅退出(Graceful Shutdown)确保资源正确释放:

func main() {
srv := &http.Server{Addr: ":8080"}
// 收到信号后优雅退出
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
go func() {
<-quit
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
srv.Shutdown(ctx)
}()
srv.ListenAndServe()
}

三、SOLID 原则详解#

Robert C. Martin 的《代码整洁之道》提出了 SOLID 原则,这是面向对象设计的核心准则。

graph TB subgraph "SOLID 原则" S["S - 单一职责"] --> S1["一个类只做一件事"] O["O - 开闭原则"] --> O1["扩展开放 修改关闭"] L["L - 里氏替换"] --> L1["子类可替换父类"] I["I - 接口隔离"] --> I1["接口小而专注"] D["D - 依赖倒置"] --> D1["依赖抽象不依赖具体"] S1 --> G["可维护<br>可扩展<br>可测试"] O1 --> G L1 --> G I1 --> G D1 --> G end
原则全称说明实践要点
S单一职责原则一个类/函数只做一件事一个变更理由
O开闭原则对扩展开放,对修改关闭抽象与多态
L里氏替换原则子类可替换基类行为一致性
I接口隔离原则接口要小而专注客户端不应依赖不需要的接口
D依赖倒置原则依赖抽象而非具体依赖注入

3.1 单一职责原则(SRP)#

graph LR subgraph "违反 SRP" A["UserManager"] --> A1["创建用户"] A --> A2["发送邮件"] A --> A3["生成报告"] end subgraph "符合 SRP" B["UserService"] --> B1["创建用户"] C["EmailService"] --> C1["发送邮件"] D["ReportService"] --> D1["生成报告"] end

3.2 开闭原则(OCP)#

classDiagram class Shape { <<abstract>> +area() float } class Rectangle { +width: float +height: float +area() float } class Circle { +radius: float +area() float } class Triangle { +base: float +height: float +area() float } Shape <|-- Rectangle Shape <|-- Circle Shape <|-- Triangle class AreaCalculator { +totalArea(shapes: Shape[]) float } AreaCalculator --> Shape

新增图形类型只需添加新的 Shape 子类,无需修改 AreaCalculator。

3.3 依赖倒置原则(DIP)#

graph TB subgraph "违反 DIP" A["高层模块"] -->|直接依赖| B["低层模块A"] A --> C["低层模块B"] end subgraph "符合 DIP" D["高层模块"] -->|依赖| E["抽象接口"] F["低层模块A"] -.->|实现| E G["低层模块B"] -.->|实现| E end

四、小结#

设计良好的代码,最终目的是节省维护和开发成本

graph LR subgraph "好代码的价值" A["好代码"] --> B["可读<br>人类容易理解"] A --> C["可维护<br>易于修改和扩展"] A --> D["可调试<br>问题可追踪"] B --> E["降低沟通成本"] C --> F["降低修改成本"] D --> G["降低排查成本"] E --> H["节省开发和维护成本"] F --> H G --> H end

好代码的三个标准:

  • 可读:人类容易理解
  • 可维护:易于修改和扩展
  • 可调试:问题可追踪

好的设计不是一蹴而就,而是持续迭代的结果。每次代码变更都是改进的机会。


五、参考#


参考#

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

代码设计的原则
https://blog.souloss.com/posts/programming/code-design/
作者
Souloss
发布于
2020-12-27
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时