技术开发 频道

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

  裁剪依赖环

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

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

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

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

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

// <Name>避免命名空间相互依赖</Name>
warnif count
> 0
// 这条规则列出所有相互依赖的命名空间对。
// 命名空间对格式{ first, second }表明第一个命名空间不应该使用第二个命名空间。
// 格式中的first/second顺序是由被彼此使用的类型的个数推到出来的。
// 如果第一个命名空间使用第二个命名空间的类型的个数比相反的少,
// 则表明第一个命名空间相对于第二个来说在组织结构中处于更低层级。
//
// 找出相互依赖的两个命名空间的耦合点:
// 1) 将第一个命名空间导出到依赖矩阵的垂直方向头部。
// 2) 将第二个命名空间导出到依赖矩阵的水平方向头部。
// 3) 双击黑色单元格。
// 4) 在矩阵命令工具条中,点击按钮:Remove empty Row(s) en Column(s)。
// 到这里,依赖矩阵就显示出了导致耦合的类型。
//
// 遵循这条规则能有效地避免出现命名空间依赖环。
// 可以在我们的关于分解代码的白皮书中找到这方面的更多内容。
// http://www.ndepend.com/WhiteBooks.aspx


// 优化:限定程序集的范围
// 如果命名空间是相互依赖的
// - 则它们必定在同一个程序集中被声明
// - 父程序集必定ContainsNamespaceDependencyCycle
from assembly in Application.Assemblies.Where(a
=> a.ContainsNamespaceDependencyCycle != null && a.ContainsNamespaceDependencyCycle.Value)

// hashset用来避免重复报告 A <-> B and B <-> A
let hashset = new HashSet<INamespace>()

// 优化:限定命名空间集合
// 如果一个命名空间没有Level值,则它必定在依赖环中,
// 或者直接或间接地使用了某个依赖环。
let namespacesSuspect = assembly.ChildNamespaces.Where(n => n.Level == null)

from nA in namespacesSuspect

// 使用nA选择相互依赖的命名空间
let unused = hashset.Add(nA) // Populate hashset
let namespacesMutuallyDependentWith_nA = nA.NamespacesUsed.Using(nA)
      .Except(hashset)
// <-- 避免重复报告 A <-> B and B <-> A
where namespacesMutuallyDependentWith_nA.Count()
> 0

from nB in namespacesMutuallyDependentWith_nA

// nA和nB是相互依赖的。
// 首先选择不应该使用另一个的那个。
// 第一个命名空间是由它使用的第二个命名空间的类型的个数更少这个事实推导出来的。
let typesOfBUsedByA = nB.ChildTypes.UsedBy(nA)
let typesOfAUsedByB = nA.ChildTypes.UsedBy(nB)
let first = (typesOfBUsedByA.Count() > typesOfAUsedByB.Count()) ? nB : nA
let second = (first == nA) ? nB : nA
let typesOfFirstUsedBySecond = (first == nA) ? typesOfAUsedByB : typesOfBUsedByA
let typesOfSecondUsedByFirst = (first == nA) ? typesOfBUsedByA : typesOfAUsedByB
select new { first, shouldntUse = second, typesOfFirstUsedBySecond, typesOfSecondUsedByFirst }

  当你解除了所有相互依赖的命名空间对之后,第一条代码规范可能仍然会报告存在依赖环。这是因为你可能会遇到由至少三个命名空间组成的依赖环,即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的首席开发人员,百忙之中他还会安排时间享受软件技术的多个领域给他带来的乐趣。

  译文链接:http://www.infoq.com/cn/articles/NDepend

  原文链接:http://www.infoq.com/articles/NDepend

  本文转载自其它媒体,转载目的在于传递更多信息,并不代表本网赞同其观点和对其真实性负责。

0
相关文章