About separation of concern
Posted on Wed 27 August 2025 in Journal
Abstract | About SoC |
---|---|
Authors | Walter Fan |
Category | learning note |
Status | v1.0 |
Updated | 2025-08-27 |
License | CC-BY-NC-ND 4.0 |
通过关注点分离来减少认知负担
作为开发者,我们经常通过性能、测试覆盖率或可维护性等指标来衡量代码质量。但有一个关键方面经常被忽视:其他人理解和使用我们代码的难易程度。这正是关注点分离(SoC)原则的闪光点。通过将系统分解为不同的、专注的组件,我们减少了那些阅读或使用我们代码的人的认知负担,使我们的库和应用程序更直观、更愉快地使用。
什么是关注点分离?
关注点分离的核心是一种设计原则,它主张将软件系统分解为不同的部分,每个部分处理一个特定的"关注点"——一组相关的功能、职责或特性。把它想象成组织代码就像组织一个结构良好的图书馆:不是在一个混乱的架子上放着书籍、工具和随机物品,而是有明确标记的部分(小说、非小说、参考书),让你毫不费力地找到你需要的东西。
在软件术语中,关注点可能包括: - 数据存储和检索 - 业务逻辑和规则 - 用户界面渲染 - 日志记录和错误处理 - 身份验证和授权
目标不仅仅是为了分离而分离,而是创建简化理解和交互的边界。
SoC如何减少认知负担
认知负担是指处理信息所需的脑力努力。当代码混合多个关注点时,用户必须同时在脑海中保持太多概念。SoC通过以下方式缓解了这个问题:
- 限制范围:每个组件处理一个关注点,所以用户一次只需要理解系统的一个小的、专注的部分。
- 建立可预测性:具有明确职责的组件以更一致、可预见的方式运行。
- 减少心理上下文切换:用户在使用单个组件时不需要在无关概念之间跳跃(例如,数据库查询和UI渲染)。
让我们探索这如何转化为实际的代码改进。
实际例子:从混乱到清晰
例子1:一个单体函数
考虑这个混合了数据获取、业务逻辑和输出格式化的Python函数:
def process_user_order(user_id):
# 数据库关注点
conn = sqlite3.connect('mydb.db')
cursor = conn.cursor()
cursor.execute(f"SELECT * FROM users WHERE id = {user_id}")
user = cursor.fetchone()
cursor.execute(f"SELECT * FROM orders WHERE user_id = {user_id}")
orders = cursor.fetchall()
conn.close()
# 业务逻辑关注点
total = 0
for order in orders:
total += order[2] # 假设第三列是价格
if user[3] == 'premium': # 假设第四列是用户等级
total *= 0.9 # 高级用户享受10%折扣
# 格式化关注点
print(f"User: {user[1]}") # 假设第二列是姓名
print(f"Total orders: {len(orders)}")
print(f"Total amount: ${total:.2f}")
这个函数强迫用户同时理解SQL查询、折扣计算和打印格式化。如果有人想改变总价的计算方式,他们必须涉足数据库代码。如果输出需要是JSON而不是纯文本,他们可能会破坏业务逻辑。
例子2:分离的关注点
通过将其分解为专注的组件,我们减少了认知负担:
# 数据访问层(数据库关注点)
class UserRepository:
def __init__(self, db_path):
self.db_path = db_path
def get_user(self, user_id):
with sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
cursor.execute("SELECT * FROM users WHERE id = ?", (user_id,))
return cursor.fetchone()
class OrderRepository:
def __init__(self, db_path):
self.db_path = db_path
def get_orders_for_user(self, user_id):
with sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
cursor.execute("SELECT * FROM orders WHERE user_id = ?", (user_id,))
return cursor.fetchall()
# 业务逻辑层
class OrderService:
def __init__(self, user_repo, order_repo):
self.user_repo = user_repo
self.order_repo = order_repo
def calculate_total(self, user_id):
user = self.user_repo.get_user(user_id)
orders = self.order_repo.get_orders_for_user(user_id)
total = sum(order[2] for order in orders)
if user[3] == 'premium':
total *= 0.9
return total, user, orders
# 表示层
class OrderFormatter:
@staticmethod
def format_text(user, orders, total):
return (f"User: {user[1]}\n"
f"Total orders: {len(orders)}\n"
f"Total amount: ${total:.2f}")
@staticmethod
def format_json(user, orders, total):
return json.dumps({
"user": user[1],
"order_count": len(orders),
"total": total
})
现在,用户可以根据他们的需求与系统交互:
- 从事数据库更改的开发者只需要查看UserRepository
和OrderRepository
。
- 调整定价规则的人只专注于OrderService
。
- 修改输出格式的前端开发者专门使用OrderFormatter
。
每个组件都有单一职责,使代码库更容易导航和理解。
设计考虑SoC的库
在构建库或API时,SoC更加关键——你的用户将根据他们将其集成到系统中的难易程度来评判你的工作。以下是关键原则:
1. 隐藏实现细节
你的库的用户不应该需要理解其内部工作原理。例如,一个HTTP客户端库应该暴露像Get(url)
这样的方法,而不强迫用户直接处理连接池、超时或重试。这些关注点在内部被封装。
2. 提供清晰的接口
定义有良好文档的接口,将组件做什么与如何做分开。在Go中,这可能看起来像:
// PaymentProcessor定义了"做什么"(接口)
type PaymentProcessor interface {
Charge(amount float64, card Card) (Transaction, error)
}
// StripeProcessor实现了"如何做"(具体实现)
type StripeProcessor struct {
apiKey string
// 其他内部细节...
}
func (s *StripeProcessor) Charge(amount float64, card Card) (Transaction, error) {
// Stripe特定的逻辑(对用户隐藏)
}
用户依赖PaymentProcessor
(接口)而不是StripeProcessor
,减少了他们的心理负担——他们只需要理解接口契约,而不是它的实现。
3. 将配置与逻辑分离
避免将配置代码与业务逻辑混合。相反,提供专门的配置机制:
# 不好:配置与逻辑混合
def send_email(to, subject, body):
smtp_server = "smtp.example.com" # 硬编码配置
port = 587
sender = "noreply@example.com"
password = "secret" # 永远不要这样做!
with smtplib.SMTP(smtp_server, port) as server:
server.login(sender, password)
server.sendmail(sender, to, f"Subject: {subject}\n\n{body}")
# 好:分离的配置
class EmailConfig:
def __init__(self, server, port, sender, password):
self.server = server
self.port = port
self.sender = sender
self.password = password
class EmailSender:
def __init__(self, config: EmailConfig):
self.config = config
def send(self, to, subject, body):
with smtplib.SMTP(self.config.server, self.config.port) as server:
server.login(self.config.sender, self.config.password)
server.sendmail(self.config.sender, to, f"Subject: {subject}\n\n{body}")
用户配置系统一次,然后专注于使用EmailSender
——他们在日常使用中不需要考虑SMTP细节。
本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。