MM架构是一种结合了模块化软件设计与Clean/Hexagonal/Onion架构精髓的应用架构,通过业务模块(Business-Modules)和基础设施模块(Infrastructure-Modules)的分离来实现高内聚、低耦合和可测试性。
MIM架构用"业务模块+基础设施模块"的简单组合拳,打破了Clean/Hexagonal/Onion架构的复杂圈层迷信,让软件设计回归模块化本质,实现低认知负载、高可维护性和高可扩展性的三重自由。
MIM的核心逻辑可以用一句话概括:把应用拆成独立的业务模块,每个模块负责一个具体的业务流程;如果某个模块的业务逻辑太复杂,就把它的基础设施代码(比如数据库操作、网络请求、文件读写)抽出来,单独放一个基础设施模块里。业务模块是老大,基础设施模块是小弟,小弟依赖老大,老大不依赖小弟。
这听起来是不是很像Dependency Inversion Principle(依赖倒置原则)?没错,MIM就是把DIP从代码层面提升到了架构层面。但跟Clean/Hexagonal/Onion那些"圈层架构"不同,MIM没有强制性的分层,没有"Domain层必须纯洁无瑕"的洁癖,也没有"所有外部依赖必须通过Adapter转换"的繁琐流程。
打个比方,传统架构像是你家装修,必须先画一张精密的平面图:客厅是Domain,餐厅是Application,厨房是Infrastructure,然后你拿个杯子都得从客厅走到厨房再回来。MIM则是模块化装修:卧室是一个模块,里面有床(业务逻辑)和床头柜(基础设施);厨房是另一个模块,里面有灶台和冰箱。你想喝水?直接在卧室的床头柜拿,不用穿越整个房子。
业务模块:你的代码黑匣子
在MIM的世界里,业务模块(Business-Module)是绝对的C位。
一个合格的业务模块长什么样?首先,它有一个清晰的公共API,就像自动售货机的按钮——你按一下,可乐出来,你不用知道可乐是怎么从仓库滚到出口的。其次,它封装自己的数据,其他模块不能绕过API直接改它的数据,这就像你不能掀开自动售货机的玻璃门直接拿可乐,必须投币。
最重要的是,业务模块负责的是一个独立的流程,而不是一个技术层。什么是流程?"处理用户下单"是一个流程,"计算运费"是一个流程,"发送邮件通知"也是一个流程。什么是技术层?"Domain层"、"Service层"、"Repository层"——这些就是典型的反模式,因为它们把相关的业务逻辑拆散了,把不相关的技术代码堆在一起。
想象一下,如果你按照技术层来组织代码,你的"Domain层"里会有用户下单的领域模型、计算运费领域模型、邮件通知领域模型——它们八竿子打不着,却被迫住在同一个文件夹里。而按照MIM的方式,"订单模块"包含下单的业务逻辑和相关的领域模型,"物流模块"包含运费计算,"通知模块"包含邮件发送。每个模块都是一个黑匣子,输入是API调用,输出是业务结果,内部实现对外不可见。
基础设施模块:业务模块的"外挂"
但是,业务模块有一个致命弱点:如果它直接调用数据库、发送HTTP请求、读写文件,这些操作是很难单元测试的。你总不能在单元测试里真的去连数据库吧?那测试跑得比乌龟还慢,而且环境一崩全崩。
这时候就需要基础设施模块(Infrastructure-Module)登场了。基础设施模块只包含基础设施代码,没有任何业务逻辑,它是业务模块的专属小弟。业务模块定义接口,比如"IOrderRepository",基础设施模块实现这个接口,比如"OrderRepositoryDb"用数据库实现,"OrderRepositoryCache"用缓存实现。
关键来了:业务模块不依赖基础设施模块,是基础设施模块依赖业务模块。这就像是老板(业务模块)说:"我需要有人帮我订机票",然后秘书(基础设施模块)说:"我来办,我用携程还是飞猪你不用管。"老板只关心"机票订好了"这个结果,不关心过程。这样业务模块就可以轻松做单元测试,只要用一个假的秘书(Mock或Fake)来模拟订机票的行为,测试速度飞快,而且不依赖外部环境。
基础设施模块不是强制性的。如果你的模块只是一个简单的CRUD,业务逻辑薄得像张纸,那就没必要拆成两个模块,直接一个模块搞定。MIM的哲学是实用主义:该拆就拆,不该拆别硬拆,不要为了架构而架构。
对比传统架构:为什么MIM更接地气
现在我们来对比一下MIM和Clean/Hexagonal/Onion架构。这三大架构的核心思想其实跟MIM一样:让业务逻辑独立于外部世界,通过依赖倒置实现可测试性。但是,它们在实现这个思想的过程中,加了太多"仪式感"。
- 清洁架构Clean Architecture画的是同心圆,Domain在最里面,外面一层是Use Cases,再外面是Interface Adapters,最外面是Frameworks and Drivers。
- 六边形Hexagonal Architecture画的是六边形,里面有Application,外面有Ports和Adapters。
- 洋葱Onion Architecture画的是洋葱,核心是Domain Model,外面一层层包着Domain Services、Application Services。
详细见《后端架构演进介绍》https://www.jdon.com/Backend-Architecture.html
这些图看着很酷,但问题是你怎么落地?网上关于"Clean Architecture的Domain层到底该放什么"、"Hexagonal的Adapter该怎么写"的争论铺天盖地。有的团队为了实现这些架构,写了一大堆接口、工厂、适配器,代码量翻了三倍,业务逻辑却没什么变化。这就是过度工程化(Over-engineering)的悲剧。
MIM的优势在于它的简单性和灵活性。没有强制的分层,没有"必须这样画"的规矩,只有两个概念:业务模块和基础设施模块。你可以根据业务的需要自由组合,就像玩乐高积木,而不是拼一个固定的模型。
举个例子,在Hexagonal Architecture里,有一个概念叫"Driven Adapters"和"Driving Adapters",分别对应被应用调用的外部系统和调用应用的外部系统。这区分看起来很严谨,但实际操作中,很多组件是双向的,比如WebSocket既是输入也是输出,你把它放哪边都别扭。MIM就不搞这种区分,所有基础设施代码都放在基础设施模块里,简单直接。
实战案例:从混沌泥潭到清晰模块
光说不练假把式,咱们来看一个实际的例子。
假设你有一个温室控制系统(H&V Server),要控制温度和通风,还要处理设备信号、发送报警、升级固件。最初的代码是一个"大泥球"(Big Ball of Mud),所有代码混在一起,Model文件夹里有各种实体,Service文件夹里有各种服务,Interfaces文件夹里有各种接口,但谁也搞不清楚哪个服务用哪个接口,改一个功能要翻遍整个项目。
按照MIM的方式来重构,第一步是识别高层次的职责。这个系统有哪些独立的流程?控制温湿度是一个流程,处理电池报警是一个流程,分发固件升级是一个流程。每个流程就是一个候选的业务模块。
先来看电池报警模块(Battery Alarms Module)。它的职责是:接收IoT设备的信号,判断是否需要报警,如果需要就调用外部报警系统,而且15分钟内只报警一次。这个业务逻辑有一定的复杂度,所以需要配套的基础设施模块,负责接收IoT信号和调用外部HTTP API。业务模块定义接口"IAlarms",基础设施模块实现它。这样业务模块就可以独立测试,用一个假的报警器来验证"15分钟内只报警一次"的逻辑,不用真的去连IoT设备和外部系统。
再来看固件分发模块(Firmware Dispatcher Module)。它的职责是控制固件升级的百分比发布(比如先给10%的设备升级,观察没问题再全量推送),以及支持固件回滚。这也有复杂的业务逻辑,所以同样需要基础设施模块来处理HTTP端点和设备通信协议。
最复杂的是温湿度控制模块(H&V Controller)。最初的设想是它负责控制温湿度,但仔细分析后发现它还承担了模拟计算最优参数、调度任务、整合外部通风计划等职责。这违反了高内聚原则,一个模块干太多事了。所以进一步拆分:把模拟计算拆成H&V Simulation模块,把调度拆成H&V Scheduler模块,原模块只保留核心的控制逻辑。
同时发现,固件分发模块和电池报警模块都在处理设备通信,有重复的代码。这时候可以提取共享的模块:Device API模块负责设备通信协议,Signals Filter模块负责处理高吞吐量的IoT信号过滤。这些共享模块不是某个业务模块的专属基础设施,而是独立的服务模块。
最终的设计就像一张清晰的地图:每个模块职责明确,依赖关系明确,新来的程序员看模块名就知道系统是做什么的。这就是所谓的"尖叫架构"(Screaming Architecture)——代码结构本身就在尖叫着告诉你系统的业务功能,而不是藏着掖着让你猜。
测试策略:别为了测试而测试
MIM架构把可测试性作为一等公民,但这不意味着你要写一堆没用的测试。MIM推荐的测试策略是自适应的,分为三个层次,而且每个层次都是可选的。
第一层是集成测试(Integration Tests),测试所有模块组合在一起的行为,包括真实的数据库等依赖。如果你的系统有很多基础设施模块,集成测试是有价值的,可以验证模块之间的集成有没有问题。
第二层是社交型单元测试(Sociable Unit Tests),这是MIM的核心测试方式。测试的对象是整个业务模块,而不是单独的类或方法。你通过模块的公共API输入数据,验证输出和副作用,不关心内部是怎么实现的。这种测试就像黑盒测试,但比集成测试快,因为你可以用Fake来替代依赖的其他模块。
第三层是重叠型单元测试(Overlapping Unit Tests),针对特别复杂的算法或难以通过模块API覆盖的边界情况,单独测试某些类或方法。这是可选的,只有在社交型单元测试搞不定时才需要。
记住,测试是有成本的。写测试是为了让你敢重构代码,敢发布上线,不是为了追求100%的覆盖率数字。如果测试写得不好,重构时测试全崩,那测试就从资产变成了负债。
常见误区:MIM不是银弹
最后澄清几个常见的误解。有人问:MIM是不是只适用于Domain-Driven Design(DDD)?不是,MIM比DDD更底层,你可以在用DDD的项目里用MIM,也可以在不用的项目里用MIM。
有人问:MIM是不是全新的发明?也不是,它是模块化软件设计的现代化版本,结合了Clean/Hexagonal/Onion的依赖倒置思想,以及"Imperative Shell, Functional Core"等模式。你可以在很多成功的项目里看到类似的设计,只是没有一个统一的名字。
有人问:MIM和微服务有什么关系?MIM可以用在单体应用里,也可以用在微服务里。一个微服务内部可以用MIM来组织代码,避免变成"大泥球单体"。MIM的模块边界设计得好,未来把模块拆分成独立的服务也很容易。
还有人问:MIM是不是不要分层了?准确地说,MIM没有强制性的全局分层,但模块内部你可以自由组织代码。如果一个业务模块内部适合用分层,那就用;适合用函数式编程,那就用。MIM关注的是模块之间的边界,而不是模块内部的实现细节。
那些你可能会问的“灵魂拷问”
问:这玩意儿听起来跟那个什么“六边形架构”、“整洁架构”好像啊,它们到底有啥区别?
答:好问题!它们确实有点像,都用了“依赖倒置”这个核心思想,让业务逻辑不依赖外部。但区别在于,“六边形”们就像是你买了个精装修的房子,里面每个房间是干啥的(比如,这个是驱动适配器,那个是被动适配器)都给你规定死了。你住进去,就得按它的格局来,想改个非承重墙都费劲。
而MIM呢?它只给你一堆高质量的建筑材料(业务模块和基础设施模块的概念)和一套通用的建筑原则(高内聚低耦合)。你想在屋里哪儿摆沙发,哪儿放床,完全你自己说了算。所以,MIM更灵活,适应性更强。你不需要为了用架构而用架构,生搬硬套一些你根本不需要的“层”。
问:那MIM和现在很火的“垂直切片架构”比呢?
答:这两兄弟长得更像。它们都主张按功能(Feature)来组织代码,而不是按技术层。但垂直切片通常是把一个功能的所有代码(包括HTTP请求处理和数据库访问)都放在一个“切片”里。
而MIM更进一步,它建议你在切片内部,再把“业务逻辑”和“基础设施代码”这对天生爱吵架的兄弟分开,安排在不同的模块里,让它们各司其职。这样一来,MIM的测试优势就更明显了,因为它把最核心的业务逻辑隔离出来,让你可以轻松地进行“社交型单元测试”。
而在纯垂直切片里,人们往往更依赖集成测试,因为切片内部的基础设施和业务逻辑还是混在一起的。
问:我们团队人很多,MIM对协同开发友好吗?
答:简直是为团队协作量身定做的!还记得那个“模块即积木”的思想吗?当你的模块划分得足够清晰之后,每个模块就变成了一个独立的工作单元。
比如,可以让小明带着两个人,全权负责整个“电池警报模块”,从业务逻辑到基础设施。他们三个人可以在自己的模块领地内为所欲为,只要不改变对外的接口,就完全不会影响到隔壁负责“固件分发”的小红团队。
这就是“每个模块都有自己的主人”,极大地减少了代码冲突和沟通成本。你们甚至可以把这个模块的代码放在一个单独的代码仓库里,实现物理隔离,那协作起来就更爽了。
问:听你说了半天,感觉MIM就是一个更聪明的“分层架构”?
答:完全不是!传统的三层架构是水平的,像切蛋糕一样,把整个应用横着切成三层。而MIM是垂直的,像掰柚子一样,把整个应用竖着掰成一瓣一瓣的“模块”。
在三层架构里,一个“用户登录”的功能,它的代码会散落在表现层、业务逻辑层和数据访问层。
而在MIM里,所有和“用户登录”相关的代码,都集中在“用户登录模块”这一瓣柚子里。
一个水平切,一个垂直分,这是本质的区别。
MIM的目标,就是为了避免三层架构那种“改一个功能,动三个层”的尴尬局面。
问:那MIM是不是只适合用在那种超大型的企业级项目里?我写个小工具能用吗?
答:能用,但没必要。就像你不会用修建摩天大楼的工具去搭一个狗窝一样。MIM的复杂度在于模块的划分和管理,如果你只是写一个几百行代码的小脚本,硬要拆成好几个模块,那就是典型的“过度设计”,自己给自己找麻烦。
MIM最适合的是那些有一定复杂度、需要长期维护、并且可能由多人协作的中大型项目。当你的代码开始变得难以理解和维护时,就是考虑引入MIM的好时机了。记住,架构是为了解决问题而生的,不是为了制造问题而存在的。