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 显式边界的核心观点:
- 隐式依赖人,显式依赖代码——代码比记忆可靠
- 隐式灵活但脆弱——适合小团队、简单场景
- 显式严格但安全——适合大团队、复杂系统
- 边界应该逐步显式化——随系统增长,约定变规则
- 工具辅助显式边界——类型系统、接口、验证库
记住:
约定会被打破,但代码会强制执行。
好的边界不需要记住,只需要遵守。
显式边界让系统自我解释,而非依赖部落知识。

浙公网安备 33010602011771号