2-1-3-隐式边界vs显式边界

2.1.3 隐式边界 vs 显式边界

引言

你的团队可能有这样的约定:

  • "用户相关的函数都以 user_ 开头"
  • "不要直接修改数据库,要通过 Repository"
  • "所有 API 响应都用统一的格式"

这些都是隐式边界——靠约定和记忆维持的规则。

问题是:

  • 新人不知道这些约定
  • 老人可能忘记
  • 时间久了,约定被打破
  • 没有任何东西强制执行这些规则

隐式边界依赖人的自律,显式边界通过代码强制。

隐式边界的脆弱性

脆弱1:依赖约定俗成

# 团队约定:"配置参数都放在 config 字典里"
config = {
    'db_host': 'localhost',
    'db_port': 5432,
    'api_key': 'secret123'
}

# 使用时
def connect_db():
    host = config['db_host']  # 假设这个 key 存在
    port = config['db_port']
    # ...

# 问题:
# - 如果有人拼错了 key 怎么办?config['db_hots']
# - 如果有人忘记添加某个配置怎么办?
# - 如果有人添加了错误类型的值怎么办?config['db_port'] = "not a number"

问题

  • 没有类型检查
  • 没有必填字段检查
  • 拼写错误运行时才能发现
  • 完全依赖开发者记住所有规则

脆弱2:文档和代码脱节

# 文档说:"所有服务类都必须实现 start() 和 stop() 方法"

class EmailService:
    def start(self):
        print("Email service started")

    def stop(self):
        print("Email service stopped")

class PaymentService:
    def start(self):
        print("Payment service started")

    # 问题:开发者忘记实现 stop()
    # 代码依然能运行,只是调用 stop() 时会出错

问题

  • 文档只是建议,代码不强制
  • 随着时间推移,有些服务遵守,有些不遵守
  • 新人读文档,老代码却没遵守

脆弱3:命名约定

# 约定:"私有方法以 _ 开头"
class OrderProcessor:
    def process_order(self, order):
        # 公开方法
        self._validate(order)
        self._calculate_total(order)
        self._save(order)

    def _validate(self, order):
        # 应该是"私有"的
        ...

    def _calculate_total(self, order):
        # 应该是"私有"的
        ...

# 但其他地方可以直接调用
processor = OrderProcessor()
processor._validate(some_order)  # Python 不会阻止

问题

  • Python 的 _ 只是约定,不是强制
  • 无法阻止误用
  • 依赖代码审查发现问题

显式边界的力量

力量1:类型系统

# 隐式:字典配置
config = {'db_host': 'localhost', 'db_port': 5432}

# 显式:用类定义结构
from dataclasses import dataclass

@dataclass
class DatabaseConfig:
    db_host: str
    db_port: int
    db_name: str
    # 必填字段,缺少任何一个会报错

# 使用
config = DatabaseConfig(
    db_host='localhost',
    db_port=5432,
    db_name='mydb'
)

def connect_db(config: DatabaseConfig):
    # 类型明确,IDE 有自动补全
    host = config.db_host
    port = config.db_port

好处

  • 类型错误在编写时就能发现
  • 必填字段强制提供
  • IDE 提供自动补全和类型检查
  • 重构时能追踪所有使用

力量2:抽象基类

# 显式:用接口定义边界
from abc import ABC, abstractmethod

class Service(ABC):
    """所有服务必须实现的接口"""

    @abstractmethod
    def start(self):
        """启动服务"""
        pass

    @abstractmethod
    def stop(self):
        """停止服务"""
        pass

# 实现
class EmailService(Service):
    def start(self):
        print("Email service started")

    # 如果忘记实现 stop(),实例化时会报错
    # TypeError: Can't instantiate abstract class EmailService
    # with abstract method stop

好处

  • 强制实现所有必需方法
  • 不需要依赖文档或记忆
  • 编译/运行时立即检查

力量3:访问控制

# 显式:真正的私有属性
class BankAccount:
    def __init__(self, balance):
        self.__balance = balance  # 双下划线 = name mangling

    def deposit(self, amount):
        if amount > 0:
            self.__balance += amount

    def get_balance(self):
        return self.__balance

# 外部无法直接访问
account = BankAccount(1000)
# account.__balance  # AttributeError
# 只能通过公开方法
account.deposit(100)
print(account.get_balance())  # 1100

更好:用属性封装

class BankAccount:
    def __init__(self, balance):
        self._balance = balance

    @property
    def balance(self):
        """只读属性"""
        return self._balance

    def deposit(self, amount):
        if amount > 0:
            self._balance += amount

# 使用
account = BankAccount(1000)
print(account.balance)  # 1000
# account.balance = 2000  # AttributeError: can't set attribute

隐式 vs 显式的对比

维度 隐式边界 显式边界
表达方式 文档、注释、约定 代码、类型、接口
执行方式 人工检查、代码审查 编译器、运行时检查
可靠性 依赖记忆和自律 强制执行
新人友好 需要学习隐性规则 代码即文档
重构安全 容易遗漏 工具辅助,全局追踪
错误发现 运行时、用户反馈 开发时、编译时

何时使用隐式边界

场景1:团队小且稳定

# 3人团队,大家都知道规则
# 隐式约定:所有 API 响应都是 JSON
@app.route('/api/users')
def get_users():
    return jsonify(users)  # 约定俗成

场景2:规则简单明显

# 隐式约定:测试文件以 test_ 开头
# test_user.py
# test_order.py
# 这种约定足够简单,工具也支持

场景3:性能考虑

# Python 的 _ 前缀比双下划线更快
class FastClass:
    def __init__(self):
        self._value = 0  # 约定私有,但不强制

何时必须用显式边界

场景1:安全关键

# 必须:支付、认证等关键逻辑
class PaymentProcessor:
    def __init__(self, api_key: str, secret: str):
        self.__api_key = api_key  # 强制私有
        self.__secret = secret

    def process(self, amount: Decimal):  # 明确类型
        # 不能依赖约定
        ...

场景2:团队规模大

# 50人团队,不能指望所有人记住约定
# 必须用类型系统、接口定义边界

from typing import Protocol

class Repository(Protocol):
    """定义所有 Repository 的契约"""
    def save(self, entity): ...
    def find(self, id): ...

场景3:API 公开

# 对外暴露的 API 必须显式
from pydantic import BaseModel

class CreateUserRequest(BaseModel):
    username: str
    email: str
    password: str

    # 自动验证、生成文档、类型检查

从隐式到显式的演化

阶段1:识别隐式规则

# 发现:团队有很多"约定"
# - "数据库操作都在 repository 里"
# - "业务逻辑在 service 里"
# - "HTTP 处理在 controller 里"

# 问题:新人不知道,老人会忘记

阶段2:提取接口

# 把约定变成接口
class Repository(ABC):
    @abstractmethod
    def save(self, entity): ...

    @abstractmethod
    def find(self, id): ...

class Service(ABC):
    @abstractmethod
    def execute(self, request): ...

阶段3:类型约束

# 用类型系统强制约定
from typing import TypeVar, Generic

T = TypeVar('T')

class Repository(Generic[T], ABC):
    @abstractmethod
    def save(self, entity: T) -> T: ...

    @abstractmethod
    def find(self, id: int) -> T | None: ...

# 使用
class UserRepository(Repository[User]):
    def save(self, entity: User) -> User:
        # 类型明确,无法传入 Order
        ...

阶段4:运行时验证

# 对于无法静态检查的,运行时验证
from pydantic import BaseModel, validator

class User(BaseModel):
    username: str
    email: str
    age: int

    @validator('age')
    def age_must_be_positive(cls, v):
        if v < 0:
            raise ValueError('age must be positive')
        return v

# 运行时自动验证
user = User(username='alice', email='alice@example.com', age=-5)
# ValidationError: age must be positive

对使用 AI 的程序员的建议

AI 生成代码的常见问题

问题1:倾向隐式约定

# AI 可能生成
def process_data(data):
    # 假设 data 是字典,有特定的 key
    name = data['name']
    age = data['age']
    # 没有类型提示,没有验证

# 应该改成
from typing import TypedDict

class UserData(TypedDict):
    name: str
    age: int

def process_data(data: UserData):
    name = data['name']  # 类型检查
    age = data['age']

问题2:不使用抽象

# AI 可能生成
class EmailNotifier:
    def send(self, message):
        # 直接实现
        smtp.send(message)

class SMSNotifier:
    def send(self, message):
        # 另一个实现
        twilio.send(message)

# 应该改成
class Notifier(ABC):
    @abstractmethod
    def send(self, message): ...

class EmailNotifier(Notifier):
    def send(self, message):
        smtp.send(message)

如何改进

提示词策略

❌ 不好的提示:
"写一个用户管理的代码"

✅ 好的提示:
"写一个用户管理的代码,要求:
1. 使用 dataclass 或 Pydantic 定义数据模型
2. 使用 ABC 定义 Repository 接口
3. 添加完整的类型提示
4. 使用依赖注入
5. 边界要显式,不依赖隐式约定"

审查清单

真实案例

案例1:Stripe API 的显式设计

Stripe 的 Python SDK 大量使用类型提示和对象:

# 不是这样(隐式)
stripe.charge({'amount': 1000, 'currency': 'usd'})

# 而是这样(显式)
charge = stripe.Charge.create(
    amount=1000,
    currency='usd',
    source='tok_visa'
)
# 返回类型化对象,有自动补全

为什么?

  • API 是公开的,用户遍布全球
  • 不能依赖文档或约定
  • 必须通过类型和对象强制正确使用

案例2:Django vs Flask

Django(更显式)

from django.db import models

class User(models.Model):  # 继承基类,强制结构
    username = models.CharField(max_length=100)
    email = models.EmailField()

    # 强制定义字段类型、约束

Flask(更隐式)

# 配置是字典
app.config['DATABASE_URI'] = 'sqlite:///db.sqlite3'
# 依赖约定的 key 名称

结果

  • Django:更多样板代码,但边界清晰
  • Flask:更灵活,但需要更多约定

检查清单

完成代码后,检查:

如果有任何一项答案是"否",考虑增加显式边界。

总结

隐式边界 vs 显式边界的核心观点:

  1. 隐式依赖人,显式依赖代码——代码比记忆可靠
  2. 隐式灵活但脆弱——适合小团队、简单场景
  3. 显式严格但安全——适合大团队、复杂系统
  4. 边界应该逐步显式化——随系统增长,约定变规则
  5. 工具辅助显式边界——类型系统、接口、验证库

记住:

约定会被打破,但代码会强制执行。

好的边界不需要记住,只需要遵守。

显式边界让系统自我解释,而非依赖部落知识。

posted @ 2025-11-29 21:55  Jack_Q  阅读(0)  评论(0)    收藏  举报