【原创】协同开发时如何定义公有化的ORM对象

818 阅读6分钟

MedusaSorcerer的博客

好像在 ChatGPT 的问世后,博客的魅力少了一些,我也因为忙碌的工作,三年没有分享了,像极了斗破的萧家小子,第一个三年过去了,也不知道后面会不会再有第二个三年需要等待。也感谢关注我的师兄师姐们。

前言

历时三年,总会在工作中遇到一些奇奇怪怪的问题,当然,如果是技术类的缺陷问题,可能咨询 GPT 会更有参考价值。

所以今天想分享一下代码架构的问题。如果你对代码架构有一定的要求时,请你耐心的看完这篇文章。

问题

很多时候,公司会把一个产品分散成若干个团队去协作完成,这是为了能让团队成员更加专注于自身负责的模块,更好的对质量要求、性能稳定、需求把控等做到极致,能完成一次性完整交付的一种方式,而且这也是敏捷开发模式的体现。

而像 Web 开发这种协作,最离不开的就是 database 的交互和定义。例:

  • router:路由层,前端UI进行数据交互的API服务
  • datain:数据写入层,外部数据写入到数据库内
  • dataout:数据输出层,数据库数据输出到外部其他设施
graph TD
database_service <--> |orm|router
database_service <--> |orm|datain
database_service <--> |orm|dataout

基于 docker 容器开发部署的方式,每一个功能模块是一个镜像。

你应该如何定义 ORM 呢?难道在每个功能模块中都定义一份 ORM 的表结构定义对象吗?

project
   └─router
       └─orm
   └─datain
       └─orm
   └─dataout
       └─orm

在实现上,确实不失为一种办法。因为这样做确实是可以达到你的目的。但是经过迭代演进的过程中,你的表结构定义需要对字段信息发生变更,是不是会存在多处变更的情况?

这时候就发现公有化的方式有多么重要了。

措施

其实最好的方式就是定义公有化的 ORM 对象,这样的方式完全符合高内聚低耦合的价值体现:

特性备注
唯一性
简单维护
所有要求定义 ORM 的表对象,都只存在 ORM 的定义的模块中,其他业务模块需要时应该从 ORM 模块中导入,而不是重新定义一个相同/相似的表结构
公有化
开放定义
定义方式被修订/修改时,其他模块都可以通过一些测试手段感知到,如单元测试、场景测试等
私有化
个性定制
除了公共开放性的接口外,业务接口应该在业务模块内定义,而不是在公有化的 ORM 模块中定义,造成耦合性的代码
graph TD
database_service <--> |交互|router
database_service <--> |交互|datain
database_service <--> |交互|dataout
router <--- |引用| ORM
datain <--- |引用| ORM
dataout <--- |引用| ORM

实施

  1. 公有化的 ORM 应该独占一个仓库,这样的方式便于维护和迭代引用
  2. ORM 的开发仓库版本号应该和主线版本号保持一致,这样更好的方便版本之间的差异信息比对
  3. 引用方式只是一种导入的方式,如 python 中的 import 语法,但是源码获取需要依赖 git clone 的命令行
  4. 公有化的 ORM 应该实现几个功能
    • 表结构定义:这是主要承载的功能
    • 表创建函数定义:对于创建方式应该在一个地方维护,每个业务容器都应该尝试性地创建一次,并且他应该具有锁机制
    • 表session获取:session的生命周期管理应该在 ORM 中定义好,而不是在每个业务容器中单独定义,这样无法更好的管理 session 对象
    • 表通用方法定义:这是和业务无关的通用方法,如 search、delete、update、create 方法

sqlalchemy 为例子:

class User(Base):  
    __table_args__=({"mysql_charset": "utf8mb4"},)
    __blename__ = 'user'
    
    name = Column(String(255), unique=True)
    
    @classmethod
    def search(session, **kwargs):
        pass

    @classmethod
    def delete(session, **kwargs):
        pass

    @classmethod
    def update(session, **kwargs):
        pass

    @classmethod
    def create(session, **kwargs):
        pass

那么定义好了一个提供标准化接口的 ORM 之后,如何在业务模块中进行导入访问呢?

from orm import User as UserBase

class User(UserBase):
    def get_users(session, offset, limit):
        """ 获取用户列表的分页数据 """
        return session.query(User).offset(offset).limit(limit).order_by(User.id).all()

而对于业务团队来说,很多的交互方式都是通过自定义继承的子类进行访问的,而公有化的模块只是作为了父类引用的关系进行表结构定义的方式。

那么这样的方式有什么好处呢?

收益

展开讲解三个特性的收益

唯一性:你的表结构定义都是在你的 ORM 仓库中定义,虽然代码在三个容器中都存在一份,但是对于研发人员来说,这是同一份代码,因为他们都是即时性的利用 git clone 方式获取的最新代码。因此你的代码变更,都是唯一的。如你在 router 中进行 dockerfile 打包的时候进行代码获取。

公有化:其一,代码引入方式是通过 git clone 的方式,其他地方可以通过单元测试感知到你的变更是否存在改动问题。其二,数据库访问最基础的无非就是 CRUD 的交互,因此提供公有化的的基础方法能让你少写很多代码,这些方法都是极具个性化输入的,因此能满足所有的交互需求。

私有化:业务中交互数据库的方式都不尽相同,如 router 更多的是关键字模糊搜索、分页检索、创建、删除等等操作,但是对于 dataout 可能只是关注数据的持续输出,因此他的过滤条件肯定和 router 层不一致,所以这样的代码应该在业务模块下进行集成重写,而不是在公有化模块中进行,当然,还有一个原因是由于业务关系,可能你的方法中还引入了业务模块中特有的方法,如你在 ORM 检索的时候需要根据字段值去访问 router 下的同名文件,但这些文件在 datain 中并不存在,因此这样的方法总会导致错误信息,因此是不合理的。最后还有一点,就是你的 ORM 方法也是需要经过单元测试验证的,如果业务交互代码放在公有化的模块中,那么他的单测代码应该放在哪儿比较合适呢?

总结

graph TD
id1[公有化 ORM] --> |git clone|id2[业务容器]
id2 --> |继承|id3[业务模块中提供的 ORM 子类]
id3 --> |访问方法|id4[子类类方法或者静态方法]
id4 --> |获取数据|id5[调用者将数据结果返回]





完!