跳转到内容

通用编码原则

单一职责、KISS、DRY、YAGNI


规则:一个类、函数或模块只负责一件事,只有一个引起它变化的原因。

[Requirement] 一个函数如果做了”读取配置 + 解析参数 + 调用接口 + 写日志”四件事,必须拆分。

示例:

# 正确:单一职责
def load_config(path: str) -> dict:
    with open(path) as f:
        return yaml.safe_load(f)
 
def build_request(config: dict, params: dict) -> Request:
    return Request(url=config["url"], headers=config["headers"], body=params)
 
def call_api(req: Request) -> Response:
    return http.post(req.url, headers=req.headers, json=req.body)

反模式:

# 错误:一个函数干了所有事
def process(path, params):
    config = yaml.safe_load(open(path))
    req = Request(config["url"], config["headers"], params)
    resp = http.post(req.url, headers=req.headers, json=req.body)
    logger.info("请求完成")
    return resp.json()

规则:保持简单(Keep It Simple, Stupid)。优先选择最直接、最容易理解的实现,不为”可能的扩展”提前抽象。

[Standard] 能用 if/else 解决的,不要上策略模式 + 工厂 + 反射的组合拳。

[Suggestion] 评审时如果 3 个候选方案都能满足需求,选行数最少、依赖最少的那个。

示例:

# 正确:简单直接
def get_status(code: int) -> str:
    if code == 0:
        return "成功"
    if code == 1:
        return "失败"
    return "未知"

反模式:

# 错误:过度设计
class StatusStrategy(ABC):
    @abstractmethod
    def get_name(self) -> str: ...
 
class SuccessStrategy(StatusStrategy):
    def get_name(self): return "成功"
 
class FailStrategy(StatusStrategy):
    def get_name(self): return "失败"
 
class StatusFactory:
    _registry = {0: SuccessStrategy, 1: FailStrategy}
    @classmethod
    def create(cls, code):
        return cls._registry.get(code, UnknownStrategy)()
 
StatusFactory.create(code).get_name()

规则:不要重复(Don’t Repeat Yourself)。同一段逻辑不应在多处复制粘贴;当出现第二次重复时,就该抽取。

[Standard] 超过 3 行的相同代码块出现两次以上,必须抽取为函数或公共方法。

[Caution] 不要为了 DRY 而把不相关的逻辑强行合并,强行抽象比重复更难维护。

示例:

# 正确:抽取公共逻辑
def sanitize(text: str) -> str:
    return text.strip().lower().replace(" ", "_")
 
user_name = sanitize(raw_name)
file_name = sanitize(raw_file)

反模式:

# 错误:重复逻辑散落各处
user_name = raw_name.strip().lower().replace(" ", "_")
file_name = raw_file.strip().lower().replace(" ", "_")
tag_name  = raw_tag.strip().lower().replace(" ", "_")

规则:你不会需要它(You Aren’t Gonna Need It)。只为当前确认的需求写代码,不为”将来可能”预留接口、参数或扩展点。

[Requirement] 不写”为了以后可能会用到”的预留字段、参数、配置项。

[Caution] 预留扩展性不等于写死未来需求;保持代码可重构即可,不必提前实现。

示例:

# 正确:只实现当前需要的
def send_email(to: str, subject: str, body: str) -> None:
    smtp.send(to, subject, body)

反模式:

# 错误:为"将来可能"加了一堆没用的参数
def send_email(to, subject, body, cc=None, bcc=None, attachments=None,
               priority="normal", template_id=None, locale="zh-CN",
               retry=3, timeout=30, use_tls=True, ...):
    ...

同上文”单一职责原则”。一个类只做一件事。

规则:对扩展开放,对修改关闭。新增功能应通过扩展(新增类/函数)实现,而非修改已有代码。

[Standard] 新增业务分支时,优先用”新增实现类 + 注册”的方式,而不是在已有函数里堆 elif。

示例:

# 正确:新增类型只需新增一个类
class Notifier(ABC):
    @abstractmethod
    def send(self, msg: str): ...
 
class EmailNotifier(Notifier): ...
class SmsNotifier(Notifier): ...
 
# 注册器
_registry = {"email": EmailNotifier, "sms": SmsNotifier}
 
def notify(channel: str, msg: str):
    _registry[channel]().send(msg)

规则:子类必须能在不破坏程序正确性的前提下替换父类。子类不应削弱父类的前置条件,也不应加强后置条件。

[Requirement] 子类重写方法时,入参类型不能比父类更严格,返回值类型不能比父类更宽泛。

反模式:

# 错误:Square 不能替换 Rectangle
class Rectangle:
    def set_width(self, w): self.w = w
    def set_height(self, h): self.h = h
    def area(self): return self.w * self.h
 
class Square(Rectangle):
    def set_width(self, w):  self.w = self.h = w   # 改变了行为契约
    def set_height(self, h): self.w = self.h = h

规则:客户端不应被迫依赖它用不到的接口。把胖接口拆成多个小接口。

[Standard] 一个接口如果超过 5 个方法,考虑按职责拆分。

规则:高层模块不应依赖低层模块,二者都应依赖抽象;抽象不应依赖细节,细节应依赖抽象。

[Standard] 业务逻辑层不直接 import 具体的数据库实现,而是依赖 Repository 抽象接口。

示例:

# 正确:依赖抽象
class UserRepository(ABC):
    @abstractmethod
    def get(self, uid: int) -> User: ...
 
class UserService:
    def __init__(self, repo: UserRepository):   # 依赖抽象,而非 MySQLRepo
        self.repo = repo
 
class MySQLUserRepo(UserRepository): ...   # 细节依赖抽象