Better Code: 抽象: 可扩展性与可维护性的抉择

抽象程度

抽象程度高, 往往意味着: 灵活, 可扩展性高 但是, 这也同时意味着: 复杂, 可维护性低

而可维护性在一个项目的生命周期中又是非常重要的, 一次开发编写的代码, 可能有几十倍的阅读/变更/debug等等

所以, 需要取得平衡, 但是这个度应该如何平衡?

至下而上: 够用+保留一定可扩展

务实的做法:

  1. 实现功能, 简单/粗暴/丑陋, but it works!
  2. 明确分层/已知的概念, 然后进行抽象, 第一次重构; (分层/模块切分/类切分/方法挪动)
  3. 明确什么是当前的需求, 以及可见范围内的需求(要长远, 但是不能过远, 正常, 2-3年的量, 支持当前十倍作用的上层业务考虑)
  4. 明确变更点, 开闭原则, 变更点意味着未来的可扩展性; 抽出概念, 适当使用相关的设计模式, 进行第二次重构; 此时, 可能会多几个概念, 多一些模式.(但是不会多很多)
  5. 重新组织代码, 测试功能, 加单测等, 交付
  6. 每一次新需求或需求变更, 再来重新审视设计, 概念以及层的合并/拆分, 然后重构代码, 加新需求逻辑

特征: 代码中, 具体需求逻辑代码占比很大, 只包含少量必要的抽象/分层等

至上而下: 可能会过度设计

没有必要为不变的点抽出太多的概念/太多层, 没有必要为了未来几年都不会扩展的点增加某个设计模式;

注意: 是可能

容易过度设计的做法:

  1. 想要一步到位, 或者完美主义, 或者看得很远, 所以基于需求直接拆分出一堆层次和概念
  2. 基于概念, 通过组合/继承或者设计模式, 完成一堆父模块/基类/调用框架(此时没有具体代码)
  3. 实现一两个子类, 完成需求, 测试功能, 加单测, 交付

特征: 代码中, 抽象的代码占比很大, 大量的概念/抽象等, 只在几个具体实现类中包含实现代码

问题: 概念抽象真的有那么重要吗?

看场景

  1. 大型项目, 很重要
  2. 需求多变/变更频繁, 很重要
  3. 生命周期很长的项目, 很重要
  4. 其他, 不是那么重要

如果是一次性的任务, 那么quick and dirty搞完即可, 很少或根本没有抽象

如果是一个小型项目, 或者项目本身需求明确变更不频繁, 或者需求实现之后不怎么变半年一年一种实现. 那么, 概念抽象以及可扩展性并没有想象中那么重要, 而应该把可读性/可维护性放在前面, 保持适当的可扩展性, 某些极端场景, 甚至不考虑可扩展性

如果是中大型项目, 生命周期长, 需求复杂且多, 那么此时概念抽象就很重要了

怎么抉择?

看到这里, 并不是说, 一定要至下而上, 或者一定不能至上而下

而是需要根据很多因素决策: 看项目, 看需求, 看场景…..

  1. 项目大小
  2. 项目的生命周期
  3. 具体需求的复杂度
  4. 需求变更是否频繁? 新需求加入是否频繁? 是否有可见的需求?
  5. ……

例子: 一个需求及实现

需求: 从两个不同的服务, 同步数据到同一个表; 这两个服务的协议不一样, 获取的数据字段映射到数据库表字段也不一样, 并且不同字段有不同的处理规则

几种实现:

  1. 写两个脚本或者celery任务, 执行同步
    • 加字段/字段变更需要两边都改
  2. 写一个dataclass+两个client, client负责转换为统一格式
    • 加字段/变更字段, 改两个client即可
  3. 写一个dataclass+两个client+两个mapper, mapper 负责数据转换
    • 加字段/变更字段, 改两个mapper即可, 甚至mapper变更纯配置
  4. 抽象概念: syncer/fetcher/client/mapper/hook/helper/loader
  • 一次性的任务, 选 1(quick and dirty), 如果时间空余, 做到 2
  • 偶尔有字段变更, 选 2
  • 频繁字段变更, 选 3, 最好变成配置
  • 大型项目, N 个服务, N 种协议, N 种映射转换, 且不断有新逻辑, 初期还是最多做到 3, 逐步抽象, 最终可能演变成 4; 但是, 并不是一开始就那么多概念(至上而下), 而是不断迭代中演化出来的

所以, 以务实的方式来设计方案, 逐步随着项目迭代演化出最终的形态.

务实

从零开始学架构, 提到了三个原则: 合适/简单/演化

以务实的角度去思考

  • 合适, 但也不能太简陋/落后
  • 简单, 但是要能满足现在及未来可见范围内的需求
  • 演化, 但是底子要好, 避免无法演化只能重写