写点什么

解耦远不止依赖注入

作者:canonical
  • 2023-05-15
    北京
  • 本文字数:2560 字

    阅读完需:约 8 分钟

什么是耦合?如何解耦合?在面向对象技术盛行的今天,所谓的相互作用被表达为对象之间的相互关联,耦合的外在表现是持有相关对象的指针,因此解耦合问题似乎可以具体化如何最小化对象装配所需要的信息量,最后形成的解决方案就成为了依赖注入这一技术理念。具体介绍可以参见 invalid s 的回答


何为解耦


<b>依赖倒置</b>原则(DependenceInversionPrinciple,DIP)是指设计代码结构时,高层模块不应该<b>依赖</b>低层模块,二者都应该<b>依赖</b>其抽象。


但是依赖注入是解耦的关键只是一个过去流行的理解。一方面它确实提供了解耦的一种实用手段:不依赖对象整体,只依赖于最小化的抽象接口。不同的对象可以实现同样的接口。通过描述式的装配容器延迟装配选择。 但是,另一方面,它并不是在软件中实现解耦的全部手段,甚至我们可以说它不应该成为解耦的主要手段。

一. 依赖注入什么?同态像

所谓依赖于抽象而不依赖于具体的细节,本质上是说我们不依赖于对象整体,而只是依赖于它的一个同态像


数学上的同态映射,指的是对象空间中的一种变换,它将原空间 O 中的对象 a 和 b 映射到像空间中的 P(a)和 P(b),同时这种映射保持原空间中的某些运算关系 f,即原空间中定义的函数可以自然转换为像空间中的函数。


 P(f(a,b)) = f(P(a), P(b))
复制代码


对象实现接口,对象是接口的一个实例,obj is an instance of interfaceA。设计中我们需要保持对象和基类,以及对象和接口之间在概念层面上的“is a”关系,目的是为了保证针对接口编写的代码可以作用于派生对象上,即


f(interfaceA,interfaceB) == f(objA, objB)
复制代码


对象上可以有很多函数,一般只有少部分函数是执行业务功能时所必须的功能接口函数,其他大量的函数则是为了支持动态配置、维护生命周期等提供的辅助函数。如果我们坚持对象之间只依赖于功能接口,相当于是将问题同态映射到一个较小的语义空间中,从而实现问题的简化。


描述式的依赖注入容器知道所有对象之间的依赖关系,因此可以对它们的构造顺序进行全局排序,并且可以通过配置文件实现延迟装配,通过缓存实现延迟加载语义等,相当于是解耦了不必要的对象创建顺序之间的依赖(只有具有全局知识才能实现全局优化)。当然,如果我们系统的结构比较简单,本身可以被提取为明确的几个生命周期阶段,则依赖注入容器在此方面的作用会被弱化。


接口可以看作是对象的一种局部表象(Representation)。为了使得这种表象的作用最大化,一个必然的要求是表象空间中的运算具有某种完备性,即通过表象空间中定义的函数操作即可完成主要的业务功能。接口上定义的函数应该是在业务层面上能够自圆其说的,否则就会出现所谓的抽象泄露的问题。

二. 从同态映射到表象变换

如果追溯起来,耦合和解耦这样的概念其实是来自于数学和物理领域,而在数学和物理中我们实现解耦的手段要丰富、强大的多。


回想一下数学物理中最重要的 Fourier 变换理论,多个不同频率的信号叠加在一起,在时域上看来它们是完全混杂在一起的,在每一个时间点上实际上都存在着多个信号的影响。但是通过 Fourier 变换,我们在频域上可以得到完全分离的多个信号! 所以,为了实现解耦,最本质、最强大的手段应该是表象变换,接口只是表象变换的最简单的一种形式而已


解耦好坏的一个评判标准是所谓的高内聚低耦合。高内聚的极限是不可分(原子性),而低耦合的极限是不相关。线性系统情况下,解耦总是存在无限多的最优解:只需找到任意一组单位正交向量即可(正交坐标系旋转任意角度仍然是合法的正交坐标系),然后整个空间中的所有量都可以通过基向量上作用量的线性组合来得到。


表象变换,简单的说起来,可以看作是坐标系变换。事物的本质并没有变,但是在不同的坐标系中去考察的时候,我们会误以为它的复杂性,各个部分之间的相互关系在发生激烈的变化。如果反过来思考,当我们设计一个系统的时候,可以选择维度正交的线性系统作为我们的目标模型,尽量保持系统各个层面的线性性,从而实现系统结构的简化。参见


从张量积看低代码平台的设计


如果把 DSL(领域特定语言, Domain Specific Language)看作是一种全局表象,则表象变换可以表达为 F(DSL),即针对 DSL 的一个解释器或者代码生成器。但是一般来说 DSL 是针对已知领域需求的总结,它的能力总是有限的,为此我们必须补充额外的信息 Delta 才能生产目标系统。由此得到


App = F(DSL) + Delta
复制代码


也就是说我们需要把外部提供的差量信息和表象变换得到信息合并在一起。但是合并就意味着混杂,混杂一般会导致熵增(系统混乱度上升)。熵增本质上是因为信息的损失,如果能够控制不产生熵增,则必须保持信息的完整性,确保信息不损失,而在热力学中这意味系统的演化是可逆的


可逆运算意味着变化量和本体的解耦。变化量可以脱离本体独立存在,它具有自身的意义。


Delta = App - F(DSL)
复制代码


在可逆计算理论中,Docker 镜像就可以被看作是一种差量结构,应用镜像可以脱离底层的操作系统镜像而被单独的存储、传递、管理等。


引入可逆计算的思想之后,会出现一些有趣的现象。一般概念中,解耦是减少相互作用,那首先应该是减少参与相互作用的对象个数。但是如果存在可逆计算,则可以通过增加新的对象 Delta 来实现解耦。例如,对象 A 和对象 B 对同一个的功能设计存在冲突,它们可以按照最适合自己内部结构的情况进行设计,不用考虑为回避冲突存在而导致的耦合问题。当它们在一起使用时,可以补充 Delta 差量来解决它们之间的概念冲突。


 App = A + B + (-C - D + E) = A + B + Delta
复制代码


Delta 对于 A 和 B 而言是完全外置的实体。


面向对象和组件理论,本质上是试图从人类物质世界的生产活动中汲取经验,但是随着软件生产活动的深化,我们有必要重新认识软件的抽象本质。软件是在抽象的逻辑世界中存在的一种信息产品,信息并不是物质。抽象世界的构造和生产规律与物质世界是有着本质不同的。物质产品的生产总是有成本的,而复制软件的边际成本却可以是 0。将桌子从房间中移走在物质世界中必须要经过门或窗,但在抽象的信息空间中却只需要将桌子的坐标从 x 改为-x 而已。抽象元素之间的运算关系并不受众多物理约束的限制,因此信息空间中最有效的生产方式不是组装,而是掌握和制定运算规则。


关于可逆计算完整的理论阐述,可以参见我的文章


可逆计算:下一代软件构造理论


具体的一些实现方式可以参考


可逆计算的技术实现


低代码平台需要什么样的ORM引擎?(1)


低代码平台需要什么样的ORM引擎?(2)

用户头像

canonical

关注

还未添加个人签名 2022-08-30 加入

还未添加个人简介

评论

发布
暂无评论
解耦远不止依赖注入_架构设计_canonical_InfoQ写作社区