【AICon】AI 基础设施、LLM运维、大模型训练与推理,一场会议,全方位涵盖! >>> 了解详情
写点什么

避免在.NET 代码中出现不恰当依赖

  • 2012-08-20
  • 本文字数:7662 字

    阅读完需:约 25 分钟

在如何至始至终保持代码的可维护性方面我给.NET 开发者团队的最好建议是:将应用程序中的每个命名空间都当作组件看待,同时确保组件之间不存在依赖环。 通过遵守这条简单的原则,大型应用系统的结构就不会陷入大块意大利面式代码的混沌之中——而这种意大利面式代码在专业企业应用开发中往往被视为正常而非异常的现象。

命名空间即组件

从十多年前.NET 技术出现以来,Visual Studio 开发工具一直隐式地将 VS 项目作为组件(也即程序集)。这是不恰当的,因为组件应该是 _ 结构 _ 代码的逻辑部件,而程序集应该是 _ 包 _ 代码的物理部件。这导致了另一个被视为正常而非异常的现象:有些企业应用程序竟由几百个 VS 项目组成。

我为什么鼓励使用命名空间这个轻量级概念来定义组件边界呢?其好处如下:

  • 更轻量的组织:多用命名空间而少用程序集意味着所需的 VS 解决方案个数和 VS 项目个数变少了。
  • 减少了编译时间:每个 VS 项目都会在编译时产生额外的时间开销。具体点说,项目很多的话会导致编译需要花几分钟时间,但如果大幅减少 VS 项目的数量,则编译仅需花几秒钟时间。
  • 更轻量的部署:部署几十个程序集要比部署上千个简单多了。
  • 更少的应用程序启动时间:CLR 加载每个程序集时都需要付出一小些额外的性能开销。加载几十或上百个程序集的话,总共的开销就相当明显了,达到了以秒记的级别。
  • 方便了组件的层次组织:命名空间能够表达出层次结构,程序集则不能。
  • 方便了组件的细颗粒度化:存在 1000 个命名空间不是什么问题,存在 1000 个程序集就是个问题。选择构建一些非常细粒度的组件不应该因为需要专门创建相对应的 VS 项目而令人扫兴。

依赖环危害不小

组件间的依赖环会导致出现人们常说的意大利面式代码(spaghetti code)或者纠缠式代码(tangled code)。假如组件 A 依赖于 B,B 依赖于 C,而 C 依赖于 A,则 A 不能够离开 B 和 C 单独进行开发和测试。A、B 和 C 形成了一个不可见环,一种超级组件。这个超级组件的开销要比 A、B 和 C 三者的开销之和还大,这就是所谓的规模不经济现象(diseconomy of scale phenomenon)(请参见详尽文档 Software Estimation: Demystifying the Black Art by Steve McConnell )。通常,这会导致开发最小单元代码的开销呈指数级增长。这意味着,如果不能将 1000 行代码划分成相互独立的两份 500 行的代码的话,开发和维护 1000 行代码的开销要比 500 行多出三或四倍。如果是碰到意大利面式或者纠缠式代码的话,那就可能无法维护了。为了使组织架构更加合理,人们应该确保组件之间不存在依赖环,同时确保每个组件的大小是合适的(500 至 1000 行之间)。

对战设计侵蚀(design erosion)

五月份发布的 NDepend 版本 4 引入了应对应用程序环的新特性,在这里我想讨论下其所具有实践意义。

现在我们能够按照 LINQ 查询要求来实现编码规范(我们称之为 CQLinq ),我们能够利用 LINQ 的巨大灵活性构建出特定规范。其中一个我参与构建的规范是能够报告命名空间依赖环的代码规范。例如,如果我们来分析 _.NET Framework v4.5_,观察程序集 _System.Core.dll_ 内部,就会发现其存在两个命名空间依赖环,这两个环都由 7 个命名空间组成。代码规范特性可以索引环中的某个命名空间(随机选取)并展现这个环。用鼠标左键点击下图 cycle 字段可以查看依赖环所包括的命名空间:

(点击图片可以放大)

通过鼠标右键点击命名空间列表或者依赖环本身,就会出现将他们导出为依赖图(dependency graph)或者依赖矩阵(dependency matrix)的菜单。下面的截图显示了7 个相互纠缠的命名空间。但这不是循环依赖的典型图示,典型的情况是:假定两个命名空间A 和B,通过B 可以访问到A,并且反之亦然。显然,这样纠缠起来的代码是不容易维护的。

(点击图片可以放大)

让我们来看看CQLinq 的代码规范体 避免命名空间依赖环。我们可以看到开头有很多描述如何使用的注释。这是通过注释和C#代码和读者交流的好机会,感谢即将发布的 Roslyn compiler as services ,我相信所提倡的简短 C#代码摘录(excerpt)而不是 DLL 或者 VS 项目,将会越来越受欢迎。

复制代码
<i><span color="#a5a5a5">// <Name> 避免命名空间依赖环 </Name></span></i>
<span color="#a5a5a5"><span color="#000000">warnif count</span> > </span><span color="#4f81bd">0</span>
<i><span color="#a5a5a5">// 这个查询列出了应用程序的所有命名空间依赖环。</span></i>
<i><span color="#a5a5a5">// 每一行显示一个不同的环,并以缠在环中的命名空间作为前缀。</span></i>
<i><span color="#a5a5a5">//</span></i>
<i><span color="#a5a5a5">// 想要在依赖图或依赖矩阵中查看某个环,右键点击 </span></i>
<i><span color="#a5a5a5">// 该环然后将相应的命名空间导出为依赖图或依赖矩阵即可!</span></i>
<i><span color="#a5a5a5">//</span></i>
<i><span color="#a5a5a5">// 在矩阵中,依赖环以红色方块或黑色单元格表示。</span></i>
<i><span color="#a5a5a5">// 为了能够方便地浏览依赖环,依赖矩阵需有该选项:</span></i>
<i><span color="#a5a5a5">// --> 显示直接和间接依赖 </span></i>
<i><span color="#a5a5a5">//</span></i>
<i><span color="#a5a5a5">// 请阅读我们关于分解代码的白皮书,</span></i>
<i><span color="#a5a5a5">// 以更深入地了解命名空间依赖环,以及弄明白为什么 </span></i>
<i><span color="#a5a5a5">// 避免出现依赖环是组织代码结构的简单而有效的解决方案。</span></i>
<i><span color="#a5a5a5">// http://www.ndepend.com/WhiteBooks.aspx</span></i>
<i><span color="#a5a5a5">// 优化:限定程序集范围 </span></i>
<i><span color="#a5a5a5">// 如果命名空间是相互依赖的 </span></i>
<i><span color="#a5a5a5">// - 则它们必定在同一个程序集中被声明 </span></i>
<i><span color="#a5a5a5">// - 父程序集必定 ContainsNamespaceDependencyCycle</span></i>
<span color="#000000"><b>from</b> assembly <b>in</b> Application.Assemblies</span>
<span color="#000000">                 .Where(a => a.ContainsNamespaceDependencyCycle != <b>null</b> &&</span>
<span color="#000000">                           a.ContainsNamespaceDependencyCycle.Value)</span>
<i><span color="#a5a5a5">// 优化:限定命名空间范围 </span></i>
<i><span color="#a5a5a5">// 依赖环中命名空间的 Level 值必须为 null。</span></i>
<span color="#000000">let namespacesSuspect = assembly.ChildNamespaces.Where(n => n.Level == <b>null</b>)</span>
<i><span color="#a5a5a5">// hashset 用来避免再次遍历环中已经被捕获的命名空间。</span></i>
<span color="#000000">let hashset = <b>new</b> HashSet<INamespace>()</span>
<span color="#000000"><b>from</b> suspect <b>in</b> namespacesSuspect</span>
<i><span color="#a5a5a5">  // 若注释掉这一行,则将查询环中的所有命名空间。</span></i>
<span color="#000000"><b>  where</b> !hashset.Contains(suspect)</span>
<i><span color="#a5a5a5"><br></br>  // 定义 2 个代码矩阵 </span></i>
<i><span color="#a5a5a5">  // - 非直接使用嫌疑命名空间的命名空间的深度。</span></i>
<i><span color="#a5a5a5">  // - 被嫌疑命名空间非直接使用的命名空间的深度。</span></i>
<i><span color="#a5a5a5">  // 注意:直接使用的深度等于 1。</span></i>
<span color="#000000">  let namespacesUserDepth = namespacesSuspect.DepthOfIsUsing(suspect)</span>
<span color="#000000">  let namespacesUsedDepth = namespacesSuspect.DepthOfIsUsedBy(suspect)</span>
<i><span color="#a5a5a5">  // 选择使用 namespaceSuspect 或者被 namespaceSuspect 使用的命名空间 </span></i>
<span color="#000000">  let usersAndUsed = <b>from</b> n <b>in</b> namespacesSuspect <b>where</b></span>
<span color="#000000">                       namespacesUserDepth[n] > <span color="#4f81bd">0</span> &&</span>
<span color="#000000">                       namespacesUsedDepth[n] ></span><span color="#4f81bd"> 0</span>
<span color="#000000"><b>                     select</b> n</span>
<span color="#000000"><b>  where</b> usersAndUsed.Count() ></span><span color="#4f81bd"> 0</span>
<i><span color="#a5a5a5">  // 这里我们找到了使用嫌疑命名空间或者被嫌疑命名空间使用的命名空间。</span></i>
<i><span color="#a5a5a5">  // 找到了包含嫌疑命名空间的环!</span></i>
<span color="#000000">  let cycle = usersAndUsed.Append(suspect)</span>
<i><span color="#a5a5a5">  // 将环中的命名空间填充到 hashset。</span></i>
<i><span color="#a5a5a5">  // 需要使用.ToArray() 来推进迭代过程。</span></i>
<span color="#000000">  let unused1 = (<b>from</b> n <b>in</b> cycle let unused2 = hashset.Add(n) <b>select</b> n).ToArray()</span>
<span color="#000000"><b>select</b> <b>new</b> { suspect, cycle }</span>

代码规范体包括若干区域:

  • 首先,利用属性 IAssembly.ContainsNamespaceDependencyCycle 以及属性 IUser.Level ,我们可以尽可能地消除掉多余的程序集和命名空间。因此,对于每个包含命名空间依赖环的程序集 _,_ 我们只保留了被称为 _ 嫌疑命名空间(suspect namespaces)_ 的集合。
  • 定义的范围变量(range variable)_hashset_ 被用来避免由 N 个命名空间构成的环被显示 N 次。注释掉这行代码 _where !hashset.Contains(suspect)_ 则会将依赖环显示 N 次。
  • 该查询的核心是对两个扩展方法 DepthOfIsUsing() DepthOfIsUsedBy() 的调用。这两个方法非常强大,因为他们各自创建了 ICodeMetric<INamespace,ushort> 对象。通常,如果 A 依赖于 B,B 依赖于 C,则 _DepthOfIsUsing©[A]_ 的值等于 2,DepthdOfIsUsedBy(A)[C]的值也等于 2。** 基本上,如果存在一个或多个嫌疑命名空间 B 使得 _DepthOfIsUsing(A)[B] 和 _DepthOfIsUsedBy(A)[B] 的值同时非 null 且为正数,则包含嫌疑命名空间 A 的依赖环就会被检测到。**
  • 接着我们只需构建命名空间 B 的集合,然后将它附加上命名空间 A,从而使整个环包含 A。

裁剪依赖环

虽然我们拥有了检测和可视化命名空间依赖环的强大方法,但当遇到要定义到底哪个依赖必须被裁剪掉以得到层级的代码结构时,我们又一次懵了。让我们来看一看上面的截图,我们可以看到依赖环大多都是由相互依赖的成对命名空间组成的(由图中的 _ 双向箭头 _ 表示)。想要得出层级的代码结构,首先必须解决的问题是确保不存在相互依赖的组件对。

于是我们研发出了 CQLinq 的被称为避免命名空间相互依赖的代码规范。这个代码规范不仅能够陈列出相互依赖对,同时它还能指示双向依赖的哪一方应被裁剪掉 。这个指示是由所使用的类型个数推断出来的。假如A 使用了B 的20 个类型,而B 使用了A 的五个类型,很可能的结论就是B 不应该引用A。B 正在使用A 的五个类型,很可能就是由于开发者不清除代码结构而造成的意外情况。这就是代码结构侵蚀的根源。

凭我们的经验,当A 和B 相互依赖时,我们通常会自然地知道哪一方应该被裁剪掉。这是因为,如我们所想,偶然造成的依赖在个数上通常是较低的。但是如果一直不加以修复,而让这种偶然错误积累,则最终会导致出现我们在大多数企业应用中看到的大面积意大利面式代码。

给个具体的例子,下图是将我们的代码规范应用于程序集_System.Core.dll_ 的结果。我们看到这个程序集包含了16 对相互依赖的命名空间。同时,下图还验证了前面分析的结果:大多数依赖对中双方间的引用类型个数是很不对称的:

(点击图片可以放大)

下面展示了CQLinq 代码规范的主体,其和上面论述的代码规范有相似之处。如果你仔细看了前面解释的代码规范,并且清楚C#语法,则看懂这条规范的相关代码是件很容易的事情。

复制代码
<i><span color="#a5a5a5">// <Name> 避免命名空间相互依赖 </Name></span></i>
<span color="#a5a5a5"><span color="#000000">warnif count</span> > </span><span color="#4f81bd">0</span>
<i><span color="#a5a5a5">// 这条规则列出所有相互依赖的命名空间对。</span></i>
<i><span color="#a5a5a5">// 命名空间对格式{ first, second }表明第一个命名空间不应该使用第二个命名空间。</span></i>
<i><span color="#a5a5a5">// 格式中的 first/second 顺序是由被彼此使用的类型的个数推到出来的。</span></i>
<i><span color="#a5a5a5">// 如果第一个命名空间使用第二个命名空间的类型的个数比相反的少,</span></i>
<i><span color="#a5a5a5">// 则表明第一个命名空间相对于第二个来说在组织结构中处于更低层级。</span></i>
<i><span color="#a5a5a5">//</span></i>
<i><span color="#a5a5a5">// 找出相互依赖的两个命名空间的耦合点:</span></i>
<i><span color="#a5a5a5">// 1) 将第一个命名空间导出到依赖矩阵的垂直方向头部。</span></i>
<i><span color="#a5a5a5">// 2) 将第二个命名空间导出到依赖矩阵的水平方向头部。</span></i>
<i><span color="#a5a5a5">// 3) 双击黑色单元格。</span></i>
<i><span color="#a5a5a5">// 4) 在矩阵命令工具条中,点击按钮:Remove empty Row(s) en Column(s)。</span></i>
<i><span color="#a5a5a5">// 到这里,依赖矩阵就显示出了导致耦合的类型。</span></i>
<i><span color="#a5a5a5">//</span></i>
<i><span color="#a5a5a5">// 遵循这条规则能有效地避免出现命名空间依赖环。</span></i>
<i><span color="#a5a5a5">// 可以在我们的关于分解代码的白皮书中找到这方面的更多内容。</span></i>
<i><span color="#a5a5a5">// http://www.ndepend.com/WhiteBooks.aspx</span></i>
<i><span color="#a5a5a5">// 优化:限定程序集的范围 </span></i>
<i><span color="#a5a5a5">// 如果命名空间是相互依赖的 </span></i>
<i><span color="#a5a5a5">// - 则它们必定在同一个程序集中被声明 </span></i>
<i><span color="#a5a5a5">// - 父程序集必定 ContainsNamespaceDependencyCycle</span></i>
<span color="#000000"><b>from</b> assembly <b>in</b> Application.Assemblies.Where(a => a.ContainsNamespaceDependencyCycle != <b>null</b> && a.ContainsNamespaceDependencyCycle.Value)</span>
<i><span color="#a5a5a5">// hashset 用来避免重复报告 A <-> B and B <-> A</span></i>
<span color="#000000">let hashset = <b>new</b> HashSet<INamespace>()</span>
<i><span color="#a5a5a5">// 优化:限定命名空间集合 </span></i>
<i><span color="#a5a5a5">// 如果一个命名空间没有 Level 值,则它必定在依赖环中,</span></i>
<i><span color="#a5a5a5">// 或者直接或间接地使用了某个依赖环。</span></i>
<span color="#000000">let namespacesSuspect = assembly.ChildNamespaces.Where(n => n.Level == <b>null</b>)</span>
<span color="#000000"><b>from</b> nA <b>in</b> namespacesSuspect</span>
<i><span color="#a5a5a5">// 使用 nA 选择相互依赖的命名空间 </span></i>
<span color="#000000">let unused = hashset.Add(nA) </span><span color="#a5a5a5"><i>// Populate hashset</i></span>
<span color="#000000">let namespacesMutuallyDependentWith_nA = nA.NamespacesUsed.Using(nA)</span>
<span color="#000000">      .Except(hashset) </span><span color="#a5a5a5"><i>// <-- 避免重复报告 A <-> B and B <-> A </i></span>
<span color="#000000"><b>where</b> namespacesMutuallyDependentWith_nA.Count() ></span> <span color="#4f81bd">0</span>
<span color="#000000"><b>from</b> nB <b>in</b> namespacesMutuallyDependentWith_nA</span>
<i><span color="#a5a5a5">// nA 和 nB 是相互依赖的。</span></i>
<i><span color="#a5a5a5">// 首先选择不应该使用另一个的那个。</span></i>
<i><span color="#a5a5a5">// 第一个命名空间是由它使用的第二个命名空间的类型的个数更少这个事实推导出来的。</span></i>
<span color="#000000">let typesOfBUsedByA = nB.ChildTypes.UsedBy(nA)</span>
<span color="#000000">let typesOfAUsedByB = nA.ChildTypes.UsedBy(nB)</span>
<span color="#000000">let first = (typesOfBUsedByA.Count() > typesOfAUsedByB.Count()) ? nB : nA</span>
<span color="#000000">let second = (first == nA) ? nB : nA</span>
<span color="#000000">let typesOfFirstUsedBySecond = (first == nA) ? typesOfAUsedByB : typesOfBUsedByA</span>
<span color="#000000">let typesOfSecondUsedByFirst = (first == nA) ? typesOfBUsedByA : typesOfAUsedByB</span>
<span color="#000000"><b>select</b> <b>new</b> { first, shouldntUse = second, typesOfFirstUsedBySecond, typesOfSecondUsedByFirst }</span>

当你解除了所有相互依赖的命名空间对之后,第一条代码规范可能仍然会报告存在依赖环。这是因为你可能会遇到由至少三个命名空间组成的依赖环,即 _A 依赖于 B,B 依赖于 C,C 依赖于 A_ 。这看起来很令人抓狂,但在实践中,这样的环通常是容易解除的。事实上,当 3 个或者更多的组件形成了这样的环形关系时,确定哪个处于最低一级是件微不足道的事情,你很容易就可以确定应该从环中的哪个地方裁剪。

结论

  • 很让人兴奋,现在我们能使用这两条强大的代码规范来检测命名空间依赖环以及指示怎样解除依赖环。
  • 另外,令我特别喜悦的是,我们通过 _ 两个单一的 __ 文本式 C#代码摘录 _ 添加了这些强大特性,有利于阅读、编写、分享和推敲。NDepend 做了将它们编译和 _ 即时 _ 执行的工作,并以 _ 可浏览和交互 _ 的方式发布。从技术上讲, 现在我们可以在几分钟之内添加完成用户要求的全新特性(我们已经推出了 200 个 CQLinq 代码规范)。同时,更为优越的是,用户甚至可以自己开发出新特性!

关于作者

Patrick Smacchia是法国一位 Visual C#方向的微软最有价值专家(MVP),他在软件开发行业打拼了 20 多年。从数学和计算科学专业毕业之后,他从事过多个软件行业领域的工作,包括在 Société Générale 的证券交易系统,在 Amadeus 的航空票务系统,在 Alcatel 的卫星基站。同时他还创作出版了《.NET 2 和 C# 2 实战》一书,这是一本从实际经验出发介绍和探讨.NET 平台的书籍。他从 2004 年 4 月份开始研发 NDepend 工具,以帮助.NET 开发者检测和修复他们的代码中存在的相关问题。他目前是 NDepend 的首席开发人员,百忙之中他还会安排时间享受软件技术的多个领域给他带来的乐趣。

查看英文原文: Cut off wrong dependencies in your .NET code


感谢侯伯薇对本文的审校。

给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ )或者腾讯微博( @InfoQ )关注我们,并与我们的编辑和其他读者朋友交流。

2012-08-20 04:003992

评论

发布
暂无评论
发现更多内容

深度操作系统20.7正式发布!

深度操作系统

国产操作系统 deepin 深度操作系统 深度 deepin20.7

助你成为专业终端人,阿里巴巴第三届终端练习生计划开启报名!

阿里技术

前端 移动开发

低代码开发平台的功能有哪些?低代码“功能清单”一览

优秀

低代码 企业级低代码平台

Redis 主从复制演进历程与百度智能云的实践

Baidu AICLOUD

数据库 redis 底层原理

《MySQL自传》

MySQL 数据库 玖章算术 叶正盛 斗佛

13th 发布在即,一文带你回顾Intel 12th Core

鼎道智联

英特尔 13th处理器 酷睿处理器 12th处理器

我们总结了 3 大使用建议,并首次公开 Nacos3.0 规划图 | Nacos 开源 4 周年

阿里巴巴中间件

阿里云 开源 微服务 云原生 nacos

如何梳理企业流程管理?

优秀

业务流程管理 主业务流程梳理

JavaScript 装饰器介绍

掘金安东尼

前端 9月月更

如何守护数据安全? 这里有一份RDS灾备方案为你支招

京东科技开发者

数据库 安全 灾备 主机安全 RDS

个推TechDay直播回顾 | 分享基于Flink的实时数仓搭建秘诀 附课件下载

个推

数据湖 实时数仓 flink window 数仓建设 大数据仓库

wallys IPQ8072 4x4 2.4G & 5G /QCN9074 11ax 4x4 6G M.2

wallys-wifi6

QCN9074 IPQ8072

技术科普:如何应用视觉显著性模型优化远控编码算法?

贝锐

算法 编码器 视觉策略 远程控制 向日葵

首次全面解析云原生成熟度模型:解决企业「诊断难、规划难、选型难」问题

阿里巴巴中间件

阿里云 中间件 成熟度

Alibaba最新发布!耗时182天肝出来1028页分布式全栈手册太香了

了不起的程序猿

Java 阿里巴巴 分布式 java程序员

别搞Java面试八股文背诵版了! 真卷不动了...

退休的汤姆

Java 程序员 面经 社招 秋招

从实例出发,算力网络到底是如何编排的?

鲸品堂

算力网络

企业知识管理平台在企业中扮演什么样的角色?

Baklib

知识管理

复享光学发布ZURO系列光谱仪 助力中国半导体产业国产化

硬科技星球

LeaRun低代码平台 助力中小企业快速开发MES系统

力软低代码开发平台

共探人工智能新发展,AICON 2022 即将重磅开启

Geek_2d6073

天呐,我居然可以隔空作画了

华为云开发者联盟

人工智能 华为云 企业号九月金秋榜

个推TechDay直播回顾 | 分享基于Flink的实时数仓搭建秘诀

个推

《数字经济全景白皮书》证券数字化篇 重磅发布!

易观分析

金融 证券

十问 RocketMQ:十年再出发,到底有何不同?

阿里巴巴中间件

阿里云 RocketMQ 云原生 中间件

开发NFT数字藏品平台:定制搭建NFT系统

开源直播系统源码

NFT 数字藏品 数字藏品开发 数字藏品系统

如何用AscendCL的接口开发网络模型推理场景下应用?

华为云开发者联盟

人工智能 企业号九月金秋榜

腾讯云5G边缘计算拿下Linux基金会奖项,降低40%云游戏网络时延

科技热闻

开源密码管理器更安全吗?(1)

神锁离线版

开源 数据安全 密码管理 开源安全 开源软件

想了解Python中的super 函数么

华为云开发者联盟

Python 开发 企业号九月金秋榜

经验分享|分享搭建在线帮助中心的方法

Baklib

避免在.NET代码中出现不恰当依赖_.NET_Patrick Smacchia_InfoQ精选文章