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通过以下方式缓解了这个问题:

  1. 限制范围:每个组件处理一个关注点,所以用户一次只需要理解系统的一个小的、专注的部分。
  2. 建立可预测性:具有明确职责的组件以更一致、可预见的方式运行。
  3. 减少心理上下文切换:用户在使用单个组件时不需要在无关概念之间跳跃(例如,数据库查询和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
        })

现在,用户可以根据他们的需求与系统交互: - 从事数据库更改的开发者只需要查看UserRepositoryOrderRepository。 - 调整定价规则的人只专注于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 国际许可协议进行许可。