Python OOP中的显式优于隐式:让代码可测、可读、可维护的实操指南
发布时间:2026/10/9 8:06:19 锦皓数字建站

1. 显式优于隐式到底在说什么1.1 显式不是风格偏好而是一条工程契约Explicit is better than implicit这句话我背下来很早真正理解却花了很长一段时间。写Python这些年接手过不少老项目也重构过一些自己半年后都看不下去的代码。大部分难啃的场景最后都不是语法问题、不是性能问题而是隐式埋下的雷构造方法里悄悄连了数据库、对象状态在好几个地方被改、一个类能响应你根本不知道的属性。这篇是Python OOP设计思想系列的第15篇我想认真聊一聊显式是一种设计责任这件事。显式的本质不是把代码写啰嗦而是把行为、依赖、状态变化全部放在阳光下。用最直白的话来说一段代码是否隐式取决于读代码的人能不能在调用点直接推断出会发生什么。举一个生活里的类比。同样是问路一个人跟你说往前走一会儿就到了另一个人跟你说直走200米第二个路口右转。前者在信息完整的情况下确实也能到达但你在路上会一直怀疑自己是不是走过了后者虽然多说了几个字但每一步都可以对照验证。代码里的隐式设计就像那个往前走走就到了的指路人——写代码的人心里有一条完整路线但读代码的人只能靠猜。隐式代码在单行上看往往很简洁但它在系统层面把复杂度转移到了人的记忆里。一个类越是复杂、协作的对象越多这种记住上下文的负担就越重。等到你来维护这段代码的时候之前的约定已经不存在了只剩下一堆看起来很高深、跑起来全靠运气的对象。所以我说显式不是一种风格偏好而是一条工程契约你写的每一个类、每一个方法都要对未来的维护者负责。如果你不能保证他在不看文档的情况下安全地调用你的代码那就在设计上先做减法。1.2 为什么OOP场景下显式尤其重要我见过不少从脚本转过来写OOP的开发最容易犯的毛病就是把OOP当函数式用。什么意思呢写脚本的时候输入输出就摆在那数据流是线性的函数里做了什么基本一眼能看到。但换成对象之后事情发生了变化对象封装了状态方法可以在任意时间被调用调用一个方法可能改变对象内部状态而这个状态又会影响后续所有方法的行为。也就是说在OOP里状态是隐式积累的。你看到的每一个方法调用只是冰山一角真正决定程序行为的是这个对象在之前经历了什么。这种情况下如果类的设计者再不主动把行为边界、依赖关系、状态流转显式化整个系统就会变成一锅粥。我在做技术评审的时候经常问一个问题这个类的核心状态有哪些取值谁在修改它如果回答的人需要翻代码、查日志才能答上来那么这个类一定存在隐式设计的问题。换句话说对象的状态变化应当有唯一的、可审计的入口而不是散落在十个回调函数里每人改一点。显式设计在OOP里的价值我用三句话概括可测——依赖是显式传入的测试可以轻松替换可读——方法签名和行为边界一目了然不需要深入函数体猜测可维护——状态流转有明确规则改动影响面可控。接下来的内容我会从具体操作层面展开讲清楚怎么一步步把隐式设计改成显式设计同时也会分享一些我踩过的坑。2. 动手落实五个让对象形状清晰可见的实操习惯2.1 构造方法不藏副作用把静默初始化改成可预测的注入先看第一种我最常遇到的隐式设计它在很多老项目里几乎成了标配# 隐式版本构造方法里悄悄干了很多事 class ReportService: def __init__(self): self.config load_config_from_env() self.db_client DatabaseClient(self.config.db_url) self.template read_template(default) # 这里会触发一次IO self.logger setup_logger()这段代码的问题在哪里第一你创建Service()这个对象的时候它已经读了环境变量、建立了数据库连接、读了模板文件。如果此时数据库不可用程序会直接抛异常。第二测试环境下你想用一个假的数据库客户端几乎不可能因为依赖关系是写在构造方法内部的外部无法干预。显式的做法是把所有外部依赖交给调用方决定# 显式版本构造方法只接收依赖不主动产生外部动作 class ReportService: def __init__( self, config: AppConfig, db_client: DatabaseClient, template: Template, logger: Logger, ): self._config config self._db_client db_client self._template template self._logger logger两个版本一对比差别就出来了。显式版本里ReportService的依赖被完整写在了构造签名里创建一个对象时你清楚地知道它需要什么。而隐式版本依赖关系全靠看代码才知道。更关键的是隐式版本把连接数据库这种副作用放置在了对象创建阶段对象的诞生就绑定了外部资源一旦资源有问题对象连创建都做不到。我个人的经验是涉及IO、网络、随机数、时间等外部因素的依赖全部显式注入。只有纯数据逻辑才允许在构造方法里直接初始化。这样做之后测试再也不用模拟环境变量、再也不用提前启动数据库了。2.2 具名参数、类型标注与值的限定让调用者不需要猜Python的灵活有时候反而是隐式的温床。最常见的两种**kwargs参数黑洞以及用dict当万能传参对象。先看一个典型的隐式写法def send_invoice(self, invoice_id: int, **kwargs): # 谁能告诉我kwargs里可以有哪些键默认值是多少 # 是email还是recipient还是to_address这种代码调用时全靠经验和勇气。写的人为了方便把可选项全塞进kwargs读的人只能去翻函数体找到kwargs.get(email, None)然后还要担心如果传入的键名不对会发生什么。极端情况下有的人干脆在函数体里加了一堆kwargs.get()让原本清晰的处理逻辑淹没在有没有值的判断里。更值得警惕的是可变默认参数这算是Python圈里的经典陷阱了def add_to_cache(self, key: str, value: object, cache: list []): cache.append((key, value))共享的可变默认参数会让所有调用这个方法的对象共享同一个list而这种共享很容易被忽略。显式改法是默认值改为None在函数体内部重新实例化def add_to_cache(self, key: str, value: object, cache: list | None None): cache cache if cache is not None else [] cache.append((key, value))对于可选项我更推荐两种替代方案一种是直接写成具名参数并鼓励调用方用关键字传参另一种是定义一个小的配置对象。比如上面的send_invoice可以改成def send_invoice( self, invoice_id: int, *, email: str | None None, cc: list[str] | None None, attach_pdf: bool True, ): ...这里用*强制后续参数必须以关键字形式传入避免位置参数一多就搞不清谁是谁。同时每个参数都有名字、有类型标注调用点一眼就能看出这个方法的完整行为边界。这样做的好处是错误的键名会在调用时直接报错而不是在函数内部运行了几十行之后才因为某个KeyError崩溃。2.3 配置与环境读取显式化从模块级get_config到显式传参配置读取可能是隐式设计的重灾区。很多Python项目里都有这样一个模块# config.py DB_URL os.getenv(DB_URL) DEBUG os.getenv(DEBUG) true然后在业务类里直接import这个模块# order_service.py import config class OrderService: def create_order(self, ...): db DatabaseClient(config.DB_URL, debugconfig.DEBUG)这段代码的问题在于OrderService对配置的依赖是完全隐式的。你光看OrderService的代码不知道它还需要config里的什么东西。更麻烦的是测试你要构造不同配置来测试就得先设置环境变量、再重新导入模块、还要提防模块缓存导致配置不更新。显式设计的做法是把配置本身建模成一个对象并且从构造函数里传入from dataclasses import dataclass, field dataclass(frozenTrue) class AppConfig: db_url: str debug: bool False report_template_path: str templates/default.html然后在入口处统一组装def main(): config AppConfig( db_urlos.getenv(DB_URL, sqlite:///dev.db), debugTrue, ) db_client DatabaseClient(config.db_url) order_service OrderService(configconfig, db_clientdb_client)这样做有很多好处。测试的时候你不需要去污染环境变量直接构造一个AppConfig实例传入就行def test_order_service(): config AppConfig(db_urlsqlite:///test.db) order_service OrderService(configconfig, db_clientFakeDbClient())配置读取显式化的本质是把类从哪里拿配置这件事从不可见的全局环境变成了可见的参数传递。我对这个改动的感受是它一开始会增加代码量但换来的是每个类的依赖列表就是它的业务边界面板。以后维护的时候你拿到一个类的构造函数就知道它需要什么不需要满项目搜索import。2.4 用枚举和状态表取代隐式字符串判断字符串比较可能是Python代码里最常见的隐式状态表达方式。看这个例子if order.status paid: self._send_notification(order)这段代码单独看没问题问题在于paid这个字符串并没有被定义在任何地方。不同模块可能写出各种变体paid、Paid、已支付、PAID最后在某个深夜里因为一个大小写不匹配系统出bug了。显式的做法是用枚举来定义状态的合法集合from enum import Enum class OrderStatus(Enum): PENDING pending PAID paid SHIPPED shipped COMPLETED completed CANCELLED cancelled REFUNDED refunded然后用枚举成员替代字符串order.status OrderStatus.PAID if order.status is OrderStatus.PAID: self._send_notification(order)可能有人会觉得这是多此一举但我实际用下来枚举在IDE补全、类型检查、集中管理合法值上价值非常大。你写OrderStatus.PAID时IDE会给你补全你写字符串paid的时候IDE什么都帮不了你。更重要的枚举让状态一共有哪些这件事变成了显式信息——任何新接手的人打开OrderStatus这个类就知道订单在整个生命周期中会有哪些状态而不是靠翻数据库里的值来猜。这里有一个我踩过的坑需要提醒如果数据库里已经存了历史字符串值改成枚举时要注意兼容映射。比如库里同时存在paid和Paid就需要在转换时做归一化处理否则线上会在迁移后出现一堆KeyError。2.5 返回值与异常路径显式化要么给结果要么给原因还有一种隐式设计藏在返回值里。最常见的表现是方法有时候返回对象有时候返回None而且没有跟调用方说清楚。def find_user(self, user_id: int): user self._db.get(user_id) if user: return user # 这里没有return函数隐式返回None调用方怎么写通常是这样的user find_user(42) if user is not None: ...问题在于None表达的信息太隐式了。它可能是用户不存在可能是数据库连接失败也可能是参数不合法。调用方根本无从分辨。遇到这种情况我建议显式地做三件事第一用Optional[T]标注可能为空的返回值至少让调用方知道这个函数会返回Nonedef find_user(self, user_id: int) - User | None:第二区分正常的空结果和异常路径。如果用户不存在是正常业务情况返回None没问题但如果数据库连接失败这属于异常应该显式抛出def find_user(self, user_id: int) - User | None: try: user self._db.get(user_id) except DBConnectionError as e: raise ServiceUnavailableError(db unavailable) from e return user第三避免通过修改可变对象再返回True表示成功的隐式写法。比如def update_user_name(self, user: User, new_name: str) - bool: if len(new_name) 2: return False user.name new_name return True这段代码同时做了修改传入对象和返回成功与否两件事而且调用方如果不仔细看很容易忽略它修改了user对象。更显式的做法是或者让方法专注修改对象本身或者返回一个新对象成功与否通过异常或结果对象来表达。显式返回路径的核心逻辑是让调用方不需要把返回了什么和发生了什么两件事混在一起猜。要么给结果要么给原因千万不要只给一个不明不白的None或True。3. OOP中常见的隐式设计陷阱自查3.1 魔法方法当万能入口__getattr__带来的无边界的对象Python的魔法方法功能强大但也容易被滥用。其中__getattr__是我见过的最容易制造隐式设计的元凶之一。class DynamicProxy: def __init__(self, target): self._target target def __getattr__(self, name): return getattr(self._target, name)这样设计看似灵活但它带来的问题非常明显。你拿到了这个proxy对象想看看它有哪些方法你看到的都是被动态转发的。IDE补全失效、类型检查器报错、代码里拼错了任何一个属性名都不会在报错层面直接暴露而是会在运行时抛出一个在__getattr__里产生的AttributeError。这个错误发生的位置可能离真正的拼错点很远。我自己刚学Python时特别痴迷这种写法觉得一切都能转发很酷。后来在一个项目里我用__getattr__做了一个配置代理允许config.any_key返回任意配置项。结果线上出了一个诡异的bug最后定位到是因为我把config.timeout写成了config.time_out而__getattr__恰好吞掉了这个拼写错误返回了None程序在后面默默用None去加减乘除。显式设计的约束是能用普通方法就绝不用魔法方法。__getattr__这类魔法方法只适合在框架层、代理层等极少数场景使用而且使用前一定要想清楚——我能不能用一个显式的方法来覆盖这个需求比如config.get(timeout, default3)虽然多了几个字符但每个键的拼写错误都会立即被发现。3.2 运行时动态加属性让对象的形状变成一个谜与__getattr__类似的是运行时通过setattr或直接obj.attr value给对象加属性。这在Python里合法但属于隐式设计里非常危险的一种。举个例子class Order: def __init__(self, order_id: int): self.order_id order_id order Order(123) order.discount 0.8 # 它本来不在类定义里这在运行时完全合法。但如果哪个同事在某处代码里读了order.discount然后发现某个订单没有这个属性程序就会在运行时崩溃。更糟糕的是你没法通过看类的定义来了解这个对象的完整结构——你只能去搜整个项目看看哪里有.discount 的赋值。对于这种问题我建议的显式对策是类的所有字段都写进类的定义里。如果对象有动态扩展需求应该定义一个显式的方法或数据结构比如class Order: def __init__(self, order_id: int): self.order_id order_id self.extra: dict[str, object] {} def update_extra(self, key: str, value: object) - None: self.extra[key] value这时候所有人都知道extra是一个显式的扩展点而不是这个类未来会长出什么属性完全看运气。顺带一提如果你真的希望限制属性只能有固定的几个字段可以用__slots__。但注意它在继承场景下有坑需要谨慎。3.3 全局单例与模块级可变状态最隐蔽的隐式依赖最后要说的是全局单例和模块级可变状态这是我排坑排到最痛的一类。一个模块加载时我们就确定了它的全局状态而这个状态可以被任何import它的模块读取甚至修改。这种模块与模块之间构成了一条完全隐式的通信通道。举一个企业级项目里非常常见的例子# db.py _db_connection None def get_db(): global _db_connection if _db_connection is None: _db_connection connect() return _db_connection所有业务类都直接从get_db()拿数据库连接。这种写法写起来确实省事但测试是灾难你想用假的数据库连接得先重置_db_connection这个全局变量而且如果两个测试用例并发执行数据库连接就会被交叉污染。更隐蔽的模块级可变状态还可能带来性能问题。我遇到过的一个bug就是模块加载时从配置文件读取了一个值存为全局变量结果运维在管理后台改了配置后发现程序里读取的一直是旧值——因为全局变量只在加载时被赋了一次值。这就是隐式的时间依赖。显式化的方向很清晰对象与对象之间的依赖通过构造参数传递对象与外部资源之间的依赖通过接口注入。全局单例只用于进程级的共享资源管理绝不应成为业务类默认的取用通道。4. 实操案例把一个隐式订单状态管理重构为显式设计4.1 原始实现状态判断散落、流转入口不统一的日常噩梦下面我用一个具体案例完整演示一次从隐式到显式的重构过程。假设我们有一个订单类最初的实现长这样class Order: def __init__(self, order_id: int): self.order_id order_id self.status pending # 可能的值pending/paid/shipped/completed/cancelled self.paid_at None self.shipped_at None def mark_paid(self): self.status paid self.paid_at time.now() send_email(self.order_id, paid) log_metric(order_paid) def ship(self): if self.status ! paid: raise ValueError(只有已支付的订单才能发货) self.status shipped self.shipped_at time.now() notify_warehouse(self.order_id)这套代码看起来还能跑但它隐藏的问题不少第一状态字符串散落在各个方法里不同模块可能约定写出不同值。第二mark_paid()里一次性做了三件事改状态、记录时间、发邮件、打日志。这些副作用混在一起测试时你想只验证状态变化必须同时处理邮件和日志。第三发货时校验了状态但退款呢取消呢没有任何统一的校验入口每个方法里自己写一段if写得多了难免漏。4.2 重构第一刀枚举类型把状态集合显式框定第一步先定义订单状态的枚举以及一张显式的状态转移表from enum import Enum class OrderStatus(Enum): PENDING pending PAID paid SHIPPED shipped COMPLETED completed CANCELLED cancelled REFUNDED refunded # 显式定义哪些状态可以转移到哪些状态 TRANSITIONS { OrderStatus.PENDING: {OrderStatus.PAID, OrderStatus.CANCELLED}, OrderStatus.PAID: {OrderStatus.SHIPPED, OrderStatus.REFUNDED}, OrderStatus.SHIPPED: {OrderStatus.COMPLETED, OrderStatus.RETURNED}, OrderStatus.COMPLETED: set(), OrderStatus.CANCELLED: set(), OrderStatus.REFUNDED: set(), }有了这张表我们就可以在订单类里实现一个统一的、显式的状态变更入口class Order: def __init__(self, order_id: int): self.order_id order_id self._status OrderStatus.PENDING property def status(self) - OrderStatus: return self._status def _transition_to(self, new_status: OrderStatus) - None: if new_status not in TRANSITIONS.get(self._status, set()): raise InvalidTransitionError( f不能从 {self._status} 转移到 {new_status} ) self._status new_status def mark_paid(self) - None: self._transition_to(OrderStatus.PAID) self.paid_at time.now()注意我把状态改动的逻辑集中到了_transition_to这一个私有方法里。以后无论谁来添加新的业务方法只要是改状态都必须走这个入口非法流转会统一抛出异常而不是散落在各处if里。这一刀的好处是立竿见影的。原来ship()里的if self.status ! paid校验现在被TRANSITIONS表代替了以后如果要新增已发货可以申请退款这种规则只需要改TRANSITIONS表而不需要去翻所有业务方法。4.3 重构第二刀依赖注入与副作用剥离让核心逻辑没有隐藏动作重构完状态管理第二步是处理副作用。原来的mark_paid()里直接调用了send_email和log_metric这两个函数从哪里来的大概率是从某个util模块import进来的。这种隐式依赖在测试时逼着你mock掉两个函数。显式设计的处理方式有两种一是把依赖通过构造函数注入二是在更上层的应用服务里编排副作用。我更推荐第二种因为它能让Order类保持纯粹的业务逻辑。实际操作时我会把核心状态变更和外部通知拆开class Order: def __init__( self, order_id: int, time_provider: Callable[[], float] none, ): self.order_id order_id self._status OrderStatus.PENDING self._time_provider time_provider or time.time def mark_paid(self) - None: self._transition_to(OrderStatus.PAID) self.paid_at self._time_provider() # 应用服务负责编排与副作用 class OrderApplicationService: def __init__(self, order_repo, email_sender, metrics): self.order_repo order_repo self.email_sender email_sender self.metrics metrics def mark_order_paid(self, order_id: int) - Order: order self.order_repo.get(order_id) order.mark_paid() self.order_repo.save(order) self.email_sender.send(order_id, paid) self.metrics.increment(order_paid) return order这样拆分之后Order类变成了一块纯逻辑的积木它只负责维持自身状态的一致性外部副作用统一收敛到应用服务层。这样做的好处是Order可以独立测试且不需要mock邮件和日志而应用服务是一次性的编排代码所有外部依赖都通过构造参数显式传入测试时完全可控。4.4 重构之后的测试体验mock少了确定性多了重构前写测试是什么感觉要patch邮件发送、patch日志、patch时间、patch全局数据库连接一个简单测试能写上二十行setup。重构之后再写体验完全不同def test_mark_paid_transitions_status(): fake_time iter([1000.0, 1001.0]) order Order(order_id1, time_providerlambda: next(fake_time)) order.mark_paid() assert order.status is OrderStatus.PAID assert order.paid_at 1000.0 def test_cannot_ship_before_paid(): order Order(order_id1) with pytest.raises(InvalidTransitionError): order.ship() def test_mark_paid_with_fake_notifications(): order Order(order_id1) notifier FakeNotifier() application_service OrderApplicationService( order_repoFakeRepo([order]), email_sendernotifier, metricsFakeMetrics(), ) application_service.mark_order_paid(1) assert notifier.last_order_id 1看到区别了吗每次测试只需要构造尽可能少的依赖并且所有依赖都是显式的Fake对象。如果再遇到新同事来改这个类他不需要理解项目里那么多全局约定只看构造函数的参数和TRANSITIONS表就能明白整个状态机的行为边界。重构的收益不是代码变短了而是每次改状态的路径都可以枚举、可以审计、可以测试。5. 显式不要做过头平衡、自查与团队落地5.1 显式不等于冗长什么时候可以适当隐式看到这里可能有人会担心什么都要显式代码会不会变得很长、很啰嗦说实话我在实践初期确实犯过过度设计的毛病把每个方法的参数都做了精细的配置对象结果一个原本两行的函数被撑到二十行。慢慢磨合之后我总结出一个判断标准如果隐式的那部分是所有人都能安全默认的那么它可以保留如果隐式会影响正确性、可测性或可排错性就必须显式。举几个可以正大光明隐式的例子。Python的with语句隐式管理上下文这是语言级约定不需要显式传一个context对象进去。内置魔法方法比如__len__、__iter__本身就是语言协议这时候用它们是显式地告诉读者这个对象支持迭代而不是隐式偷懒。再比如方法内部非常局部的临时变量在一个函数体内传递没必要塞进类属性但如果你要把一个变量从A类传到B类再传到C类那它就必须成为显式参数。真正要警惕的不是隐式三个字本身而是隐式地影响外部行为。如果一个隐式约定只影响函数内部的局部逻辑它危害有限但如果它影响了对象状态、外部IO、全局配置那就要立刻显式化。另外一个常见的误区是参数数量。有的人为了显式把一个方法写到十二个参数。这其实不是显式而是散装。遇到这种情况正确做法是把关联参数凝聚成一个值对象比如ShippingAddress、ReportOptions一个参数传递一个完整的领域概念而不是让调用者逐一传入城市、街道、邮编、联系电话。显式追求的是每个概念都清晰不是每个值都堆在签名里。5.2 五个提问快速定位代码里的隐式设计经验积累得多了我梳理出一套自查问题。每次Review代码或者排查线上问题的时候我都会沿着这五个问题走一遍基本能快速定位到隐式设计的根子。第一问这个类在构造时会做哪些不可见的动作如果答案是会连数据库、会发邮件、会读环境变量那它的初始化就是隐式的必须改成显式注入。第二问这个对象的状态有哪些取值谁在修改它有没有唯一入口如果状态可以在类外被随手赋值、可以被多个模块修改那它就是隐式状态流。第三问这个方法的参数是否完整描述了它的输入如果参数里出现了**kwargs、泛泛的dict、或者依赖模块级全局变量那输入边界就是隐式的。第四问这个类的依赖从import和签名能不能看出来如果光看import列表就已经长到要滚动翻页而且大量import来自自定义模块的全局状态那就是隐式依赖堆叠。第五问一个不熟悉业务的新手能不能在10秒内说出这个类的核心职责如果不能说明它的职责边界本身是模糊的。这里模糊的隐式已经不是设计风格问题而是架构问题了。这五个问题我在团队里也推行过。做法很简单每周代码评审时随机挑一个类大家一起用这五个问题过一遍。通常过到第三个问题就会有人开始冒汗因为一旦深究很多类都经不起这种拷问。但没关系能发现隐式设计的位置就说明已经找到了重构的起点。5.3 Review清单把显式设计写进流程单靠个人自觉显式设计很难在团队里稳定落地。我的经验是把部分显式要求写进代码评审的Checklist里让规则变得可执行。目前我在团队里推的Review清单包括四条核心要求第一新写的类禁止在构造方法里执行IO操作或访问全局配置。所有外部依赖必须通过构造参数传入。如果实在有无法避免的默认值也要通过类级别的常量显式声明。第二任何方法如果改变了对象状态必须通过统一的、有校验的状态迁移入口禁止在类外部直接修改关键状态字段。数据库的update操作也一样都应该收敛到Repository层。第三方法的返回值类型必须显式标注。不能返回None的地方要么用Optional标注要么抛异常。调用方在使用返回值之前必须能够从类型标注上判断出空值情况。第四所有跨模块共享的可变状态必须显式封装成对象并通过参数传递禁止直接import模块级变量进行读写。这四条不是硬性防住了所有隐式设计但它们把最容易出问题的几个点框住了。对于一个已经线上运行的老项目我建议不要一次性推翻重写而是挑最痛的那个模块先做样板重构其他模块看到好处后再跟上。我在实际推动过程中做过一个订单模块的示范改造两个星期后另外两个服务组主动找到我要求提供同样的Review检查清单。6. 写在最后经验与教训讲到这里想起了一个印象特别深的踩坑经历。有一年我们上线了一个新的支付回调服务代码写得很快但生产环境出了个诡异的问题配置中心里改了某个功能的开关服务里的行为一直不变。排查了半天最后发现问题出在一个模块加载时的全局变量上。它在服务启动时读了一次配置之后所有请求都拿着这个旧值。那行代码本身没有任何逻辑错误是一个再普通不过的IS_FEATURE_ON os.getenv(FEATURE_ON) 1。但正是这种隐式的时间依赖让运行时行为和配置源脱了节。那次之后我把自己的一个原则升级了面向对象的设计不仅要对对象的状态负责也要对对象的信息来源负责。一个对象从哪里拿到配置、从哪里拿到数据库连接、从哪里拿到时间都必须是一眼可见的。如果这些信息是隐式的那么你在启动时的一切正常、测试时的一切顺利都只是暂时的运气迟早有一天会在某个深夜变成事故。相对地显式设计带来的安心感是非常直接的。重构订单模块的那段时间每次改动状态机规则我只需要改TRANSITIONS表再跑一遍测试就能立刻知道影响面每次新写一个业务类我只要照着依赖全部注入、状态有唯一入口、副作用交给上层编排的套路走后面维护起来就再也没被这行代码哪里来的这种问题折磨过。如果你也想开始实践这个思路我的建议是不用追求一步到位。下次新写一个类的时候先问自己三个问题这个类需要什么依赖它有哪些状态这些状态会怎样变化把这三个问题的答案写成注释或者直接写进构造函数的签名里就已经是显式设计的起点了。随着这种问法变成习惯你再回头看以前那些运行全靠猜的代码会很自然地想要重构它们。显式优于隐式从来不是一句挂在嘴边的口号它是在每一次构造对象、每一次命名方法、每一次选择参数传递方式时都做出来的具体选择。每个选择单独看都很小但累积起来就是一个项目长期维护时真正的分水岭。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。